updating configuration

This commit is contained in:
Your Name
2026-07-14 14:12:07 +00:00
parent 456f43d435
commit 6199c62362
83 changed files with 552 additions and 364 deletions
+3 -3
View File
@@ -1,4 +1,5 @@
import datetime
import globals
from automation import Automation, Base # type: ignore
#
@@ -21,7 +22,7 @@ class Balance(Base):
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
@@ -61,5 +62,4 @@ class Balance(Base):
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)
globals.send_telegram(self, msg, target = self.args['notify'])
+66 -65
View File
@@ -1,65 +1,66 @@
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")
import appdaemon.plugins.hass.hassapi as hass
import datetime
import globals
#
# 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 != "":
globals.send_telegram(self, message)
@@ -1,5 +1,6 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
import globals
class BMSMonitor(hass.Hass):
def initialize(self):
@@ -17,7 +18,7 @@ class BMSMonitor(hass.Hass):
self.current_state = "IDLE" # IDLE, CHARGING, DISCHARGING
self.timer_handle = None
self.was_charging = False
# Переменная для хранения времени начала отключения
self.outage_start_time = None
@@ -45,15 +46,15 @@ class BMSMonitor(hass.Hass):
# Логика переходов состояний
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"):
@@ -62,14 +63,14 @@ class BMSMonitor(hass.Hass):
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":
# Можно добавить отдельное уведомление, если нужно
@@ -84,14 +85,14 @@ class BMSMonitor(hass.Hass):
soc = float(new)
if soc >= 100 and self.current_state == "CHARGING" and not self.was_charging:
# Небольшая защита от повторных уведомлений
self.was_charging = True
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.send_status_report()
self.timer_handle = self.run_every(self.periodic_status, datetime.datetime.now(), self.interval)
self.log("Таймер статуса разрядки запущен")
@@ -113,12 +114,12 @@ class BMSMonitor(hass.Hass):
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):
@@ -134,16 +135,16 @@ class BMSMonitor(hass.Hass):
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"
@@ -159,7 +160,6 @@ class BMSMonitor(hass.Hass):
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)
globals.send_telegram(self, message, target = self.notification_target, parse_mode = parse_mode)
except Exception as e:
self.log(f"Ошибка отправки Telegram: {e}")
self.log(f"Ошибка отправки Telegram: {e}")
+45 -30
View File
@@ -1,6 +1,4 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
#
# App to show dishwasher state
@@ -18,58 +16,75 @@ import datetime
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:
if 'run_state' not in self.args or 'door_sensor' not in self.args or 'counter' not in self.args or 'state_extended' 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'
self.listen_event_handle_list.append(self.listen_state(self.door_state_change, self.args['door_sensor']))
self.listen_event_handle_list.append(self.listen_state(self.run_state_change, self.args['run_state']))
def get_extended_state(self):
return self.get_state(self.args['state_extended'])
def set_extended_state(self, state):
self.call_service("input_select/select_option", entity_id = self.args['state_extended'], option = state)
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()
if new != "on":
return
self.call_service("counter/increment", entity_id = self.args['counter'])
extended_state = self.get_extended_state()
open_count = int(self.get_state(self.args['counter']))
if extended_state == "Washed":
self.set_extended_state("Empty")
elif extended_state == "Empty":
if open_count < 10:
self.set_extended_state("Collecting Dishes")
elif open_count > 10:
self.set_extended_state("Ready to wash")
elif extended_state == "Collecting Dishes" and open_count > 10:
self.set_extended_state("Ready to wash")
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("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.set_extended_state("Washing")
elif new == "Finished":
self.log('resetting counter')
self.call_service("counter/reset", entity_id = self.args['counter'])
self.run_state = "finished"
self.set_extended_state("Washed")
self.log('Current state is: {}'.format(self.run_state))
self.log('Current state is: {}'.format(self.get_extended_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))
open_count = self.get_state(self.args['counter'])
extended_state = self.get_extended_state()
self.log('counter state={}, machine state = {}'.format(open_count, extended_state))
if (self.run_state == "finished"):
# Есть посуда, которую нужно разобрать
# red color
if extended_state == "Washed":
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
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[255, 0, 0])
elif (self.run_state == "washing"):
elif extended_state == "Washing":
self.log('washing in progress')
self.call_service("light/turn_off", entity_id = self.args['lamp'])
if 'lamp' in self.args:
self.call_service("light/turn_off", entity_id = self.args['lamp'])
elif (open_count > 10):
elif extended_state == "Ready to wash":
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
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[245, 236, 0])
else:
self.call_service("light/turn_off", entity_id = self.args['lamp'])
elif 'lamp' in self.args:
self.call_service("light/turn_off", entity_id = self.args['lamp'])
@@ -5,9 +5,10 @@ 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
counter: counter.bosch_dishwasher_door_open_count
door_sensor: binary_sensor.bosch_dishwasher_door
run_state: sensor.bosch_dishwasher_operation_state
state_extended: input_select.bosch_dishwasher_state_extended
lamp: light.espresense_kitchen_led_1
global_dependencies:
- globals
@@ -0,0 +1,90 @@
import appdaemon.plugins.hass.hassapi as hass
import random
#
# App to remind about dishwasher when someone enters the kitchen
#
# Args:
#
# state_extended = input_select with extended dishwasher state
# motion_sensors = list of motion sensors to watch
# alice = Yandex Station media_player entity
# cooldown (optional) = seconds between reminders, default 1800
# constraint (optional) = input_boolean to enable/disable the app
#
# Release Notes
#
# Version 1.0:
# Initial Version
# Version 1.1:
# Added "Ready to wash" reminders
class DishwasherReminder(hass.Hass):
PHRASES_WASHED = [
"Посудомойка готова! Пора разобрать посуду.",
"Чистая посуда сидит в посудомойке как в Open Space — пора выпустить на волю.",
"Посудомойка отработала, теперь твоя очередь — разгрузить.",
"Тарелки уже высохли и скучают, спаси их из посудомойки.",
"Посудомойка не склад для чистого, разгрузи уже.",
"Грязной посуды нет, а чистая застряла внутри — спасательная операция!",
"Посудомойка помыла всё, осталось только разобрать. Ну же!",
"Чистые тарелки устроили вечеринку в посудомойке — пора их разогнать.",
"Не забудь разобрать посудомойку, иначе завтра некуда будет класть грязное.",
"Посудомойка ждёт разгрузки — всё помыто, высушено и нетерпеливо дребезжит.",
]
PHRASES_READY_TO_WASH = [
"Посудомойка скучает без работы — пора её запустить.",
"Гора посуды накопилась, посудомойка просит смелости нажать пуск.",
"Тарелки в посудомойке собрались на митинг — жми старт и отпусти их мыться.",
"Посудомойка готова к бою, осталось только нажать кнопку.",
"Хватит кормить посудомойку тарелками — пора включить и отдохнуть.",
"Посудомойка смотрит на тебя с надеждой — запускай программу.",
"Грязная посуда накопилась как снежный ком — время пустить посудомойку в ход.",
"Твои тарелки ждут водных процедур — включай посудомойку!",
"Посудомойка обидится, если её ещё раз оставят без дела — жми пуск.",
"Посуда застряла в посудомойке без мытья — самое время нажать старт и уйти пить чай.",
]
STATE_PHRASES = {
"Washed": PHRASES_WASHED,
"Ready to wash": PHRASES_READY_TO_WASH,
}
def initialize(self):
if 'state_extended' not in self.args or 'motion_sensors' not in self.args or 'alice' not in self.args:
self.log("Please define state_extended, motion_sensors and alice")
return
self.cooldown = self.args.get('cooldown', 1800)
self.last_announced = {}
for sensor in self.args['motion_sensors']:
self.listen_state(self.motion_detected, sensor)
def motion_detected(self, entity, attribute, old, new, kwargs):
if new != "on" or old == "on":
return
# if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
# self.log('disabled by constraint')
# return
state = self.get_state(self.args['state_extended'])
phrases = self.STATE_PHRASES.get(state)
if phrases is None:
return
last = self.last_announced.get(state)
if last is not None:
elapsed = (self.datetime() - last).total_seconds()
if elapsed < self.cooldown:
self.log('cooldown active for {}, skipping'.format(state))
return
self.say(random.choice(phrases))
self.last_announced[state] = self.datetime()
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)
@@ -0,0 +1,15 @@
#################################
# DISHWASHER REMINDER #
#################################
dishwasher_reminder:
class: DishwasherReminder
module: dishwasher_reminder
# constraint: input_boolean.dishwasher_reminder
state_extended: input_select.bosch_dishwasher_state_extended
motion_sensors:
- binary_sensor.motion_kitchenset_left_occupancy
- binary_sensor.motion_kitchenset_right_occupancy
alice: media_player.yandex_station_m00r46000m53db
cooldown: 3600
global_dependencies:
- globals
+29 -28
View File
@@ -1,28 +1,29 @@
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)
import appdaemon.plugins.hass.hassapi as hass
import datetime
import globals
#
# 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')
globals.send_telegram(self, new, target = notifier)
self.log(new)
@@ -1,6 +1,7 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
import globals
#
# App to send email report for devices running low on battery
@@ -24,10 +25,10 @@ class Teeth(hass.Hass):
# Устанавливаем сброс в 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']))
@@ -78,5 +79,4 @@ class Teeth(hass.Hass):
notify = self.args["notify"]
now = datetime.datetime.now()+timedelta(hours=3)
message="Уже {}, а ты еще не почистил зубы. Самое время =)".format(now.strftime("%H:%M"))
self.notify(message, name = notify)
globals.send_telegram(self, message, target = notify)
@@ -24,7 +24,7 @@ class WaterCountersAlarm(hass.Hass):
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))
@@ -69,8 +69,8 @@ class WaterCountersAlarm(hass.Hass):
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")
globals.send_telegram(self, message)
def check_checkup_dates(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
@@ -78,7 +78,7 @@ class WaterCountersAlarm(hass.Hass):
entities = self.get_state()
if entities == None:
return
values = {}
checkup_devices = []
for entity_id in entities:
@@ -106,4 +106,4 @@ class WaterCountersAlarm(hass.Hass):
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")
globals.send_telegram(self, message)