updating configuration

This commit is contained in:
Your Name
2026-03-10 07:08:59 +00:00
commit 9498a076ee
238 changed files with 18119 additions and 0 deletions
@@ -0,0 +1,65 @@
import datetime
from automation import Automation, Base # type: ignore
#
# App to send email report for low balance
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Balance(Base):
def initialize(self):
super().initialize()
self.check_balance({"force": 1})
self.run_daily(self.check_balance, datetime.time(10, 0, 0))
def check_balance(self, kwargs):
# if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
# return
values = {}
low = []
for key in self.entity_ids:
device = self.entity_ids[key]
balance = self.get_state(device)
if balance != None and self.is_number(balance):
if float(balance) < self.args["threshold"]:
low.append(device)
values[device] = balance
message=""
name = "Алтуфьево"
if 'name' in self.args:
name = self.args['name']
if low:
message += "Пожалуйста пополните баланс ({}): (&lt; {}) \n".format(name, self.args["threshold"])
for device in low:
message += " - {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if low or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.log(message)
self.notify_tg(message) #name = "telegram_monster"
def is_number(self, s):
try:
float(s)
return True
except ValueError:
return False
def notify_tg(self, msg):
if not 'notify' in self.args:
return
extra_data = {'parse_mode': 'html'}
self.notify(msg, name = self.args['notify'], data=extra_data)
@@ -0,0 +1,61 @@
#################################
# SENSOR BATTERY LOW REMINDER #
#################################
balance_low_report:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Алтуфьево
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
epd_balance: sensor.epd_balance
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
balance_low_report_belorechenskaya:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Белореченская
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_belorechenskaya_balance
epd_balance: sensor.epd_balance_belorechenskaya
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
balance_low_report_fonvizina:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Фонвизина
entity_ids:
fonvizina_epd_balance: sensor.fonvizina_epd_balance
fonvizina_trash_balance: sensor.fonvizina_trash_balance
fonvizina_kladovka_balance: sensor.fonvizina_kladovka_balance
fonvizina_epd_mosru_balance: sensor.epd_balance_mosru_fonvizina
always_send: 0
threshold: -100
inverted: true
notify: telegram
global_dependencies:
- globals
balance_low_report_milashenkova:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Милашенкова
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_milashenkova_balance
epd_balance: sensor.epd_balance_milashenkova
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,65 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to send email report for devices running low on battery
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Battery(hass.Hass):
def initialize(self):
#self.check_batteries({"force": 1})
time = datetime.time(10, 0, 0)
self.run_daily(self.check_batteries, time)
self.check_batteries(None)
def check_batteries(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
devices = self.get_state()
values = {}
low = []
for deviceName in devices:
device = devices[deviceName]
if device == None:
continue
battery = None
if "attributes" in device:
if "battery" in device["attributes"]:
battery = device["attributes"]["battery"]
elif "battery_level" in device["attributes"]:
battery = device["attributes"]["battery_level"]
elif '_battery' in device['entity_id']:
battery = self.get_state(device['entity_id'])
if not battery.isdigit():
continue
battery = int(battery)
print('{} = {}'.format(device['entity_id'], battery))
if battery != None:
print('{} = {}'.format(device['entity_id'], battery))
if battery < self.args["threshold"] and battery != 0:
low.append(deviceName)
values[deviceName] = battery
message=""
if low:
message += "У следующих датчиков садится батарея: (< {}) \n".format(self.args["threshold"])
for device in low:
message += "{}: {}%\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if low: # or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
@@ -0,0 +1,12 @@
#################################
# SENSOR BATTERY LOW REMINDER #
#################################
sensor_battery_low_report:
class: Battery
module: battery
constraint: input_boolean.battery_report
always_send: 0
threshold: 10
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,165 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
class BMSMonitor(hass.Hass):
def initialize(self):
# Получаем настройки из apps.yaml
self.chat_id = self.args.get("chat_id")
self.power_entity = self.args.get("power_sensor")
self.soc_entity = self.args.get("soc_sensor")
self.current_entity = self.args.get("current_sensor")
self.voltage_entity = self.args.get("voltage_sensor")
self.threshold = float(self.args.get("power_threshold", 5))
self.interval = int(self.args.get("discharge_interval", 300))
self.notification_target = self.args.get("notification_target", 'telegram')
# Внутренние переменные состояния
self.current_state = "IDLE" # IDLE, CHARGING, DISCHARGING
self.timer_handle = None
self.was_charging = False
# Переменная для хранения времени начала отключения
self.outage_start_time = None
# Слушаем изменения мощности
self.listen_state(self.on_power_change, self.power_entity)
# Слушаем SOC для детектирования 100%
self.listen_state(self.on_soc_change, self.soc_entity)
self.log("BMS Monitor запущен. Ожидание изменений...")
def on_power_change(self, entity, attribute, old, new, kwargs):
try:
power = float(new)
except (ValueError, TypeError):
return
# Определяем новый режим
if power > self.threshold:
new_state = "CHARGING"
elif power < -self.threshold:
new_state = "DISCHARGING"
else:
new_state = "IDLE"
# Логика переходов состояний
if self.current_state != new_state:
self.log(f"Смена состояния BMS: {self.current_state} -> {new_state}")
# 1. Переход в разрядку (Отключили питание)
if new_state == "DISCHARGING" and self.current_state != "DISCHARGING":
# Фиксируем время начала отключения
self.outage_start_time = datetime.datetime.now()
self.send_telegram("⚠️ <b>ВНИМАНИЕ: Отключено питание!</b>\nБатарея перешла в режим разрядки.", "html")
self.start_discharge_timer()
# 2. Выход из разрядки (Питание восстановлено)
# Срабатывает при переходе DISCHARGING -> IDLE или DISCHARGING -> CHARGING
elif self.current_state == "DISCHARGING" and new_state in ("IDLE", "CHARGING"):
duration_msg = ""
if self.outage_start_time:
duration = datetime.datetime.now() - self.outage_start_time
duration_msg = f"\n⏱ <b>Время без электричества: {self._format_timedelta(duration)}</b>"
self.outage_start_time = None # Сбрасываем таймер
self.send_telegram(f"✅ <b>Питание восстановлено!</b>\nБатарея вышла из режима разрядки.{duration_msg}", "html")
self.stop_discharge_timer()
# 3. Переход в простой после зарядки (Зарядка окончена)
elif new_state == "IDLE" and self.current_state == "CHARGING":
self.send_telegram("🔋 <b>Зарядка завершена!</b>\nБатарея полностью заряжена или нагрузка отключена.", "html")
# 4. Переход IDLE -> CHARGING (если не было разрядки перед этим)
elif new_state == "CHARGING" and self.current_state == "IDLE":
# Можно добавить отдельное уведомление, если нужно
pass
self.current_state = new_state
self.was_charging = (new_state == "CHARGING")
def on_soc_change(self, entity, attribute, old, new, kwargs):
# Дополнительная проверка на 100% SOC, если мощность еще не упала в ноль
try:
soc = float(new)
if soc >= 100 and self.current_state == "CHARGING" and not self.was_charging:
# Небольшая защита от повторных уведомлений
self.was_charging = True
except (ValueError, TypeError):
pass
def start_discharge_timer(self):
if self.timer_handle is None:
# Запускаем таймер: сразу первое сообщение, потом каждые N секунд
self.send_status_report()
self.timer_handle = self.run_every(self.periodic_status, datetime.datetime.now(), self.interval)
self.log("Таймер статуса разрядки запущен")
def stop_discharge_timer(self):
if self.timer_handle is not None:
self.cancel_timer(self.timer_handle)
self.timer_handle = None
self.log("Таймер статуса разрядки остановлен")
def periodic_status(self, kwargs):
# Проверяем, все ли еще в режиме разрядки перед отправкой
if self.current_state == "DISCHARGING":
self.send_status_report()
else:
self.stop_discharge_timer()
def _format_timedelta(self, td):
"""Преобразует timedelta в строку вида 'X ч Y мин'"""
total_seconds = int(td.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, _ = divmod(remainder, 60)
parts = []
if hours > 0:
parts.append(f"{hours} ч")
parts.append(f"{minutes} мин")
return " ".join(parts)
def send_status_report(self):
try:
# Получаем значения напрямую
power_raw = self.get_state(self.power_entity)
soc_raw = self.get_state(self.soc_entity)
current_raw = self.get_state(self.current_entity)
voltage_raw = self.get_state(self.voltage_entity)
# Преобразуем и округляем средствами Python
power = round(float(power_raw), 1) if power_raw not in (None, 'unknown', 'unavailable') else 0
soc = round(float(soc_raw), 1) if soc_raw not in (None, 'unknown', 'unavailable') else 0
current = round(float(current_raw), 2) if current_raw not in (None, 'unknown', 'unavailable') else 0
voltage = round(float(voltage_raw), 2) if voltage_raw not in (None, 'unknown', 'unavailable') else 0
# Определяем цвет/иконку для направления тока
direction = "🔻 Разряд" if power < 0 else "🔺 Заряд"
# Добавляем время без электричества, если известно
outage_info = ""
if self.outage_start_time:
duration = datetime.datetime.now() - self.outage_start_time
outage_info = f"\n⏳ Без света: <b>{self._format_timedelta(duration)}</b>"
msg = (
f"📊 <b>Статус батареи (JK BMS)</b>\n\n"
f"🔋 SOC: <b>{soc}%</b>\n"
f"⚡ Мощность: <b>{power} Вт</b> ({direction})\n"
f"🔌 Ток: <b>{current} А</b>\n"
f"🔋 Напряжение: <b>{voltage} В</b>"
f"{outage_info}\n"
f"🕒 Время: {datetime.datetime.now().strftime('%H:%M:%S')}"
)
self.send_telegram(msg, "html")
except Exception as e:
self.log(f"Ошибка при отправке статуса: {e}")
def send_telegram(self, message, parse_mode="html"):
try:
extra_data = {'parse_mode': parse_mode}
self.notify(message, name = self.notification_target, data=extra_data)
except Exception as e:
self.log(f"Ошибка отправки Telegram: {e}")
@@ -0,0 +1,14 @@
bms_monitor:
module: bms_monitor
class: BMSMonitor
# Telegram настройки
notification_target: telegram_monster
# Сущности из вашей карточки
power_sensor: sensor.jk_bms_jk_bms_power
soc_sensor: sensor.jk_bms_jk_bms_state_of_charge
current_sensor: sensor.jk_bms_jk_bms_current
voltage_sensor: sensor.jk_bms_jk_bms_total_voltage
# Порог мощности (Вт) для определения режима (чтобы избежать шумов)
power_threshold: 5
# Интервал оповещений при разрядке (секунды)
discharge_interval: 300
@@ -0,0 +1,75 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
#
# App to show dishwasher state
#
# Args:
#
# door_sensor = dishwasher door sensor
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Dishwasher(hass.Hass):
listen_event_handle_list = []
run_state = ""
def initialize(self):
if 'run_state' not in self.args or 'door_sensor' not in self.args or 'counter' not in self.args:
self.log("Please define all args")
return
self.listen_event_handle_list.append(self.listen_state(self.door_state_change, self.args['door_sensor'])) #, attribute='action'
self.listen_event_handle_list.append(self.listen_state(self.run_state_change, self.args['run_state'])) #, attribute='action'
def door_state_change(self, entity, attribute, old, new, kwargs):
if (new == "on"):
self.call_service("counter/increment", entity_id = self.args['counter'])
if (self.run_state == "finished"):
self.run_state = "collecting"
self.set_light_state_by_counter()
def run_state_change(self, entity, attribute, old, new, kwargs):
self.log("event: {}: {}->{}".format(attribute,old,new))
if (new == "Run"):
self.log('resetting counter')
self.call_service("counter/reset", entity_id = self.args['counter'])
self.run_state = "washing"
if (new == "Finished"):
self.log('resetting counter')
self.call_service("counter/reset", entity_id = self.args['counter'])
self.run_state = "finished"
self.log('Current state is: {}'.format(self.run_state))
self.set_light_state_by_counter()
def set_light_state_by_counter(self):
state = self.get_state(self.args['counter'])
open_count = int(state)
self.log('counter state={}, machine state = {}'.format(state, self.run_state))
if (self.run_state == "finished"):
# Есть посуда, которую нужно разобрать
# red color
self.log('Time to take dishes out.')
if 'lamp' in self.args:
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[255, 0, 0]) #red
elif (self.run_state == "washing"):
self.log('washing in progress')
self.call_service("light/turn_off", entity_id = self.args['lamp'])
elif (open_count > 10):
self.log('Time to run dishwasher.')
if 'lamp' in self.args:
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[245, 236, 0]) #yellow
else:
self.call_service("light/turn_off", entity_id = self.args['lamp'])
@@ -0,0 +1,13 @@
#################################
# TEETH BRUSHING REMINDER #
#################################
dishwasher:
class: Dishwasher
module: dishwasher
constraint: input_boolean.dishwasher_state_light
counter: counter.dishwasher_door_open_count
door_sensor: binary_sensor.hc_dishwasher_door
run_state: sensor.hc_dishwasher_state
lamp: light.espresense_kitchen_led_1
global_dependencies:
- globals
@@ -0,0 +1,28 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to send reports about unsuccessfull auths
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Security(hass.Hass):
def initialize(self):
self.log('---------------------')
self.listen_event_handle_list = []
self.listen_event_handle_list.append(self.listen_state(self.state_change, 'persistent_notification.http_login', attribute='message'))
def state_change(self, entity, attribute, old, new, kwargs):
notifier = self.args.get('notify', 'telegram')
self.notify(new, name = notifier)
self.log(new)
@@ -0,0 +1,10 @@
#################################
# INVALID AUTH REPORT #
#################################
invalid_auth_report:
class: Security
module: security
alice: media_player.yandex_station_m00r46000m53db
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,82 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
#
# App to send email report for devices running low on battery
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Teeth(hass.Hass):
listen_event_handle_list = []
def initialize(self):
# Устанавливаем сброс в 10 утра
reset_time = datetime.time(10, 0, 0)
self.run_daily(self.reset_action, reset_time)
# Запускаем проверку напоминаний
self.timer = self.run_every(self.remind_if_needed, self.datetime()+timedelta(seconds=1), self.args['remind_interval'])
# Слушаем изменения состояния кнопки
self.listen_event_handle_list.append(self.listen_state(self.button_state_change, self.args['mute_button']))
def button_state_change(self, entity, attribute, old, new, kwargs):
self.log("event: {}={}".format(attribute,new))
if attribute == 'state' and new == 'single':
self.log('Muting notifications')
self.set_state(self.args['mute_boolean'], state='on')
self.say("Принято")
def say(self, command):
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=self.args['alice'], media_content_type='text', media_content_id=command)
def reset_action(self, kwargs):
self.log('Resetting mute state')
self.set_state(self.args['mute_boolean'], state='off')
def remind_if_needed(self, kwargs):
# Проверяем состояние input_boolean
if self.get_state(self.args['mute_boolean']) == 'on':
return
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('disabled by constraint')
return
home_sensors = []
if 'home_sensors' in self.args:
home_sensors = self.args['home_sensors']
at_home = False
is_any = False
for sensor in home_sensors:
is_any = True
state = self.get_state(sensor)
if state != 'not_home':
at_home = True
if not is_any:
at_home = True
if not at_home:
self.log('User is away, skipping notification')
return
notify = "telegram"
if "notify" in self.args:
notify = self.args["notify"]
now = datetime.datetime.now()+timedelta(hours=3)
message="Уже {}, а ты еще не почистил зубы. Самое время =)".format(now.strftime("%H:%M"))
self.notify(message, name = notify)
@@ -0,0 +1,15 @@
#################################
# TEETH BRUSHING REMINDER #
#################################
teeth_danil_brush_reminder:
class: Teeth
module: teeth_brushing
constraint: input_boolean.teeth_brushing_reminder
mute_boolean: input_boolean.teeth_muted
notify: telegram_monster
remind_interval: 1800
mute_button: sensor.button_shower_sink_action
home_sensors: [person.danil]
alice: media_player.yandex_station_ff98f029ab48468d28c557be
global_dependencies:
- globals
@@ -0,0 +1,109 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
import globals
#
# App to send notifications when counters state needs attention
#
# Args:
#
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class WaterCountersAlarm(hass.Hass):
checkup_threasold = 31
send_threasold = 31
def initialize(self):
if not globals.check_args(self, ['checkup_threasold', 'send_threasold']):
return
self.checkup_threasold = self.args['checkup_threasold']
self.send_threasold = self.args['send_threasold']
self.check_send_dates({"force": 1})
self.run_daily(self.check_checkup_dates, datetime.time(10, 0, 0))
self.run_daily(self.check_send_dates, datetime.time(10, 1, 0))
#self.check_checkup_dates([])
def check_send_dates(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
devices = self.get_state()
if devices == None:
return
values = {}
send_devices = []
for deviceName in devices:
device = devices[deviceName]
if (device == None):
continue
send_date = None
if "attributes" in device:
if "date" in device["attributes"]:
send_date = device["attributes"]["date"]
if send_date != None:
self.log('found device {} with send date {}'.format(deviceName, send_date))
if send_date == "":
send_devices.append(deviceName)
else:
send_date_date = None
if '-' in send_date:
send_date_date = datetime.datetime.strptime(send_date, '%Y-%m-%d')
else:
send_date_date = datetime.datetime.strptime(send_date, '%d.%m.%Y')
if send_date_date < (datetime.datetime.now() - datetime.timedelta(days=self.send_threasold)):
send_devices.append(deviceName)
values[deviceName] = send_date
message=""
if send_devices:
message += "ВНИМАНИЕ! У вас есть счетчики, показания по которым давно не передавались:\n"
for device in send_devices:
message += "- {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if send_devices or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
def check_checkup_dates(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
entities = self.get_state()
if entities == None:
return
values = {}
checkup_devices = []
for entity_id in entities:
entity = entities[entity_id]
if entity == None:
# self.log('not found {} in entities list.'.format(entity_id))
continue
checkup = None
if "attributes" in entity:
if "checkup" in entity["attributes"]:
checkup = entity["attributes"]["checkup"]
if checkup != None:
self.log('found device {} with checkup date {}'.format(entity_id, checkup))
checkup_date = datetime.datetime.strptime(checkup, '%Y-%m-%d')
if (checkup_date - datetime.timedelta(days=self.checkup_threasold)) < datetime.datetime.now():
checkup_devices.append(entity_id)
values[entity_id] = checkup
message=""
if checkup_devices:
message += "ВНИМАНИЕ! У вас есть счетчик, нуждающийся в поверке:\n"
for device in checkup_devices:
message += "- {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if checkup_devices or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
@@ -0,0 +1,12 @@
#################################
# WATER COUNTERS ALARM #
#################################
water_counters:
class: WaterCountersAlarm
module: water_counters
constraint: input_boolean.water_counters_report
notify: telegram
checkup_threasold: 31
send_threasold: 31
global_dependencies:
- globals