Init
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- '3.5'
|
||||||
|
install:
|
||||||
|
- 'pip3 install homeassistant'
|
||||||
|
- 'mv secrets.yaml.sample secrets.yaml'
|
||||||
|
script:
|
||||||
|
- 'cd hass/settings/ && hass -c . --script check_config | tee check.output'
|
||||||
|
- '! less check.output | grep -q ERROR'
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#SHELL := /bin/bash
|
||||||
|
|
||||||
|
ARGS = `arg="$(filter-out $@,$(MAKECMDGOALS))" && echo $${arg:-${1}}`
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@echo "Checking python syntax with pyflakes"
|
||||||
|
@pyflakes appdaemon
|
||||||
|
|
||||||
|
update_secrets_sample:
|
||||||
|
@echo "Masking passwords..."
|
||||||
|
@cat secrets.yaml | sed "s/\:.*/\: xxxxxxxxx/g" > secrets.yaml.sample #mask passwords
|
||||||
|
@sed -i "s/latitude:.*/latitude: 1.0/g" secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/longitude:.*/longitude: 1.0/g" secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/aqara1_mac:.*/aqara1_mac: xxxxxxxxxxxx/g" secrets.yaml.sample #len 12
|
||||||
|
@sed -i "s/aqara1_key:.*/aqara1_key: xxxxxxxxxxxxxxxx/g" secrets.yaml.sample #len 16
|
||||||
|
@sed -i "s/mirobot_key:.*/mirobot_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" secrets.yaml.sample #len 32
|
||||||
|
@sed -i "s/powerstrip_key:.*/powerstrip_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" secrets.yaml.sample #len 32
|
||||||
|
@sed -i "s/telegram_chat:.*/telegram_chat: 0/g" secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/timezone:.*/timezone: Europe\/London/g" secrets.yaml.sample #must be valid
|
||||||
|
|
||||||
|
commit: update_secrets_sample
|
||||||
|
git add .
|
||||||
|
git commit -m "$(call ARGS,\"updating configuration\")"
|
||||||
|
git push
|
||||||
|
|
||||||
|
%:
|
||||||
|
@:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# home-assistant-config
|
||||||
|
[](https://travis-ci.org/monster1025/hass-config)
|
||||||
|
|
||||||
|
My Home Assistant Configuration
|
||||||
|
|
||||||
|
|
||||||
|
Device List:
|
||||||
|
- Xiaomi Gateway (sensors: motion, door, plug, buttons, temperature\humidity).
|
||||||
|
- Xiaomi scale (via BLE app).
|
||||||
|
- Xiaomi robot vacuum (v1).
|
||||||
|
- Broadlink (sp-mini, rm mini).
|
||||||
|
- ESP8266 (water_valve and water counters).
|
||||||
|
- BLE Presence control (raspberry=room assistant, nut and miband as tags).
|
||||||
|
- Work presence (apacs integration).
|
||||||
|
- Yeelight WiFi bulbs.
|
||||||
|
- SOnOff relay for light.
|
||||||
|
- Yandex.Traffic
|
||||||
|
- NarodMon for 'external sensors'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
ARG VERSION
|
||||||
|
FROM acockburn/appdaemon:${VERSION}
|
||||||
|
|
||||||
|
#log encoding temporary fix
|
||||||
|
RUN sed -i -e 's/safe_enc = lambda s: str(s).encode("utf-8", "replace").decode("ascii", "replace")/safe_enc = lambda s: str(s)/g' /usr/src/app/appdaemon/utils.py
|
||||||
|
|
||||||
|
RUN pip3 install \
|
||||||
|
emp_mos_api==0.9 \
|
||||||
|
sqlalchemy \
|
||||||
|
mysqlclient
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
log:
|
||||||
|
logfile: STDOUT
|
||||||
|
errorfile: STDERR
|
||||||
|
|
||||||
|
appdaemon:
|
||||||
|
threads: 10
|
||||||
|
api_port: 5000
|
||||||
|
api_key: !secret ha_password
|
||||||
|
time_zone: !secret timezone
|
||||||
|
narodmon_key: !secret narodmon_key
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
HASS:
|
||||||
|
type: hass
|
||||||
|
ha_url: !secret ha_url
|
||||||
|
#ha_key: !secret ha_password
|
||||||
|
token: !secret ha_token
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
|
||||||
|
#
|
||||||
|
# Basic alarm clock.
|
||||||
|
# It will trigger 'alarmclock_trigger' event on alarm time.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# time = input_datetime for time (alarm time).
|
||||||
|
# state = input_boolean for alarm state (on/off)
|
||||||
|
# days = list of days when alarm is active
|
||||||
|
# prealarm_delta (optional, in minutes, default=15) = time befor main alarm, when 'alarmclock_prealarm' event will fire (you can trigger slow light on before main alarm sound).
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AlarmClock(hass.Hass):
|
||||||
|
alarmtimer = None
|
||||||
|
prealarmtimer = None
|
||||||
|
prealarm_delta = 15
|
||||||
|
def initialize(self):
|
||||||
|
if "days" not in self.args or "time" not in self.args or "state" not in self.args:
|
||||||
|
self.error("Please provide days, time and state in config!")
|
||||||
|
return
|
||||||
|
if 'prealarm_delta' in self.args:
|
||||||
|
self.prealarm_delta = self.args['prealarm_delta']
|
||||||
|
self.log('Alarm will trigger in days: {}.'.format(self.args['days']))
|
||||||
|
self.listen_state(self.alarm_time_changed, self.args['time'])
|
||||||
|
self.listen_state(self.alarm_time_changed, self.args['state'])
|
||||||
|
self.alarm_time_changed(None, None, None, None, None)
|
||||||
|
|
||||||
|
def alarm_time_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
#cacnel old timer
|
||||||
|
self.cancel_current_timer()
|
||||||
|
|
||||||
|
alarm_state = self.get_state(self.args['state'])
|
||||||
|
if alarm_state == 'off':
|
||||||
|
self.log('Turning off alarm.')
|
||||||
|
return
|
||||||
|
|
||||||
|
#set new timer
|
||||||
|
time_string = self.get_state(self.args["time"])
|
||||||
|
if time_string == "unknown":
|
||||||
|
return
|
||||||
|
alarm_time = self.parse_time(time_string)
|
||||||
|
self.run_timer(alarm_time)
|
||||||
|
|
||||||
|
def run_timer(self, alarm_time):
|
||||||
|
prealarm_time = (datetime.combine(date.min, alarm_time) - timedelta(0, self.prealarm_delta * 60)).time()
|
||||||
|
self.alarmtimer = self.run_daily(self.alarmclock_trigger, alarm_time)
|
||||||
|
self.prealarmtimer = self.run_daily(self.alarmclock_prealarm, prealarm_time)
|
||||||
|
self.log("Alarm was set to {}, prealarm: {}.".format(alarm_time, prealarm_time))
|
||||||
|
|
||||||
|
def cancel_current_timer(self):
|
||||||
|
if self.alarmtimer != None and self.prealarmtimer != None:
|
||||||
|
self.cancel_timer(self.alarmtimer)
|
||||||
|
self.cancel_timer(self.prealarmtimer)
|
||||||
|
|
||||||
|
def alarmclock_trigger(self, kwargs):
|
||||||
|
if not self.constrain_days(self.args['days']):
|
||||||
|
return
|
||||||
|
self.log("Alarmclock triggered!")
|
||||||
|
self.fire_event("alarmclock_alarm")
|
||||||
|
|
||||||
|
def alarmclock_prealarm(self, kwargs):
|
||||||
|
if not self.constrain_days(self.args['days']):
|
||||||
|
return
|
||||||
|
self.log("Pre alarmclock triggered!")
|
||||||
|
self.fire_event("alarmclock_prealarm")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#################################
|
||||||
|
# ALARM CLOCK #
|
||||||
|
#################################
|
||||||
|
alarmclock_weekdays:
|
||||||
|
module: alarm_clock
|
||||||
|
class: AlarmClock
|
||||||
|
state: input_boolean.alarmclock_weekdays
|
||||||
|
time: input_datetime.alarmclock_weekdays
|
||||||
|
days: mon,tue,wed,thu,fri
|
||||||
|
prealarm_delta: 15
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
alarmclock_weekend:
|
||||||
|
module: alarm_clock
|
||||||
|
class: AlarmClock
|
||||||
|
state: input_boolean.alarmclock_weekend
|
||||||
|
time: input_datetime.alarmclock_weekend
|
||||||
|
days: sat,sun
|
||||||
|
prealarm_delta: 15
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
#
|
||||||
|
# Very special case, this will run sound 'alarm' on xiaomi gateway based on alarm clock event (alarmclock_trigger)
|
||||||
|
# and stop it when sensor became active.
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = sensor, which will stop alarm (bath motion sensor for me)
|
||||||
|
# ringtone = (default = 20) xiaomi ringtone id.
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AlarmclockAction(hass.Hass):
|
||||||
|
timer = None
|
||||||
|
ringtone = 20
|
||||||
|
volume = 10
|
||||||
|
gw_mac = ''
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ["sensor","gw_mac"]):
|
||||||
|
return
|
||||||
|
self.gw_mac = self.args['gw_mac']
|
||||||
|
if 'ringtone' in self.args:
|
||||||
|
self.ringtone = self.args['ringtone']
|
||||||
|
self.listen_event(self.alarmclock_alarm, "alarmclock_alarm")
|
||||||
|
self.listen_event(self.alarmclock_stop, "alarmclock_stop")
|
||||||
|
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||||
|
|
||||||
|
def alarmclock_stop(self, event_id, event_args, kwargs):
|
||||||
|
self.sensor_trigger(None, None, None, 'on', None)
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
if new == "on" and self.timer != None:
|
||||||
|
self.call_service("xiaomi_aqara/stop_ringtone", gw_mac=self.gw_mac)
|
||||||
|
self.cancel_current_timer()
|
||||||
|
|
||||||
|
def alarmclock_alarm(self, event_id, event_args, kwargs):
|
||||||
|
self.log('Alarm triggered!')
|
||||||
|
self.volume = 10
|
||||||
|
self.timer = self.run_every(self.timer_tick, self.datetime(), 17)
|
||||||
|
|
||||||
|
def timer_tick(self, kwargs):
|
||||||
|
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=self.volume)
|
||||||
|
if self.volume < 100:
|
||||||
|
self.volume = self.volume + 10
|
||||||
|
|
||||||
|
def cancel_current_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.log('canceling timer')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
alarm_clock_action:
|
||||||
|
module: alarm_clock_action
|
||||||
|
class: AlarmclockAction
|
||||||
|
sensor: group.bath_motion #bath motion sensor
|
||||||
|
gw_mac: !secret aqara1_mac
|
||||||
|
ringtone: 20
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Pre alarm event.
|
||||||
|
# This app will slowly turn on the light after 'alarmclock_alarm' event. It will fire 15 mins(by default) before alarm.
|
||||||
|
# and stop when sensor became active.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# sensor = sensor, which will stop pre-alarm (bath motion sensor for me)
|
||||||
|
# entity = entity to slowly turn on. Must support brightness control.
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AlarmclockPrealarm(hass.Hass):
|
||||||
|
brightness = 1
|
||||||
|
color_temp = 588
|
||||||
|
timer = None
|
||||||
|
def initialize(self):
|
||||||
|
if "sensor" not in self.args or "entity" not in self.args:
|
||||||
|
self.error("Please provide sensor and entity in config!")
|
||||||
|
return
|
||||||
|
self.listen_event(self.alarmclock_prealarm, "alarmclock_prealarm")
|
||||||
|
self.listen_event(self.alarmclock_stop, "alarmclock_stop")
|
||||||
|
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||||
|
|
||||||
|
def alarmclock_stop(self, event_id, event_args, kwargs):
|
||||||
|
self.sensor_trigger(None, None, None, 'on', None)
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
if new == "on":
|
||||||
|
if self.timer != None:
|
||||||
|
self.brightness = 255
|
||||||
|
self.color_temp = 240
|
||||||
|
self.timer_tick(None)
|
||||||
|
self.cancel_current_timer()
|
||||||
|
|
||||||
|
def cancel_current_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.log('canceling timer')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
|
|
||||||
|
def alarmclock_prealarm(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.log('Prealarm triggered!')
|
||||||
|
self.brightness = 1
|
||||||
|
self.color_temp = 588
|
||||||
|
self.timer = self.run_every(self.timer_tick, self.datetime(), 10)
|
||||||
|
|
||||||
|
def timer_tick(self, kwargs):
|
||||||
|
attributes = self.get_state(self.args['entity'], attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
# we have a group here
|
||||||
|
if 'entity_id' in attributes:
|
||||||
|
for entity_id in attributes['entity_id']:
|
||||||
|
self.make_entity_action(entity_id, None)
|
||||||
|
else:
|
||||||
|
# simple entity
|
||||||
|
self.make_entity_action(self.args['entity'], attributes)
|
||||||
|
|
||||||
|
if (self.brightness < 245):
|
||||||
|
self.brightness = self.brightness + 2
|
||||||
|
elif (self.color_temp > 220):
|
||||||
|
self.color_temp = self.color_temp - 30
|
||||||
|
else:
|
||||||
|
self.cancel_current_timer()
|
||||||
|
self.log("brightness: {}, color_temp: {}".format(self.brightness, self.color_temp))
|
||||||
|
|
||||||
|
def make_entity_action(self, entity_id, attributes):
|
||||||
|
if attributes == None:
|
||||||
|
attributes = self.get_state(entity_id, attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
#state = self.get_state(entity_id)
|
||||||
|
self.turn_on(entity_id, brightness = self.brightness, color_temp = self.color_temp)
|
||||||
|
# self.log(attributes)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
alarm_clock_prealarm:
|
||||||
|
module: alarm_clock_prealarm
|
||||||
|
class: AlarmclockPrealarm
|
||||||
|
sensor: group.bath_motion #bath motion sensor
|
||||||
|
entity: group.mainroom_color
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import json
|
||||||
|
|
||||||
|
#
|
||||||
|
# Yandex.Alice integration
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class YandexAlice(hass.Hass):
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.register_endpoint(self.api_call)
|
||||||
|
self.sessionStorage = {}
|
||||||
|
self.log('started')
|
||||||
|
|
||||||
|
def api_call(self, request):
|
||||||
|
# js = json.dumps(request, indent=4, sort_keys=True)
|
||||||
|
# self.log('Request: %s' % js)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"version": request['version'],
|
||||||
|
"session": request['session'],
|
||||||
|
"response": {
|
||||||
|
"end_session": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.handle_dialog(request, response)
|
||||||
|
|
||||||
|
# self.log('response: {}'.format(response['response']['text']))
|
||||||
|
# js = json.dumps(response, indent=4, sort_keys=True)
|
||||||
|
# self.log('Response: %s' % js)
|
||||||
|
|
||||||
|
return response, 200
|
||||||
|
|
||||||
|
# Функция для непосредственной обработки диалога.
|
||||||
|
def handle_dialog(self, req, res):
|
||||||
|
user_id = req['session']['user_id']
|
||||||
|
command = req['request']['original_utterance'].lower()
|
||||||
|
command_password = req['request']['command'].lower().replace(" ", "")
|
||||||
|
password = "123456"
|
||||||
|
self.log('command: {}'.format(command))
|
||||||
|
|
||||||
|
if command == "ping":
|
||||||
|
res['response']['text'] = 'pong =)'
|
||||||
|
return
|
||||||
|
|
||||||
|
if user_id not in self.sessionStorage:
|
||||||
|
self.sessionStorage[user_id] = { 'auth': False }
|
||||||
|
res['response']['text'] = 'Привет. Я могу управлять твоим домом. Для начала работы назови пароль.'
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_password != password and self.sessionStorage[user_id]['auth'] == False:
|
||||||
|
res['response']['text'] = 'Неправильный пароль: %s.' % command_password
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обрабатываем ответ пользователя.
|
||||||
|
if command_password == password and self.sessionStorage[user_id]['auth'] == False:
|
||||||
|
self.sessionStorage[user_id]['auth'] = True
|
||||||
|
res['response']['text'] = 'Вы авторизованы.'
|
||||||
|
return
|
||||||
|
|
||||||
|
self.handle_command(req, res)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Функция возвращает две подсказки для ответа.
|
||||||
|
def handle_command(self, req, res):
|
||||||
|
user_id = req['session']['user_id']
|
||||||
|
command = req['request']['original_utterance'].lower()
|
||||||
|
command = command.replace('попроси управление домом', '').strip()
|
||||||
|
command = command.replace('алиса', '').strip()
|
||||||
|
isAuth = self.sessionStorage[user_id]['auth'] == True
|
||||||
|
|
||||||
|
if not isAuth:
|
||||||
|
res['response']['text'] = 'Вы не авторизованы.'
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["включи свет", "включить свет"]:
|
||||||
|
res['response']['text'] = 'Включаю свет.'
|
||||||
|
self.turn_on("group.all_lights")
|
||||||
|
self.turn_on("switch.kitchen_mini_light")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["включи свет в зале", "включить свет в зале","включи свет в гостинной", "включить свет в гостинной"]:
|
||||||
|
res['response']['text'] = 'Свет включен.'
|
||||||
|
self.turn_on("group.mainroom_light")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["выключи свет в зале", "выключить свет в зале","выключи свет в гостинной", "выключить свет в гостинной"]:
|
||||||
|
res['response']['text'] = 'Свет выключен.'
|
||||||
|
self.turn_off("group.mainroom_light")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["выключи свет", "выключить свет", "выключи весь свет"]:
|
||||||
|
res['response']['text'] = 'Свет выключен.'
|
||||||
|
self.turn_off("group.all_lights")
|
||||||
|
self.turn_off("switch.kitchen_mini_light")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["хватит", "алиса, хватит", "алиса хватит", "отмена"]:
|
||||||
|
res['response']['text'] = 'Хорошо'
|
||||||
|
res['response']['end_session'] = True
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["я ушел", "алиса я ушел"]:
|
||||||
|
self.call_service("alarm_control_panel/alarm_arm_away", entity_id="alarm_control_panel.ha_alarm")
|
||||||
|
res['response']['text'] = 'Приятной дороги.'
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["я вернулся", "я пришёл", "я пришел"]:
|
||||||
|
self.call_service("alarm_control_panel/alarm_disarm", entity_id="alarm_control_panel.ha_alarm")
|
||||||
|
res['response']['text'] = 'Добро пожаловать домой.'
|
||||||
|
return
|
||||||
|
|
||||||
|
if command in ["запусти навык управление домом"]:
|
||||||
|
res['response']['text'] = 'Уже запущен.'
|
||||||
|
return
|
||||||
|
|
||||||
|
res['response']['text'] = 'Извините, я не знаю команды %s.' % command
|
||||||
|
return
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#################################
|
||||||
|
# YANDEX.ALICE #
|
||||||
|
#################################
|
||||||
|
alice:
|
||||||
|
module: alice
|
||||||
|
class: YandexAlice
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import datetime
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# App recieves xiaomi scale readings, parse user weight and set sensor in home-assistant
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = generated sensor name
|
||||||
|
# friendly_name (optional) = sensor friendly name
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class MiScale(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ["sensor"]):
|
||||||
|
return
|
||||||
|
self.register_endpoint(self.api_call)
|
||||||
|
self.log('started')
|
||||||
|
|
||||||
|
def api_call(self, data):
|
||||||
|
# filter only my data
|
||||||
|
prev_weight = 70
|
||||||
|
weight_date = datetime.datetime.now()
|
||||||
|
for current in data:
|
||||||
|
if abs(current['Weight'] - prev_weight) > 5:
|
||||||
|
continue
|
||||||
|
# self.log(current)
|
||||||
|
prev_weight = current['Weight']
|
||||||
|
weight_date = current['Time']
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
attributes['datetime'] = weight_date
|
||||||
|
attributes['unit_of_measurement'] = 'kg'
|
||||||
|
attributes['description'] = 'Created and updated from appdaemon (mi_scale)'
|
||||||
|
if 'friendly_name' in self.args:
|
||||||
|
attributes['friendly_name'] = self.args['friendly_name']
|
||||||
|
|
||||||
|
self.set_state(self.args['sensor'], state = prev_weight, attributes = attributes)
|
||||||
|
return 'ok', 200
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#################################
|
||||||
|
# SCALE #
|
||||||
|
#################################
|
||||||
|
mi_scale:
|
||||||
|
module: mi_scale
|
||||||
|
class: MiScale
|
||||||
|
sensor: sensor.xiaomi_scale
|
||||||
|
friendly_name: "Весы"
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to serve tags list via api call
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# secrets_file = path to secrets file
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class TagsList(hass.Hass):
|
||||||
|
secrets_file = '/conf/secrets.yaml'
|
||||||
|
def initialize(self):
|
||||||
|
self.register_endpoint(self.api_call)
|
||||||
|
if 'secrets_file' in self.args:
|
||||||
|
self.secrets_file = self.args['secrets_file']
|
||||||
|
|
||||||
|
def api_call(self, data):
|
||||||
|
tags = []
|
||||||
|
|
||||||
|
with open(self.secrets_file) as f:
|
||||||
|
content = f.readlines()
|
||||||
|
for line in content:
|
||||||
|
if (line.startswith('tag_')):
|
||||||
|
line = line.replace('\n','')
|
||||||
|
tag = line.split(':')[1].strip()
|
||||||
|
tags.append(tag)
|
||||||
|
return tags, 200
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#################################
|
||||||
|
# TAGS #
|
||||||
|
#################################
|
||||||
|
tags:
|
||||||
|
module: tags
|
||||||
|
class: TagsList
|
||||||
|
secrets_file: /conf/secrets.yaml
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#################################################################
|
||||||
|
## Global
|
||||||
|
#################################################################
|
||||||
|
global_modules:
|
||||||
|
- globals
|
||||||
|
- automation
|
||||||
|
|
||||||
|
automation:
|
||||||
|
class: Automation
|
||||||
|
module: automation
|
||||||
|
#################################
|
||||||
|
# CUBE #
|
||||||
|
#################################
|
||||||
|
# cube:
|
||||||
|
# module: cube
|
||||||
|
# class: Cube
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# NARODMON #
|
||||||
|
#################################
|
||||||
|
# narodmon_sensor:
|
||||||
|
# module: narodmon_sensor
|
||||||
|
# class: NarodmonSensor
|
||||||
|
# entity: sensor.lux_outside
|
||||||
|
# friendly_name: 'Освещенность (Улица)'
|
||||||
|
# user: 1089
|
||||||
|
# sensorid: 7574
|
||||||
|
# interval: 120
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
class Base(hass.Hass):
|
||||||
|
"""Define a base automation object."""
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
# self.log('Initialize base')
|
||||||
|
|
||||||
|
# Define a holding place for HASS entity IDs:
|
||||||
|
self.entity_ids = self.args.get('entity_ids', {})
|
||||||
|
|
||||||
|
# Define a holding place for any scheduler handles that the automation
|
||||||
|
# wants to keep track of:
|
||||||
|
self.handles = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
# Define a holding place for key/value properties for this automation:
|
||||||
|
self.properties = self.args.get('properties', {})
|
||||||
|
|
||||||
|
# Take every dependecy and create a reference to it:
|
||||||
|
for app in self.args.get('dependencies', []):
|
||||||
|
if not getattr(self, app, None):
|
||||||
|
setattr(self, app, self.get_app(app))
|
||||||
|
|
||||||
|
class Automation(Base):
|
||||||
|
"""Define a base automation object."""
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().initialize()
|
||||||
|
# self.log('Initialize automation')
|
||||||
|
|
||||||
|
# Define a reference to the "manager app" – for example, a trash-
|
||||||
|
# related automation might carry a reference to TrashManager:
|
||||||
|
if self.args.get('app'):
|
||||||
|
self.app = getattr(self, self.args['app'])
|
||||||
|
|
||||||
|
# Set the entity ID of the input boolean that will control whether
|
||||||
|
# this automation is enabled or not:
|
||||||
|
self.enabled_entity_id = None # type: ignore
|
||||||
|
enabled_config = self.args.get('enabled_config', {})
|
||||||
|
if enabled_config:
|
||||||
|
if enabled_config.get('toggle_name'):
|
||||||
|
self.enabled_entity_id = 'input_boolean.{0}'.format(
|
||||||
|
enabled_config['toggle_name'])
|
||||||
|
else:
|
||||||
|
self.enabled_entity_id = 'input_boolean.{0}'.format(self.name)
|
||||||
|
|
||||||
|
# Register any "mode alterations" for this automation – for example,
|
||||||
|
# perhaps it should be disabled when Vacation Mode is enabled:
|
||||||
|
mode_alterations = self.args.get('mode_alterations', {})
|
||||||
|
if mode_alterations:
|
||||||
|
for mode, value in mode_alterations.items():
|
||||||
|
mode_app = getattr(self, mode)
|
||||||
|
mode_app.register_enabled_entity(self.enabled_entity_id, value)
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
# for PID
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Climate(hass.Hass):
|
||||||
|
ir_topic = "home/remote/rm2/code/set"
|
||||||
|
target_temp = 24.0 #default
|
||||||
|
pid = PID(P = 0.2, I=0.0, D=0.0)
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if "target_temp" not in self.args or "temp_sensor" not in self.args or "ha_panel" not in self.args or "door_window" not in self.args:
|
||||||
|
self.error("Please provide target_temp, temp_sensor, ha_panel, door_window in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.target_temp_changed, self.args['target_temp'])
|
||||||
|
self.listen_state(self.temp_sensor_changed, self.args['temp_sensor'])
|
||||||
|
self.listen_state(self.constraint_changed, self.args['constraint'])
|
||||||
|
self.listen_state(self.ha_panel_changed, self.args['ha_panel'])
|
||||||
|
self.listen_state(self.door_window_changed, self.args['door_window'])
|
||||||
|
self.target_temp_changed("", "", "", "", "")
|
||||||
|
|
||||||
|
def door_window_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def ha_panel_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def constraint_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def target_temp_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.target_temp = float(self.get_state(self.args['target_temp']))
|
||||||
|
self.pid.SetPoint = self.target_temp
|
||||||
|
self.log('Target temp changed to: ' + str(self.target_temp))
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def temp_sensor_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.log('New room temp: ' + str(new))
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def split_off(self):
|
||||||
|
remote = Remote()
|
||||||
|
code = remote.set_mode("NONE", "NONE", self.target_temp, "OFF")
|
||||||
|
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
||||||
|
|
||||||
|
def do_action(self):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
self.log("Temperature control is disabled.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверить alarm_panel
|
||||||
|
ha_panel_state = self.get_state(self.args['ha_panel'])
|
||||||
|
if (ha_panel_state != 'disarmed'):
|
||||||
|
self.log("Nobody home. Turning off split.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверить датчики дверей и окон
|
||||||
|
door_window_state = self.get_state(self.args['door_window'])
|
||||||
|
if (door_window_state != 'off'):
|
||||||
|
self.log("Balcony door is opened. Turning off split.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
room_temp = float(self.get_state(self.args['temp_sensor']))
|
||||||
|
self.pid.update(room_temp)
|
||||||
|
output = self.pid.output
|
||||||
|
self.log("PID: " + str(output) + ".")
|
||||||
|
|
||||||
|
# Определяем режим
|
||||||
|
mode = "COOLING"
|
||||||
|
room_temp = float(self.get_state(self.args['temp_sensor']))
|
||||||
|
if (room_temp >= self.target_temp):
|
||||||
|
mode = "COOLING"
|
||||||
|
else:
|
||||||
|
mode = "HEATING"
|
||||||
|
|
||||||
|
temp_diff = abs(room_temp - self.target_temp)
|
||||||
|
if (output < 0):
|
||||||
|
self.log("Temperature differs on {} degree, turn on split for {}".format(temp_diff, mode))
|
||||||
|
remote = Remote()
|
||||||
|
code = remote.set_mode(mode, "2", int(self.target_temp), "ON")
|
||||||
|
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
||||||
|
else:
|
||||||
|
self.log("Temperature is ok, turn off split.")
|
||||||
|
self.split_off()
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to control climate device (split system)
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# notify = notification platform to send notifications to
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
Library that genrates LG air conditioner remote codes
|
||||||
|
"""
|
||||||
|
FIRST_BYTE = 136 # b10001000
|
||||||
|
|
||||||
|
STATE = {
|
||||||
|
"ON": 0,
|
||||||
|
"OFF": 24, # b11000
|
||||||
|
"CHANGE_MODE": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE = {
|
||||||
|
"HEATING": 4, # b100
|
||||||
|
"AUTO": 3, # b011
|
||||||
|
"DEHUIDIFICATION": 1, # b001
|
||||||
|
"COOLING": 0,
|
||||||
|
"NONE": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMPERATURE_OFFSET = 15
|
||||||
|
|
||||||
|
FAN = {
|
||||||
|
"1": 1,
|
||||||
|
"2": 0,
|
||||||
|
"3": 2,
|
||||||
|
"4": 4, # b100
|
||||||
|
"NONE": 5, # b101
|
||||||
|
}
|
||||||
|
|
||||||
|
FIRST_HIGH = 8271
|
||||||
|
FIRST_LOW = 4298
|
||||||
|
ZERO_AND_ONE_HIGH = 439
|
||||||
|
ZERO_LOW = 647
|
||||||
|
ONE_LOW = 1709
|
||||||
|
|
||||||
|
BUFFER_SIZE = 59
|
||||||
|
|
||||||
|
def test_bit(num, offset):
|
||||||
|
"""
|
||||||
|
Test the num(int) if at the given offset bit is 1
|
||||||
|
"""
|
||||||
|
mask = 1 << offset
|
||||||
|
return num & mask
|
||||||
|
|
||||||
|
def set_bit(num, offset):
|
||||||
|
"""
|
||||||
|
Set bit to at the given offset to 1
|
||||||
|
"""
|
||||||
|
mask = 1 << offset
|
||||||
|
return num | mask
|
||||||
|
|
||||||
|
class Remote(object):
|
||||||
|
"""
|
||||||
|
Library that genrates LG air conditioner remote codes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.codes = [0] * BUFFER_SIZE
|
||||||
|
self.crc = 0
|
||||||
|
|
||||||
|
def set_mode(self, mode, fan, temperature, state):
|
||||||
|
"""
|
||||||
|
Generate code and put it in the buffer
|
||||||
|
"""
|
||||||
|
self.codes[0] = FIRST_HIGH
|
||||||
|
self.codes[1] = FIRST_LOW
|
||||||
|
self.crc = 0
|
||||||
|
|
||||||
|
self.fill_buffer(0, 8, FIRST_BYTE)
|
||||||
|
self.fill_buffer(8, 5, STATE[state])
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(13, 3, MODE['NONE'])
|
||||||
|
else:
|
||||||
|
self.fill_buffer(13, 3, MODE[mode])
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(16, 4, 0)
|
||||||
|
else:
|
||||||
|
self.fill_buffer(16, 4, temperature - TEMPERATURE_OFFSET)
|
||||||
|
|
||||||
|
self.fill_buffer(20, 1, 0) # jet
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(21, 3, FAN['NONE'])
|
||||||
|
else:
|
||||||
|
self.fill_buffer(21, 3, FAN[fan])
|
||||||
|
|
||||||
|
self.fill_buffer(24, 4, self.crc)
|
||||||
|
self.codes[BUFFER_SIZE - 1] = ZERO_AND_ONE_HIGH
|
||||||
|
self.codes = binascii.hexlify(self.lirc2broadlink(self.codes))
|
||||||
|
return self.codes
|
||||||
|
|
||||||
|
def lirc2broadlink(self, pulses):
|
||||||
|
array = bytearray()
|
||||||
|
|
||||||
|
for pulse in pulses:
|
||||||
|
pulse = int(pulse * 269 / 8192) # 32.84ms units
|
||||||
|
|
||||||
|
if pulse < 256:
|
||||||
|
array += bytearray(struct.pack('>B', pulse)) # big endian (1-byte)
|
||||||
|
else:
|
||||||
|
array += bytearray([0x00]) # indicate next number is 2-bytes
|
||||||
|
array += bytearray(struct.pack('>H', pulse)) # big endian (2-bytes)
|
||||||
|
|
||||||
|
packet = bytearray([0x26, 0x00]) # 0x26 = IR, 0x00 = no repeats
|
||||||
|
packet += bytearray(struct.pack('<H', len(array))) # little endian byte count
|
||||||
|
packet += array
|
||||||
|
packet += bytearray([0x0d, 0x05]) # IR terminator
|
||||||
|
|
||||||
|
# Add 0s to make ultimate packet size a multiple of 16 for 128-bit AES encryption.
|
||||||
|
remainder = (len(packet) + 4) % 16 # rm.send_data() adds 4-byte header (02 00 00 00)
|
||||||
|
if remainder:
|
||||||
|
packet += bytearray(16 - remainder)
|
||||||
|
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def fill_buffer(self, pos, bits, value):
|
||||||
|
"""
|
||||||
|
Fill buffer
|
||||||
|
"""
|
||||||
|
i = bits
|
||||||
|
while i > 0:
|
||||||
|
index = 2 + 2 * (pos + bits-i)
|
||||||
|
self.codes[index] = ZERO_AND_ONE_HIGH
|
||||||
|
|
||||||
|
if test_bit(value, i - 1) != 0:
|
||||||
|
self.codes[index + 1] = ONE_LOW
|
||||||
|
else:
|
||||||
|
self.codes[index + 1] = ZERO_LOW
|
||||||
|
|
||||||
|
if test_bit(value, i - 1) != 0:
|
||||||
|
bitset = 0
|
||||||
|
bitset = set_bit(bitset, (128 + i - pos - bits - 1) % 4)
|
||||||
|
self.crc = self.crc + bitset
|
||||||
|
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------- PID CONTROLLER --------------------------------------------
|
||||||
|
class PID:
|
||||||
|
"""PID Controller
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, P=0.2, I=0.0, D=0.0):
|
||||||
|
|
||||||
|
self.Kp = P
|
||||||
|
self.Ki = I
|
||||||
|
self.Kd = D
|
||||||
|
|
||||||
|
self.sample_time = 0.00
|
||||||
|
self.current_time = time.time()
|
||||||
|
self.last_time = self.current_time
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clears PID computations and coefficients"""
|
||||||
|
self.SetPoint = 0.0
|
||||||
|
|
||||||
|
self.PTerm = 0.0
|
||||||
|
self.ITerm = 0.0
|
||||||
|
self.DTerm = 0.0
|
||||||
|
self.last_error = 0.0
|
||||||
|
|
||||||
|
# Windup Guard
|
||||||
|
self.int_error = 0.0
|
||||||
|
self.windup_guard = 20.0
|
||||||
|
|
||||||
|
self.output = 0.0
|
||||||
|
|
||||||
|
def update(self, feedback_value):
|
||||||
|
"""Calculates PID value for given reference feedback
|
||||||
|
.. math::
|
||||||
|
u(t) = K_p e(t) + K_i \int_{0}^{t} e(t)dt + K_d {de}/{dt}
|
||||||
|
.. figure:: images/pid_1.png
|
||||||
|
:align: center
|
||||||
|
Test PID with Kp=1.2, Ki=1, Kd=0.001 (test_pid.py)
|
||||||
|
"""
|
||||||
|
error = self.SetPoint - feedback_value
|
||||||
|
|
||||||
|
self.current_time = time.time()
|
||||||
|
delta_time = self.current_time - self.last_time
|
||||||
|
delta_error = error - self.last_error
|
||||||
|
|
||||||
|
if (delta_time >= self.sample_time):
|
||||||
|
self.PTerm = self.Kp * error
|
||||||
|
self.ITerm += error * delta_time
|
||||||
|
|
||||||
|
if (self.ITerm < -self.windup_guard):
|
||||||
|
self.ITerm = -self.windup_guard
|
||||||
|
elif (self.ITerm > self.windup_guard):
|
||||||
|
self.ITerm = self.windup_guard
|
||||||
|
|
||||||
|
self.DTerm = 0.0
|
||||||
|
if delta_time > 0:
|
||||||
|
self.DTerm = delta_error / delta_time
|
||||||
|
|
||||||
|
# Remember last time and last error for next calculation
|
||||||
|
self.last_time = self.current_time
|
||||||
|
self.last_error = error
|
||||||
|
|
||||||
|
self.output = self.PTerm + (self.Ki * self.ITerm) + (self.Kd * self.DTerm)
|
||||||
|
|
||||||
|
def setKp(self, proportional_gain):
|
||||||
|
"""Determines how aggressively the PID reacts to the current error with setting Proportional Gain"""
|
||||||
|
self.Kp = proportional_gain
|
||||||
|
|
||||||
|
def setKi(self, integral_gain):
|
||||||
|
"""Determines how aggressively the PID reacts to the current error with setting Integral Gain"""
|
||||||
|
self.Ki = integral_gain
|
||||||
|
|
||||||
|
def setKd(self, derivative_gain):
|
||||||
|
"""Determines how aggressively the PID reacts to the current error with setting Derivative Gain"""
|
||||||
|
self.Kd = derivative_gain
|
||||||
|
|
||||||
|
def setWindup(self, windup):
|
||||||
|
"""Integral windup, also known as integrator windup or reset windup,
|
||||||
|
refers to the situation in a PID feedback controller where
|
||||||
|
a large change in setpoint occurs (say a positive change)
|
||||||
|
and the integral terms accumulates a significant error
|
||||||
|
during the rise (windup), thus overshooting and continuing
|
||||||
|
to increase as this accumulated error is unwound
|
||||||
|
(offset by errors in the other direction).
|
||||||
|
The specific problem is the excess overshooting.
|
||||||
|
"""
|
||||||
|
self.windup_guard = windup
|
||||||
|
|
||||||
|
def setSampleTime(self, sample_time):
|
||||||
|
"""PID that should be updated at a regular interval.
|
||||||
|
Based on a pre-determined sampe time, the PID decides if it should compute or return immediately.
|
||||||
|
"""
|
||||||
|
self.sample_time = sample_time
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
class Climate(hass.Hass):
|
||||||
|
ir_topic = "home/remote/rm2/code/set"
|
||||||
|
split_state = "None"
|
||||||
|
timer = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
if "ha_panel" not in self.args or "door_window" not in self.args:
|
||||||
|
self.error("Please provide temp_sensor, ha_panel, door_window in config!")
|
||||||
|
return
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.constraint_changed, self.args['constraint']))
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.ha_panel_changed, self.args['ha_panel']))
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.door_window_changed, self.args['door_window']))
|
||||||
|
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.nest_changed, 'binary_sensor.nest_y1'))
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.nest_changed, 'binary_sensor.nest_w1'))
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def nest_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def door_window_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def ha_panel_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def constraint_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.do_action()
|
||||||
|
|
||||||
|
def split_off(self):
|
||||||
|
if self.split_state != "on" and self.split_state != "None":
|
||||||
|
return
|
||||||
|
remote = Remote()
|
||||||
|
code = remote.set_mode("NONE", "NONE", 18, "OFF")
|
||||||
|
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
||||||
|
self.split_state = "off"
|
||||||
|
if (self.timer != None):
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = self.run_in(self.run_in_split_off, 15)
|
||||||
|
|
||||||
|
def run_in_split_off(self, args):
|
||||||
|
self.turn_off("switch.plug_158d00010dd98d")
|
||||||
|
|
||||||
|
def split_on(self, mode, temp):
|
||||||
|
self.turn_on("switch.plug_158d00010dd98d")
|
||||||
|
if (self.timer != None):
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = self.run_in(self.run_in_split_mode, 10, mode=mode, temp=temp, state="ON")
|
||||||
|
|
||||||
|
def run_in_split_mode(self, args):
|
||||||
|
remote = Remote()
|
||||||
|
code = remote.set_mode(args['mode'], "2", args['temp'], args['state'])
|
||||||
|
self.call_service("mqtt/publish", topic = self.ir_topic, payload = code.decode("utf-8"))
|
||||||
|
|
||||||
|
def set_color_entity(self, to):
|
||||||
|
if 'color_entity' not in self.args:
|
||||||
|
return
|
||||||
|
entity = self.args['color_entity']
|
||||||
|
if to == "hot":
|
||||||
|
self.turn_on(entity, rgb_color=[255,0,0], brightness = 100)
|
||||||
|
if to == "cold":
|
||||||
|
self.turn_on(entity, rgb_color=[0,0,255], brightness = 100)
|
||||||
|
if to == "off":
|
||||||
|
self.turn_off(entity)
|
||||||
|
|
||||||
|
def do_action(self):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
self.log("Temperature control is disabled.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверить alarm_panel
|
||||||
|
ha_panel_state = self.get_state(self.args['ha_panel'])
|
||||||
|
if (ha_panel_state != 'disarmed'):
|
||||||
|
self.log("Nobody home. Turning off split.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверить датчики дверей и окон
|
||||||
|
door_window_state = self.get_state(self.args['door_window'])
|
||||||
|
if (door_window_state != 'off'):
|
||||||
|
self.log("Balcony door is opened. Turning off split.")
|
||||||
|
self.split_off()
|
||||||
|
return
|
||||||
|
|
||||||
|
nest_target_cool = self.get_state('climate.living_room', attribute="temperature")
|
||||||
|
nest_target_hot = nest_target_cool
|
||||||
|
if nest_target_cool == None and nest_target_hot == None:
|
||||||
|
nest_target_cool = self.get_state('climate.living_room', attribute="target_temp_high")
|
||||||
|
nest_target_hot = self.get_state('climate.living_room', attribute="target_temp_low")
|
||||||
|
|
||||||
|
nest_w1 = "off"
|
||||||
|
nest_y1 = "off"
|
||||||
|
|
||||||
|
nest_w1 = self.get_state('binary_sensor.nest_w1')
|
||||||
|
nest_y1 = self.get_state('binary_sensor.nest_y1')
|
||||||
|
|
||||||
|
if nest_y1 == "on":
|
||||||
|
self.split_state = "on"
|
||||||
|
self.log("Turn on split for COOLING to {}.".format(nest_target_cool))
|
||||||
|
self.split_on("COOLING", 20)
|
||||||
|
self.set_color_entity('cold')
|
||||||
|
|
||||||
|
elif nest_w1 == "on":
|
||||||
|
self.split_state = "on"
|
||||||
|
self.log("Turn on split for HEATING to {}".format(nest_target_hot))
|
||||||
|
self.split_on("HEATING", 28)
|
||||||
|
self.set_color_entity('hot')
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.log("Nest is reporting that temp is ok.")
|
||||||
|
self.split_off()
|
||||||
|
self.set_color_entity('off')
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if self.listen_event_handle_list != None:
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to control climate device (split system)
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# notify = notification platform to send notifications to
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
Library that genrates LG air conditioner remote codes
|
||||||
|
"""
|
||||||
|
FIRST_BYTE = 136 # b10001000
|
||||||
|
|
||||||
|
STATE = {
|
||||||
|
"ON": 0,
|
||||||
|
"OFF": 24, # b11000
|
||||||
|
"CHANGE_MODE": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE = {
|
||||||
|
"HEATING": 4, # b100
|
||||||
|
"AUTO": 3, # b011
|
||||||
|
"DEHUIDIFICATION": 1, # b001
|
||||||
|
"COOLING": 0,
|
||||||
|
"NONE": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMPERATURE_OFFSET = 15
|
||||||
|
|
||||||
|
FAN = {
|
||||||
|
"1": 1,
|
||||||
|
"2": 0,
|
||||||
|
"3": 2,
|
||||||
|
"4": 4, # b100
|
||||||
|
"NONE": 5, # b101
|
||||||
|
}
|
||||||
|
|
||||||
|
FIRST_HIGH = 8271
|
||||||
|
FIRST_LOW = 4298
|
||||||
|
ZERO_AND_ONE_HIGH = 439
|
||||||
|
ZERO_LOW = 647
|
||||||
|
ONE_LOW = 1709
|
||||||
|
|
||||||
|
BUFFER_SIZE = 59
|
||||||
|
|
||||||
|
def test_bit(num, offset):
|
||||||
|
"""
|
||||||
|
Test the num(int) if at the given offset bit is 1
|
||||||
|
"""
|
||||||
|
mask = 1 << offset
|
||||||
|
return num & mask
|
||||||
|
|
||||||
|
def set_bit(num, offset):
|
||||||
|
"""
|
||||||
|
Set bit to at the given offset to 1
|
||||||
|
"""
|
||||||
|
mask = 1 << offset
|
||||||
|
return num | mask
|
||||||
|
|
||||||
|
class Remote(object):
|
||||||
|
"""
|
||||||
|
Library that genrates LG air conditioner remote codes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.codes = [0] * BUFFER_SIZE
|
||||||
|
self.crc = 0
|
||||||
|
|
||||||
|
def set_mode(self, mode, fan, temperature, state):
|
||||||
|
"""
|
||||||
|
Generate code and put it in the buffer
|
||||||
|
"""
|
||||||
|
self.codes[0] = FIRST_HIGH
|
||||||
|
self.codes[1] = FIRST_LOW
|
||||||
|
self.crc = 0
|
||||||
|
|
||||||
|
self.fill_buffer(0, 8, FIRST_BYTE)
|
||||||
|
self.fill_buffer(8, 5, STATE[state])
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(13, 3, MODE['NONE'])
|
||||||
|
else:
|
||||||
|
self.fill_buffer(13, 3, MODE[mode])
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(16, 4, 0)
|
||||||
|
else:
|
||||||
|
self.fill_buffer(16, 4, temperature - TEMPERATURE_OFFSET)
|
||||||
|
|
||||||
|
self.fill_buffer(20, 1, 0) # jet
|
||||||
|
|
||||||
|
if state == 'OFF':
|
||||||
|
self.fill_buffer(21, 3, FAN['NONE'])
|
||||||
|
else:
|
||||||
|
self.fill_buffer(21, 3, FAN[fan])
|
||||||
|
|
||||||
|
self.fill_buffer(24, 4, self.crc)
|
||||||
|
self.codes[BUFFER_SIZE - 1] = ZERO_AND_ONE_HIGH
|
||||||
|
self.codes = binascii.hexlify(self.lirc2broadlink(self.codes))
|
||||||
|
return self.codes
|
||||||
|
|
||||||
|
def lirc2broadlink(self, pulses):
|
||||||
|
array = bytearray()
|
||||||
|
|
||||||
|
for pulse in pulses:
|
||||||
|
pulse = int(pulse * 269 / 8192) # 32.84ms units
|
||||||
|
|
||||||
|
if pulse < 256:
|
||||||
|
array += bytearray(struct.pack('>B', pulse)) # big endian (1-byte)
|
||||||
|
else:
|
||||||
|
array += bytearray([0x00]) # indicate next number is 2-bytes
|
||||||
|
array += bytearray(struct.pack('>H', pulse)) # big endian (2-bytes)
|
||||||
|
|
||||||
|
packet = bytearray([0x26, 0x00]) # 0x26 = IR, 0x00 = no repeats
|
||||||
|
packet += bytearray(struct.pack('<H', len(array))) # little endian byte count
|
||||||
|
packet += array
|
||||||
|
packet += bytearray([0x0d, 0x05]) # IR terminator
|
||||||
|
|
||||||
|
# Add 0s to make ultimate packet size a multiple of 16 for 128-bit AES encryption.
|
||||||
|
remainder = (len(packet) + 4) % 16 # rm.send_data() adds 4-byte header (02 00 00 00)
|
||||||
|
if remainder:
|
||||||
|
packet += bytearray(16 - remainder)
|
||||||
|
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def fill_buffer(self, pos, bits, value):
|
||||||
|
"""
|
||||||
|
Fill buffer
|
||||||
|
"""
|
||||||
|
i = bits
|
||||||
|
while i > 0:
|
||||||
|
index = 2 + 2 * (pos + bits-i)
|
||||||
|
self.codes[index] = ZERO_AND_ONE_HIGH
|
||||||
|
|
||||||
|
if test_bit(value, i - 1) != 0:
|
||||||
|
self.codes[index + 1] = ONE_LOW
|
||||||
|
else:
|
||||||
|
self.codes[index + 1] = ZERO_LOW
|
||||||
|
|
||||||
|
if test_bit(value, i - 1) != 0:
|
||||||
|
bitset = 0
|
||||||
|
bitset = set_bit(bitset, (128 + i - pos - bits - 1) % 4)
|
||||||
|
self.crc = self.crc + bitset
|
||||||
|
|
||||||
|
i -= 1
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#################################
|
||||||
|
# CLIMATE #
|
||||||
|
#################################
|
||||||
|
mainroom_climate:
|
||||||
|
module: climate
|
||||||
|
class: Climate
|
||||||
|
constraint: input_boolean.mainroom_climate_control
|
||||||
|
target_temp: input_number.mainroom_temperature
|
||||||
|
temp_sensor: sensor.temperature_158d0001149b3c #mainroom temp sensor
|
||||||
|
ha_panel: alarm_control_panel.ha_alarm
|
||||||
|
door_window: group.mainroom_door_window
|
||||||
|
color_entity: light.gateway_light_f0b429aa1463
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#cube_tap_twice
|
||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# This app reacts to xiaomi cube events
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Cube(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
self.listen_event(self.cube_flip90, "cube_flip90")
|
||||||
|
self.listen_event(self.cube_flip180, "cube_flip180")
|
||||||
|
self.listen_event(self.cube_shake_air, "cube_shake_air")
|
||||||
|
self.listen_event(self.cube_move, "cube_move")
|
||||||
|
self.listen_event(self.cube_tap_twice, "cube_tap_twice")
|
||||||
|
self.listen_event(self.cube_swing, "cube_swing")
|
||||||
|
|
||||||
|
def cube_flip90(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_flip90")
|
||||||
|
# turn on next light
|
||||||
|
self.fire_event("button_mainroom_click")
|
||||||
|
|
||||||
|
def cube_flip180(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_flip180")
|
||||||
|
|
||||||
|
def cube_shake_air(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_shake_air")
|
||||||
|
|
||||||
|
def cube_move(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_move")
|
||||||
|
|
||||||
|
def cube_swing(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_swing")
|
||||||
|
|
||||||
|
def cube_tap_twice(self, event_id, event_args, kwargs):
|
||||||
|
self.log("cube_tap_twice")
|
||||||
|
attributes = self.get_state("group.mainroom_light", attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
self.log(attributes)
|
||||||
|
for entity_id in attributes["entity_id"]:
|
||||||
|
self.turn_off(entity_id)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to send notification when doorbell is ringing
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# notify = notification platform to send notifications to
|
||||||
|
# ringtone (optional) = ringtone to play
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Doorbell(hass.Hass):
|
||||||
|
ringtone = 10
|
||||||
|
gw_mac = ''
|
||||||
|
def initialize(self):
|
||||||
|
if "notify" not in self.args or "sensor" not in self.args:
|
||||||
|
self.error("Please provide notify, sensor in config!")
|
||||||
|
return
|
||||||
|
if "gw_mac" in self.args:
|
||||||
|
self.gw_mac = self.args['gw_mac']
|
||||||
|
|
||||||
|
if 'ringtone' in self.args:
|
||||||
|
self.ringtone = self.args['ringtone']
|
||||||
|
|
||||||
|
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
if new != "off":
|
||||||
|
return
|
||||||
|
self.log("doorbell call")
|
||||||
|
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=100)
|
||||||
|
self.notify("Звонок в дверь!!!", name = self.args['notify'])
|
||||||
|
for x in range(3):
|
||||||
|
self.send_image()
|
||||||
|
|
||||||
|
|
||||||
|
def send_image(self):
|
||||||
|
self.call_service("camera/snapshot", entity_id="camera.entrance", filename="/config/www/camera_image.jpg")
|
||||||
|
extra_data = {'photo': {'file':'/config/www/camera_image.jpg', 'capture': 'Фото звонившего.'}}
|
||||||
|
self.notify("Фото звонившего", name = self.args['notify'], data=extra_data)
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#################################
|
||||||
|
# INTERCOM #
|
||||||
|
#################################
|
||||||
|
doorbell:
|
||||||
|
module: doorbell
|
||||||
|
class: Doorbell
|
||||||
|
sensor: binary_sensor.door_window_sensor_158d0001f34374
|
||||||
|
notify: telegram
|
||||||
|
gw_mac: !secret aqara1_mac
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to send notification when intercom is ringing
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# notify = notification platform to send notifications to
|
||||||
|
# ringtone (optional) = ringtone to play
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Intercom(hass.Hass):
|
||||||
|
ringtone = 10
|
||||||
|
gw_mac = ''
|
||||||
|
def initialize(self):
|
||||||
|
if "notify" not in self.args:
|
||||||
|
self.error("Please provide notify in config!")
|
||||||
|
return
|
||||||
|
if "gw_mac" in self.args:
|
||||||
|
self.gw_mac = self.args['gw_mac']
|
||||||
|
|
||||||
|
if 'ringtone' in self.args:
|
||||||
|
self.ringtone = self.args['ringtone']
|
||||||
|
self.listen_event(self.domofon_call, "domofon_call")
|
||||||
|
|
||||||
|
def domofon_call(self, event_id, event_args, kwargs):
|
||||||
|
self.log("domofon_call")
|
||||||
|
self.log(event_args)
|
||||||
|
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=100)
|
||||||
|
self.notify("Звонок в домофон!!!", name = self.args['notify'])
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#################################
|
||||||
|
# INTERCOM #
|
||||||
|
#################################
|
||||||
|
intercom:
|
||||||
|
module: intercom
|
||||||
|
class: Intercom
|
||||||
|
notify: telegram
|
||||||
|
gw_mac: !secret aqara1_mac
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# This app fix event turn off after hass restart
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AutomationFix(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
self.listen_event(self.ha_event, "plugin_started")
|
||||||
|
|
||||||
|
def ha_event(self, event_name, data, kwargs):
|
||||||
|
self.run_in(self.turn_on_automations_timer, 10)
|
||||||
|
|
||||||
|
def turn_on_automations_timer(self, kwargs):
|
||||||
|
self.log('turning on automations')
|
||||||
|
self.call_service('automation/turn_on', entity_id='group.all_automations')
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#################################
|
||||||
|
# FIXES #
|
||||||
|
#################################
|
||||||
|
automation_fix:
|
||||||
|
module: automation_fix
|
||||||
|
class: AutomationFix
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Tuple, Union
|
||||||
|
|
||||||
|
#
|
||||||
|
# Global module
|
||||||
|
# contain global functions
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
"""Define various constants."""
|
||||||
|
CONF_PEOPLE = 'people'
|
||||||
|
|
||||||
|
def most_common(the_list: list) -> Any:
|
||||||
|
"""Return the most common element in a list."""
|
||||||
|
return max(set(the_list), key=the_list.count)
|
||||||
|
|
||||||
|
def check_properties(self, args):
|
||||||
|
missing = []
|
||||||
|
for arg in args:
|
||||||
|
if arg not in self.properties:
|
||||||
|
missing.append(arg)
|
||||||
|
if len(missing) > 0:
|
||||||
|
self.error("Please provide {} in properties!".format(missing))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_args(self, args):
|
||||||
|
missing = []
|
||||||
|
for arg in args:
|
||||||
|
if arg not in self.args:
|
||||||
|
missing.append(arg)
|
||||||
|
if len(missing) > 0:
|
||||||
|
self.error("Please provide {} in config!".format(missing))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_group_entities(self, group):
|
||||||
|
attributes = self.get_state(group, attribute = "all")
|
||||||
|
if (attributes == None):
|
||||||
|
self.log('Warning: Possible invalid group: {}'.format(group))
|
||||||
|
return []
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
|
||||||
|
#group
|
||||||
|
if "entity_id" in attributes:
|
||||||
|
return attributes["entity_id"]
|
||||||
|
|
||||||
|
#simple item
|
||||||
|
return [group]
|
||||||
|
|
||||||
|
def turn_on(self, entity_id, state):
|
||||||
|
if entity_id.startswith("light"):
|
||||||
|
self.call_service("light/turn_off", entity_id = entity_id, transition = 1, brightness = 255)
|
||||||
|
else:
|
||||||
|
self.turn_off(entity_id, state)
|
||||||
|
|
||||||
|
def turn_off(self, entity_id, state):
|
||||||
|
if entity_id.startswith("light"):
|
||||||
|
self.call_service("light/turn_off", entity_id = entity_id, transition = 1)
|
||||||
|
else:
|
||||||
|
self.turn_off(entity_id, state)
|
||||||
|
|
||||||
|
def notification(self, to, message):
|
||||||
|
self.notify(message, name = "telegram")
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
|
||||||
|
class KitchenLedController(hass.Hass):
|
||||||
|
listen_event_handle_list = []
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self,['watch_entity']):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.entity_changed, self.args['watch_entity']))
|
||||||
|
|
||||||
|
def entity_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
stete = "off"
|
||||||
|
if new == "off":
|
||||||
|
stete = "off"
|
||||||
|
if new == "on":
|
||||||
|
stete = "on"
|
||||||
|
self.call_service("mqtt/publish", topic = "home/kitchen-led/kitchen-led/state/set", payload = stete)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#################################
|
||||||
|
# LIGHT BUTTONS #
|
||||||
|
#################################
|
||||||
|
kitchen_led_controller:
|
||||||
|
module: kitchen_led_controller
|
||||||
|
class: KitchenLedController
|
||||||
|
watch_entity: light.kitchen
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
|
||||||
|
class LightButton(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self,['entities', 'sensor']):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
self.listen_event_handle_list.append(self.listen_event(self.event_detected, "xiaomi_aqara.click"))
|
||||||
|
|
||||||
|
def event_detected(self, event_name, data, kwargs):
|
||||||
|
if data["entity_id"] != self.args["sensor"]:
|
||||||
|
return
|
||||||
|
if data["click_type"] == "single":
|
||||||
|
self.log('button_click')
|
||||||
|
self.make_action_by_mode("off")
|
||||||
|
if data["click_type"] == "double":
|
||||||
|
self.log('button_dblclick')
|
||||||
|
self.make_action_by_mode("on")
|
||||||
|
|
||||||
|
def make_action_by_mode(self, state):
|
||||||
|
states = self.fill_states()
|
||||||
|
control_entity = self.args.get('control_input', None)
|
||||||
|
if (control_entity != None):
|
||||||
|
entity_id = next(iter(states))
|
||||||
|
state = self.get_state(entity_id)
|
||||||
|
self.set_state(entity_id, self.invert_state(state))
|
||||||
|
self.set_state(control_entity, state)
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self.args["entities"]) > 1:
|
||||||
|
self.toggle_next_with_state(state, states)
|
||||||
|
|
||||||
|
def toggle_next_with_state(self, state, states):
|
||||||
|
for entity_id in states:
|
||||||
|
entity_state = states[entity_id]
|
||||||
|
if (entity_state == state):
|
||||||
|
self.set_state(entity_id, self.invert_state(state))
|
||||||
|
self.log('toggling {}'.format(entity_id))
|
||||||
|
return
|
||||||
|
|
||||||
|
for entity_id in states:
|
||||||
|
self.log('toggling {}'.format(entity_id))
|
||||||
|
self.toggle(entity_id)
|
||||||
|
|
||||||
|
def set_state(self, entity_id, state):
|
||||||
|
if (state == "off"):
|
||||||
|
self.log('turn_off {}'.format(entity_id))
|
||||||
|
self.set_off(entity_id)
|
||||||
|
if (state == "on"):
|
||||||
|
self.log('turn_on {}'.format(entity_id))
|
||||||
|
self.set_on(entity_id)
|
||||||
|
|
||||||
|
def set_on(self, entity_id):
|
||||||
|
if entity_id.startswith("light"):
|
||||||
|
self.call_service("light/turn_on", entity_id = entity_id, transition = 1, brightness = 255)
|
||||||
|
else:
|
||||||
|
self.turn_on(entity_id)
|
||||||
|
|
||||||
|
def set_off(self, entity_id):
|
||||||
|
if entity_id.startswith("light"):
|
||||||
|
self.call_service("light/turn_off", entity_id = entity_id, transition = 1)
|
||||||
|
else:
|
||||||
|
self.turn_off(entity_id)
|
||||||
|
|
||||||
|
|
||||||
|
def invert_state(self, state):
|
||||||
|
if (state == "off"):
|
||||||
|
return "on"
|
||||||
|
return "off"
|
||||||
|
|
||||||
|
def fill_states(self):
|
||||||
|
states = {}
|
||||||
|
for entity_id in self.split_device_list(self.args["entities"]):
|
||||||
|
state = self.get_state(entity_id)
|
||||||
|
if (state == None):
|
||||||
|
self.log("Warning! Device {} not found (state: {}).".format(entity_id, state))
|
||||||
|
continue
|
||||||
|
states[entity_id] = state
|
||||||
|
return states
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if self.listen_event_handle_list != None:
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#################################
|
||||||
|
# LIGHT BUTTONS #
|
||||||
|
#################################
|
||||||
|
light_button_main:
|
||||||
|
module: lightbutton
|
||||||
|
class: LightButton
|
||||||
|
sensor: binary_sensor.switch_158d00013f81a9
|
||||||
|
entities: light.yeelight_rgb_34ce008fe328,light.yeelight_rgb_286c071065c7,light.yeelight_white_34ce0087ff33,light.yeelight_white_f0b4290e5444,light.yeelight_white_f0b429a8f6df
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_button_hall:
|
||||||
|
module: lightbutton
|
||||||
|
class: LightButton
|
||||||
|
sensor: binary_sensor.switch_158d000102f7e6
|
||||||
|
entities: group.hall_light
|
||||||
|
control_input: input_boolean.hall_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_button_bath:
|
||||||
|
module: lightbutton
|
||||||
|
class: LightButton
|
||||||
|
sensor: binary_sensor.switch_158d0001256810
|
||||||
|
entities: group.bath_light
|
||||||
|
control_input: input_boolean.bath_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_button_kitchen:
|
||||||
|
module: lightbutton
|
||||||
|
class: LightButton
|
||||||
|
sensor: binary_sensor.switch_158d00012d5720
|
||||||
|
entities: group.kitchen_light
|
||||||
|
control_input: input_boolean.kitchen_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_button_balcony:
|
||||||
|
module: lightbutton
|
||||||
|
class: LightButton
|
||||||
|
sensor: binary_sensor.wall_switch_158d000183a5fe
|
||||||
|
entities: group.balcony_light
|
||||||
|
control_input: input_boolean.balcony_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# This app will use motion sensors to turn on and off control_entity for 'timeout' time.
|
||||||
|
# It work with xiaomi motion sensors.
|
||||||
|
# main algorithm: when sensor turns on - turn on the light; when sensor is off - set timer for 'timeout';
|
||||||
|
# if sensor goes on - turn off the timer; when timer was hit - turn off the light.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = motion sensor to use (will work with other sensor types too)
|
||||||
|
# timeout = timeout after sensor will turned off (keep in mind that xiaomi motion sensor has own timeout!)
|
||||||
|
# control_entity = entity, that will be controlled (example: group of light bulbs)
|
||||||
|
# constraint = you can define constraint (input_boolean), that will turn on and off this app.
|
||||||
|
# after_sundown (optionally) - whether to only trigger after sundown. example: True
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class LightControl(hass.Hass):
|
||||||
|
timer = None
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ["sensor", "timeout", "control_entity"]):
|
||||||
|
return
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.sensor_trigger, self.args['sensor']))
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
if 'after_sundown' in self.args and self.args['after_sundown'] and not self.sun_down():
|
||||||
|
# self.log('Sun is up, and we will turn it only after sundown.')
|
||||||
|
return
|
||||||
|
|
||||||
|
#sensor is off
|
||||||
|
if new == 'off' and old == 'on':
|
||||||
|
self.log('Sensor is off - running timer for {}s.'.format(self.args['timeout']))
|
||||||
|
self.run_timer()
|
||||||
|
#sensor is on
|
||||||
|
if new == 'on' and old == 'off':
|
||||||
|
#turnin on control entity and wait while sensor will be off, than run timer.
|
||||||
|
self.control_entity_on()
|
||||||
|
self.stop_timer()
|
||||||
|
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if self.listen_event_handle_list != None:
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
|
|
||||||
|
############ TIMER ########################
|
||||||
|
def run_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.stop_timer()
|
||||||
|
self.timer = self.run_in(self.control_entity_off, self.args['timeout'])
|
||||||
|
|
||||||
|
def control_entity_on(self):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.get_state(self.args['control_entity']) == 'off':
|
||||||
|
self.turn_on(self.args['control_entity'])
|
||||||
|
self.log("Light on ({}).".format(self.args['control_entity']))
|
||||||
|
|
||||||
|
def control_entity_off(self, kwargs):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
if self.get_state(self.args['control_entity']) == 'on':
|
||||||
|
self.turn_off(self.args['control_entity'])
|
||||||
|
self.log("Light off.")
|
||||||
|
self.stop_timer()
|
||||||
|
|
||||||
|
def stop_timer(self):
|
||||||
|
if (self.timer == None):
|
||||||
|
return
|
||||||
|
self.log('stopping timer.')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#################################
|
||||||
|
# LIGHT CONTROL #
|
||||||
|
#################################
|
||||||
|
light_control_mainroom:
|
||||||
|
module: lightcontrol
|
||||||
|
class: LightControl
|
||||||
|
sensor: group.mainroom_motion
|
||||||
|
control_entity: group.mainroom_light
|
||||||
|
timeout: 60
|
||||||
|
constraint: input_boolean.mainroom_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_control_kitchen:
|
||||||
|
module: lightcontrol
|
||||||
|
class: LightControl
|
||||||
|
sensor: group.kitchen_motion
|
||||||
|
control_entity: group.kitchen_light
|
||||||
|
timeout: 300
|
||||||
|
constraint: input_boolean.kitchen_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_control_hall:
|
||||||
|
module: lightcontrol
|
||||||
|
class: LightControl
|
||||||
|
sensor: group.hall_motion
|
||||||
|
control_entity: group.hall_light
|
||||||
|
timeout: 120
|
||||||
|
constraint: input_boolean.hall_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_control_balcony:
|
||||||
|
module: lightcontrol
|
||||||
|
class: LightControl
|
||||||
|
sensor: group.balcony_door_window
|
||||||
|
control_entity: group.balcony_light
|
||||||
|
timeout: 3
|
||||||
|
constraint: input_boolean.balcony_light_control
|
||||||
|
after_sundown: True
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# This app will use motion sensors to turn on and off control_entity for 'timeout' time.
|
||||||
|
# It work with xiaomi motion sensors.
|
||||||
|
# main algorithm: when sensor turns on - turn on the light; when sensor is off - set timer for 'timeout';
|
||||||
|
# if sensor goes on - turn off the timer; when timer was hit - turn off the light.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = motion sensor to use (will work with other sensor types too)
|
||||||
|
# timeout = timeout after sensor will turned off (keep in mind that xiaomi motion sensor has own timeout!)
|
||||||
|
# control_entity = entity, that will be controlled (example: group of light bulbs)
|
||||||
|
# constraint = you can define constraint (input_boolean), that will turn on and off this app.
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class LightControlBath(hass.Hass):
|
||||||
|
timer = None
|
||||||
|
door_state_changed = None
|
||||||
|
def initialize(self):
|
||||||
|
if "sensor" not in self.args or 'door_sensor' not in self.args or "timeout" not in self.args or "control_entity" not in self.args:
|
||||||
|
self.error("Please provide sensor, door_sensor, control_entity and timeout in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||||
|
self.listen_state(self.door_sensor_trigger, self.args['door_sensor'])
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
#sensor is off
|
||||||
|
if new == 'off' and old == 'on':
|
||||||
|
self.log('door_state_changed:{}'.format(self.door_state_changed))
|
||||||
|
if self.door_state_changed == False:
|
||||||
|
return
|
||||||
|
self.log('Sensor is off - running timer for {}s.'.format(self.args['timeout']))
|
||||||
|
self.run_timer()
|
||||||
|
#sensor is on
|
||||||
|
|
||||||
|
if new == 'on' and old == 'off':
|
||||||
|
#turnin on control entity and wait while sensor will be off, than run timer.
|
||||||
|
self.control_entity_on()
|
||||||
|
self.stop_timer()
|
||||||
|
door_state = self.get_state(self.args['door_sensor'])
|
||||||
|
if door_state == "off":
|
||||||
|
self.door_state_changed = False
|
||||||
|
else :
|
||||||
|
self.door_state_changed = None
|
||||||
|
|
||||||
|
|
||||||
|
def door_sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
#door is open
|
||||||
|
sensor_state = self.get_state(self.args['sensor'])
|
||||||
|
if new == 'on' and old == 'off' and sensor_state == 'off':
|
||||||
|
self.log('door is opened now, runnig the timer')
|
||||||
|
if self.timer == None:
|
||||||
|
self.run_timer()
|
||||||
|
if self.door_state_changed == False:
|
||||||
|
self.door_state_changed = True
|
||||||
|
self.log('door_state_changed:{}'.format(self.door_state_changed))
|
||||||
|
|
||||||
|
############ TIMER ########################
|
||||||
|
def control_entity_on(self):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
if self.get_state(self.args['control_entity']) == 'off':
|
||||||
|
self.turn_on(self.args['control_entity'])
|
||||||
|
self.log("Light on.")
|
||||||
|
|
||||||
|
def control_entity_off(self, kwargs):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
if self.door_state_changed == False:
|
||||||
|
self.log('Motion is detected inside and door is still closed - dont turn off the light')
|
||||||
|
return
|
||||||
|
if self.get_state(self.args['control_entity']) == 'on':
|
||||||
|
self.turn_off(self.args['control_entity'])
|
||||||
|
self.log("Light off.")
|
||||||
|
self.stop_timer()
|
||||||
|
|
||||||
|
def run_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.stop_timer()
|
||||||
|
self.timer = self.run_in(self.control_entity_off, self.args['timeout'])
|
||||||
|
|
||||||
|
def stop_timer(self):
|
||||||
|
if (self.timer == None):
|
||||||
|
return
|
||||||
|
self.log('stopping timer.')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#################################
|
||||||
|
# LIGHT CONTROL #
|
||||||
|
#################################
|
||||||
|
light_control_bath:
|
||||||
|
module: lightcontrol_bath
|
||||||
|
class: LightControlBath
|
||||||
|
sensor: group.bath_motion
|
||||||
|
door_sensor: binary_sensor.door_window_sensor_158d000111a469 #TODO: make it group
|
||||||
|
control_entity: group.bath_light
|
||||||
|
timeout: 60
|
||||||
|
constraint: input_boolean.bath_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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=""
|
||||||
|
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_monster")
|
||||||
|
|
||||||
|
def is_number(self, s):
|
||||||
|
try:
|
||||||
|
float(s)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#################################
|
||||||
|
# SENSOR BATTERY LOW REMINDER #
|
||||||
|
#################################
|
||||||
|
balance_low_report:
|
||||||
|
class: Balance
|
||||||
|
module: balance
|
||||||
|
entity_ids:
|
||||||
|
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
|
||||||
|
epd_balance: sensor.epd_balance
|
||||||
|
|
||||||
|
always_send: 0
|
||||||
|
threshold: -100
|
||||||
|
notify: telegram_monster
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
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 device in devices:
|
||||||
|
battery = None
|
||||||
|
if "attributes" in devices[device]:
|
||||||
|
if "battery" in devices[device]["attributes"]:
|
||||||
|
battery = devices[device]["attributes"]["battery"]
|
||||||
|
if "battery_level" in devices[device]["attributes"]:
|
||||||
|
battery = devices[device]["attributes"]["battery_level"]
|
||||||
|
if battery != None:
|
||||||
|
if battery < self.args["threshold"]:
|
||||||
|
low.append(device)
|
||||||
|
values[device] = 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,11 @@
|
|||||||
|
#################################
|
||||||
|
# SENSOR BATTERY LOW REMINDER #
|
||||||
|
#################################
|
||||||
|
sensor_battery_low_report:
|
||||||
|
class: Battery
|
||||||
|
module: battery
|
||||||
|
constraint: input_boolean.battery_report
|
||||||
|
always_send: 0
|
||||||
|
threshold: 10
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
from datetime import timedelta
|
||||||
|
import datetime
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to monitor high power usage when away.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class PowerWatchdog(hass.Hass):
|
||||||
|
listen_event_handle_list = []
|
||||||
|
notified_today = False
|
||||||
|
standby_power_limit = 10
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ['entity', 'thresold', 'ha_panel', 'water_devices']):
|
||||||
|
return
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.power_changed, self.args['entity']))
|
||||||
|
|
||||||
|
def power_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
ha_panel_state = self.get_state(self.args['ha_panel'])
|
||||||
|
if (ha_panel_state != "armed_away"):
|
||||||
|
return
|
||||||
|
# self.log('Power usage changed - {}kWh.'.format(new))
|
||||||
|
|
||||||
|
# if water devices is on - then skipping this alert
|
||||||
|
devices = self.args['water_devices']
|
||||||
|
for device in devices:
|
||||||
|
state = self.get_state(device)
|
||||||
|
attributes = self.get_state(device, attribute = "all")['attributes']
|
||||||
|
if 'load_power' not in attributes:
|
||||||
|
continue
|
||||||
|
load_power = attributes['load_power']
|
||||||
|
# self.log('{} load power is: {}'.format(device, load_power))
|
||||||
|
if state == 'on' and load_power > self.standby_power_limit:
|
||||||
|
return
|
||||||
|
|
||||||
|
power = float(new)
|
||||||
|
if power > self.args['thresold']:
|
||||||
|
if not self.notified_today:
|
||||||
|
self.notified_today = True
|
||||||
|
self.log('Power usage too much when away - {}kWh.'.format(new))
|
||||||
|
self.notify('В ваше отсутствие наблюдается высокое потребление электроэнергии ({}), не оставили ли вы работающие устройства?'.format(power), name = 'telegram')
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#################################
|
||||||
|
# SENSOR BATTERY LOW REMINDER #
|
||||||
|
#################################
|
||||||
|
watchdog_power:
|
||||||
|
class: PowerWatchdog
|
||||||
|
module: power
|
||||||
|
entity: sensor.power_active_power
|
||||||
|
ha_panel: alarm_control_panel.ha_alarm
|
||||||
|
thresold: 0.6
|
||||||
|
water_devices:
|
||||||
|
- switch.plug_158d0001a68c64 # Посудомойка
|
||||||
|
- switch.plug_158d0001a4cfb4 # стиралка
|
||||||
|
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
from datetime import timedelta
|
||||||
|
import datetime
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# App to monitor entity update time.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Watchdog(hass.Hass):
|
||||||
|
listen_event_handle_list = []
|
||||||
|
timezone = 3
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ['entity','update_interval', 'timeout']):
|
||||||
|
return
|
||||||
|
self.timer = self.run_every(self.watchdog_check, self.datetime(), self.args['update_interval'])
|
||||||
|
|
||||||
|
def watchdog_check(self, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
entity = self.args['entity']
|
||||||
|
attributes = self.get_state(entity, attribute='all')
|
||||||
|
if not 'last_changed' in attributes:
|
||||||
|
return
|
||||||
|
last_changed = attributes['last_changed']
|
||||||
|
last_changed_date = datetime.datetime.strptime(last_changed, '%Y-%m-%dT%H:%M:%S.%f+00:00') + timedelta(hours=self.timezone)
|
||||||
|
time_diff = datetime.datetime.now() - last_changed_date
|
||||||
|
if time_diff.seconds > self.args['timeout']:
|
||||||
|
self.log('Entity {} was changed {} seconds ago.'.format(entity, time_diff.seconds))
|
||||||
|
|
||||||
|
friendly_name = self.friendly_name(entity)
|
||||||
|
message = "Датчик {} ({}) не обновлялся в течении {} секунд.".format(friendly_name, entity, self.args['timeout'])
|
||||||
|
self.notify(message, name = "telegram")
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#################################
|
||||||
|
# SENSOR BATTERY LOW REMINDER #
|
||||||
|
#################################
|
||||||
|
watchdog_sensor_power_active_power:
|
||||||
|
class: Watchdog
|
||||||
|
module: watchdog
|
||||||
|
entity: sensor.power_active_power
|
||||||
|
update_interval: 60
|
||||||
|
timeout: 360
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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})
|
||||||
|
time = datetime.time(10, 0, 0)
|
||||||
|
self.run_daily(self.check_checkup_dates, time)
|
||||||
|
self.run_daily(self.check_send_dates, time)
|
||||||
|
|
||||||
|
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()
|
||||||
|
values = {}
|
||||||
|
send_devices = []
|
||||||
|
for device in devices:
|
||||||
|
send_date = None
|
||||||
|
if "attributes" in devices[device]:
|
||||||
|
if "date" in devices[device]["attributes"]:
|
||||||
|
send_date = devices[device]["attributes"]["date"]
|
||||||
|
if send_date != None:
|
||||||
|
self.log('found device {} with send date {}'.format(device, send_date))
|
||||||
|
if send_date == "":
|
||||||
|
send_devices.append(device)
|
||||||
|
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(device)
|
||||||
|
values[device] = 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
|
||||||
|
|
||||||
|
devices = self.get_state()
|
||||||
|
values = {}
|
||||||
|
checkup_devices = []
|
||||||
|
for device in devices:
|
||||||
|
checkup = None
|
||||||
|
if "attributes" in devices[device]:
|
||||||
|
if "checkup" in devices[device]["attributes"]:
|
||||||
|
checkup = devices[device]["attributes"]["checkup"]
|
||||||
|
if checkup != None:
|
||||||
|
self.log('found device {} with checkup date {}'.format(device, 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(device)
|
||||||
|
values[device] = 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,11 @@
|
|||||||
|
#################################
|
||||||
|
# WATER COUNTERS ALARM #
|
||||||
|
#################################
|
||||||
|
water_counters:
|
||||||
|
class: WaterCountersAlarm
|
||||||
|
module: water_counters
|
||||||
|
constraint: input_boolean.water_counters_report
|
||||||
|
checkup_threasold: 31
|
||||||
|
send_threasold: 31
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
#
|
||||||
|
# Person
|
||||||
|
# Script that creates persons with their properties.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Person(hass.Hass):
|
||||||
|
view_name = 'group.persons'
|
||||||
|
view_friendly_name = 'Люди'
|
||||||
|
tags = []
|
||||||
|
def initialize(self):
|
||||||
|
self.log('args: {}'.format(self.args))
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
self.add_person()
|
||||||
|
# self.listen_event_handle_list.append(self.listen_event(self.event_detected, "click"))
|
||||||
|
|
||||||
|
def event_detected(self, event_name, data, kwargs):
|
||||||
|
self.log("ButtonClicked: {}".format(data["entity_id"]))
|
||||||
|
#self.check_batteries({"force": 1})
|
||||||
|
|
||||||
|
def add_person(self):
|
||||||
|
view = self.get_state(self.view_name, attribute = 'all')
|
||||||
|
self.log(view)
|
||||||
|
if (view == None):
|
||||||
|
self.set_state(self.view_name, state="on",attributes={"view": False, "hidden": False, "assumed_state": "on", "friendly_name": self.view_friendly_name,"entity_id": []})
|
||||||
|
|
||||||
|
# attributes = {}
|
||||||
|
# attributes['unit_of_measurement'] = sensor['unit']
|
||||||
|
# attributes['description'] = 'Created and updated from appdaemon (narodmon)'
|
||||||
|
# if 'friendly_name' in self.args:
|
||||||
|
# attributes['friendly_name'] = self.args['friendly_name']
|
||||||
|
|
||||||
|
# self.set_state(self.args['entity'], state = sensor['value'], attributes = attributes)
|
||||||
|
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if self.listen_event_handle_list != None:
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import globals
|
||||||
|
from automation import Base
|
||||||
|
from globals import CONF_PEOPLE
|
||||||
|
from typing import Any, Tuple, Union
|
||||||
|
#
|
||||||
|
# Person
|
||||||
|
# Script that creates persons with their properties.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Person(Base):
|
||||||
|
"""Define a class to represent a person."""
|
||||||
|
def initialize(self) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().initialize()
|
||||||
|
self._add_person_to_globals()
|
||||||
|
# Listen for changes to the device trackers (to initiate re-rendering
|
||||||
|
# if needed):
|
||||||
|
for tracker in self.device_trackers:
|
||||||
|
self.listen_state(self._device_tracker_changed_cb, tracker)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first_name(self) -> str:
|
||||||
|
"""Return the person's name."""
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_trackers(self) -> list:
|
||||||
|
"""Return the device trackers associated with the person."""
|
||||||
|
return self.properties['device_trackers']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notifiers(self) -> list:
|
||||||
|
"""Return the notifiers associated with the person."""
|
||||||
|
return self.properties['notifiers']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def presence_sensor(self) -> str:
|
||||||
|
"""Return the entity ID of the generated presence status sensor."""
|
||||||
|
return 'sensor.{0}_presence_status'.format(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def presence_input_select(self) -> str:
|
||||||
|
"""Return the input select related to the person's presence."""
|
||||||
|
return self.properties['presence_input_select']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location(self) -> str:
|
||||||
|
"""Get the current location from combined device trackers."""
|
||||||
|
raw_location = globals.most_common([
|
||||||
|
self.get_tracker_state(tracker_entity)
|
||||||
|
for tracker_entity in self.properties['device_trackers']
|
||||||
|
])
|
||||||
|
|
||||||
|
if raw_location not in ('home', 'not_home'):
|
||||||
|
return raw_location
|
||||||
|
|
||||||
|
return self.get_state(self.presence_input_select)
|
||||||
|
|
||||||
|
#return raw_location
|
||||||
|
|
||||||
|
def _device_tracker_changed_cb( # pylint: disable=too-many-arguments
|
||||||
|
self, entity: Union[str, dict], attribute: str, old: str, new: str,
|
||||||
|
kwargs: dict) -> None:
|
||||||
|
"""Respond when a device tracker changes state."""
|
||||||
|
if new == old:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._render_presence_status_sensor()
|
||||||
|
|
||||||
|
def _add_person_to_globals(self):
|
||||||
|
self.global_vars.setdefault(CONF_PEOPLE, [])
|
||||||
|
|
||||||
|
existing_element = None
|
||||||
|
for element in self.global_vars[CONF_PEOPLE]:
|
||||||
|
if element.first_name == self.first_name:
|
||||||
|
existing_element = element
|
||||||
|
|
||||||
|
if existing_element == None:
|
||||||
|
self.global_vars[CONF_PEOPLE].append(self)
|
||||||
|
else:
|
||||||
|
index = self.global_vars[CONF_PEOPLE].index(existing_element)
|
||||||
|
self.global_vars[CONF_PEOPLE][index] = self
|
||||||
|
|
||||||
|
def _render_presence_status_sensor(self):
|
||||||
|
"""Update the presence status sensor."""
|
||||||
|
if self.location in ('Home', 'Just Arrived'):
|
||||||
|
picture_state = 'home'
|
||||||
|
else:
|
||||||
|
picture_state = 'away'
|
||||||
|
self.log('Setting presence sensor {} state to {}.'.format(self.presence_sensor, self.location))
|
||||||
|
self.set_state(
|
||||||
|
self.presence_sensor,
|
||||||
|
state=self.location,
|
||||||
|
attributes={
|
||||||
|
'friendly_name':
|
||||||
|
self.first_name,
|
||||||
|
'entity_picture':
|
||||||
|
'/local/{0}-{1}.png'.format(self.name, picture_state),
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
danil:
|
||||||
|
module: person
|
||||||
|
class: Person
|
||||||
|
priority: 10
|
||||||
|
properties:
|
||||||
|
at_home_sensor: binary_sensor.danil_home
|
||||||
|
device_trackers:
|
||||||
|
- sensor.tag_monster_nut
|
||||||
|
notifiers:
|
||||||
|
- telegram_monster
|
||||||
|
presence_input_select: input_select.danil_presence_status
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- automation
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
sveta:
|
||||||
|
module: person
|
||||||
|
class: Person
|
||||||
|
priority: 10
|
||||||
|
properties:
|
||||||
|
at_home_sensor: binary_sensor.sveta_home
|
||||||
|
device_trackers:
|
||||||
|
- sensor.tag_nut_guest
|
||||||
|
notifiers:
|
||||||
|
- telegram_sveta
|
||||||
|
presence_input_select: input_select.sveta_presence_status
|
||||||
|
dependencies:
|
||||||
|
- automation
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
mama:
|
||||||
|
module: person
|
||||||
|
class: Person
|
||||||
|
priority: 10
|
||||||
|
properties:
|
||||||
|
at_home_sensor: binary_sensor.mama_home
|
||||||
|
device_trackers:
|
||||||
|
- sensor.tag_mamik_band
|
||||||
|
- sensor.tag_nut_blue
|
||||||
|
notifiers:
|
||||||
|
- telegram_mama
|
||||||
|
presence_input_select: input_select.mama_presence_status
|
||||||
|
dependencies:
|
||||||
|
- automation
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Listen for presence sensor change state and change alarm control panel state.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# sensor - home presence 'sensor'
|
||||||
|
# ha_panel - alarm control panel entity (to arm and disarm).
|
||||||
|
# constraint - (optional, input_boolen), if turned off - alarm panel will be not armed\disarmed.
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AlarmPanelBySensor(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if "sensor" not in self.args or "ha_panel" not in self.args:
|
||||||
|
self.error("Please provide sensor and ha_panel in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.sensor_trigger, self.args['sensor'])
|
||||||
|
self.listen_event(self.ha_event, "ha_started")
|
||||||
|
|
||||||
|
def ha_event(self, event_name, data, kwargs):
|
||||||
|
self.log('Starting up!')
|
||||||
|
state = self.get_state(self.args['sensor'])
|
||||||
|
self.log('Updating alarm_control_panel state: {}'.format(state))
|
||||||
|
if state == "off":
|
||||||
|
self.away_mode()
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.log("{} turned {}".format(entity, new))
|
||||||
|
if new == "off" and old == "on":
|
||||||
|
self.away_mode()
|
||||||
|
if new == "on" and old == "off":
|
||||||
|
self.return_home_mode()
|
||||||
|
|
||||||
|
def away_mode(self):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.call_service("alarm_control_panel/alarm_arm_away", entity_id = self.args['ha_panel'])
|
||||||
|
|
||||||
|
def return_home_mode(self):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.call_service("alarm_control_panel/alarm_disarm", entity_id = self.args['ha_panel'])
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
alarm_panel:
|
||||||
|
module: alarm_panel
|
||||||
|
class: AlarmPanelBySensor
|
||||||
|
sensor: group.presence
|
||||||
|
ha_panel: alarm_control_panel.ha_alarm
|
||||||
|
constraint: input_boolean.presence_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Listen for Alarm panel state change and fire an events ('away_mode' and 'return_home_mode')
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# ha_panel - alarm control panel entity.
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class AlarmPanelAction(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if "ha_panel" not in self.args:
|
||||||
|
self.error("Please provide ha_panel in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.ha_panel_trigger, self.args['ha_panel'])
|
||||||
|
|
||||||
|
def ha_panel_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
# не срабатываем на triggered.
|
||||||
|
if new == "armed_away" and old != "triggered":
|
||||||
|
self.away_mode()
|
||||||
|
if new == "disarmed" and old == "armed_away":
|
||||||
|
self.return_home_mode()
|
||||||
|
|
||||||
|
def away_mode(self):
|
||||||
|
self.log('away_mode')
|
||||||
|
self.fire_event('away_mode')
|
||||||
|
|
||||||
|
def return_home_mode(self):
|
||||||
|
self.log('return_home_mode')
|
||||||
|
self.fire_event('return_home_mode')
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
alarm_panel_action:
|
||||||
|
module: alarm_panel_action
|
||||||
|
class: AlarmPanelAction
|
||||||
|
ha_panel: alarm_control_panel.ha_alarm
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Light control by presence events
|
||||||
|
# Listen for presence events and change light state
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# entities - entities to turn on and off when presence is changed.
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class EntitiesByPresence(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if 'away_entities' not in self.args or 'return_entities' not in self.args:
|
||||||
|
self.error("Please provide away_entities and return_entities in config!")
|
||||||
|
return
|
||||||
|
self.listen_event(self.away_mode, "away_mode")
|
||||||
|
self.listen_event(self.return_home_mode, "return_home_mode")
|
||||||
|
|
||||||
|
def away_mode(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
for entity_id in self.args["away_entities"]:
|
||||||
|
self.turn_off(entity_id)
|
||||||
|
|
||||||
|
def return_home_mode(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
for entity_id in self.args["return_entities"]:
|
||||||
|
self.turn_on(entity_id)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
entities_action:
|
||||||
|
module: entities_action
|
||||||
|
class: EntitiesByPresence
|
||||||
|
away_entities:
|
||||||
|
- group.all_lights
|
||||||
|
- switch.kitchen_mini_light
|
||||||
|
- switch.plug_158d0001104a0c # телевизор
|
||||||
|
- switch.plug_158d0001a68b18 # термопот
|
||||||
|
return_entities:
|
||||||
|
- light.yeelight_rgb_34ce008fe328
|
||||||
|
- light.hall
|
||||||
|
- switch.plug_158d0001104a0c # телевизор
|
||||||
|
- switch.plug_158d0001a68b18 # термопот
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# Home and away report
|
||||||
|
# Send report about home state after presence_actions was done.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# entity - group of entities to send state report
|
||||||
|
# notify - notify entity to send message
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class ReportByPresence(hass.Hass):
|
||||||
|
report_delay = 15
|
||||||
|
def initialize(self):
|
||||||
|
if 'entity' not in self.args or 'notify' not in self.args:
|
||||||
|
self.error("Please provide entity and notify in config!")
|
||||||
|
return
|
||||||
|
self.listen_event(self.away_mode, "away_mode")
|
||||||
|
self.listen_event(self.return_home_mode, "return_home_mode")
|
||||||
|
|
||||||
|
def away_mode(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
#wait 15 seconds before all devices will change thair states.
|
||||||
|
self.run_in(self.send_report, self.report_delay)
|
||||||
|
|
||||||
|
def return_home_mode(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.notify("Добро пожаловать домой!", name = self.args['notify'])
|
||||||
|
|
||||||
|
############# AWAY REPORT ##########################
|
||||||
|
def send_report(self, kwargs):
|
||||||
|
any = False
|
||||||
|
message = ""
|
||||||
|
entities = globals.get_group_entities(self, self.args['entity'])
|
||||||
|
for entity in entities:
|
||||||
|
state = self.get_state(entity)
|
||||||
|
if state == "on":
|
||||||
|
any = True
|
||||||
|
message += '{} ({}) = {}\n'.format(self.friendly_name(entity), entity, self.get_state(entity))
|
||||||
|
if any:
|
||||||
|
message = "В доме остались работать следующие устройства:\n" + message
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
|
else:
|
||||||
|
self.notify("Все устройства в доме выключены.", name = self.args['notify'])
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
report_action:
|
||||||
|
module: report_action
|
||||||
|
class: ReportByPresence
|
||||||
|
entity: group.report_entities
|
||||||
|
notify: telegram
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags counter
|
||||||
|
# This app count 'at home' BLE tags and set home presence state.
|
||||||
|
# tag names must start from 'tag_'. Room presence sensors will be generated.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# tag_prefix = tag prefix to search (in my case 'sensor.tag_')
|
||||||
|
# sensor_prefix = sensor prefix for room presence sensor (ex: if prefix 'sensor.presence_', than sensor name will be 'sensor.presence_hall')
|
||||||
|
# rooms = rooms list, where tags will be discovered (sensor will be created for each room)
|
||||||
|
# constraint_prefix (input_boolean) = constraint for tag (ex: input_boolean.tag_monster)
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
class TagsPresence(hass.Hass):
|
||||||
|
tags = []
|
||||||
|
constraint_list = []
|
||||||
|
tag_by_room_cache = {}
|
||||||
|
def initialize(self):
|
||||||
|
if 'tag_prefix' not in self.args or 'sensor_prefix' not in self.args or 'rooms' not in self.args or 'constraint_prefix' not in self.args:
|
||||||
|
self.error("Please provide tag_prefix, sensor_prefix, constraint_prefix and rooms in config!")
|
||||||
|
return
|
||||||
|
self.listen_tags()
|
||||||
|
# self.listen_event(self.ha_event, "plugin_started")
|
||||||
|
self.refresh_presence_state()
|
||||||
|
|
||||||
|
# def ha_event(self, event_name, data, kwargs):
|
||||||
|
# self.run_in(self.refresh_presence_state_timer, 10)
|
||||||
|
|
||||||
|
def refresh_presence_state_timer(self, kwargs):
|
||||||
|
self.refresh_presence_state()
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.refresh_presence_state()
|
||||||
|
|
||||||
|
def constraint_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.refresh_presence_state()
|
||||||
|
|
||||||
|
def listen_tags(self):
|
||||||
|
self.fill_tags_and_constraints()
|
||||||
|
for tag in self.tags:
|
||||||
|
self.listen_state(self.sensor_trigger, tag)
|
||||||
|
for constraint in self.constraint_list:
|
||||||
|
self.listen_state(self.constraint_trigger, constraint)
|
||||||
|
|
||||||
|
def refresh_presence_state(self):
|
||||||
|
tags_by_room = self.get_tags_state_by_room(self.tags, self.constraint_list)
|
||||||
|
for room in self.args['rooms']:
|
||||||
|
sensor = '{}{}'.format(self.args['sensor_prefix'],room)
|
||||||
|
if room in tags_by_room and len(tags_by_room[room]) > 0:
|
||||||
|
self.create_sensor(sensor, 'on')
|
||||||
|
else:
|
||||||
|
self.create_sensor(sensor, 'off')
|
||||||
|
|
||||||
|
def get_tags_state_by_room(self, tags, constraint_list):
|
||||||
|
tags_by_room = {}
|
||||||
|
for tag in tags:
|
||||||
|
constraint = '{}{}'.format(self.args['constraint_prefix'],tag.replace(self.args['tag_prefix'],''))
|
||||||
|
if constraint in constraint_list and not self.constrain_input_boolean(constraint):
|
||||||
|
continue
|
||||||
|
room = self.get_state(tag)
|
||||||
|
if room not in tags_by_room:
|
||||||
|
tags_by_room[room] = []
|
||||||
|
tags_by_room[room].append(tag)
|
||||||
|
return tags_by_room
|
||||||
|
|
||||||
|
def fill_tags_and_constraints(self):
|
||||||
|
self.tags = []
|
||||||
|
self.constraint_list = []
|
||||||
|
devices = self.get_state()
|
||||||
|
for tag in devices:
|
||||||
|
if not tag.startswith(self.args['tag_prefix']):
|
||||||
|
continue
|
||||||
|
self.tags.append(tag)
|
||||||
|
|
||||||
|
constraint = '{}{}'.format(self.args['constraint_prefix'],tag.replace(self.args['tag_prefix'],''))
|
||||||
|
if constraint in devices and constraint not in self.constraint_list:
|
||||||
|
self.constraint_list.append(constraint)
|
||||||
|
|
||||||
|
def create_sensor(self, entity_id, state, attributes = None):
|
||||||
|
current_state = self.get_state(entity_id)
|
||||||
|
if (current_state == state):
|
||||||
|
return
|
||||||
|
self.log('Updating {} state ({}->{})'.format(entity_id, current_state, state))
|
||||||
|
if attributes == None:
|
||||||
|
attributes = {}
|
||||||
|
attributes['description'] = 'Created and updated from appdaemon (tags_presence)'
|
||||||
|
self.set_state(entity_id, state = state, attributes = attributes)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
tags_presence:
|
||||||
|
module: tags_presence
|
||||||
|
class: TagsPresence
|
||||||
|
tag_prefix: sensor.tag_
|
||||||
|
sensor_prefix: sensor.presence_
|
||||||
|
constraint_prefix: input_boolean.tag_
|
||||||
|
rooms:
|
||||||
|
- hall
|
||||||
|
- balcony
|
||||||
|
- kitchen
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Water control application.
|
||||||
|
# Controls water valve when all leave and no water needed devices is working.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# water_valve = water valve entity
|
||||||
|
# notify - notify entity to send message
|
||||||
|
# constraint (optional) = input_boolean to enable\disable this automation
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class WaterValveControl(hass.Hass):
|
||||||
|
standby_power_limit = 15
|
||||||
|
timer = None
|
||||||
|
def initialize(self):
|
||||||
|
if 'water_valve' not in self.args or 'water_devices' not in self.args or 'notify' not in self.args:
|
||||||
|
self.error("Please provide water_valve, water_devices and notify in config!")
|
||||||
|
return
|
||||||
|
self.listen_event(self.away_mode, "away_mode")
|
||||||
|
self.listen_event(self.return_home_mode, "return_home_mode")
|
||||||
|
self.log(self.get_turned_on_devices())
|
||||||
|
|
||||||
|
def return_home_mode(self, event_id, event_args, kwargs):
|
||||||
|
self.timer = None
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.turn_on(self.args['water_valve'])
|
||||||
|
for device in self.args['water_devices']:
|
||||||
|
self.turn_on(device)
|
||||||
|
self.cancel_current_timer()
|
||||||
|
|
||||||
|
def away_mode(self, event_id, event_args, kwargs):
|
||||||
|
self.timer = None
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
powered_on = self.get_turned_on_devices()
|
||||||
|
if (len(powered_on) > 0):
|
||||||
|
device_names = ""
|
||||||
|
for device in powered_on:
|
||||||
|
if device_names != '':
|
||||||
|
device_names += ", "
|
||||||
|
device_names += self.friendly_name(device)
|
||||||
|
self.notify('В доме остались требующие воды устройства ({}), подача воды не будет отключена.'.format(device_names), name = self.args['notify'])
|
||||||
|
self.cancel_current_timer()
|
||||||
|
self.timer = self.run_every(self.wait_when_device_is_done, self.datetime(), 5*60)
|
||||||
|
else:
|
||||||
|
self.turn_off(self.args['water_valve'])
|
||||||
|
for device in self.args['water_devices']:
|
||||||
|
self.turn_off(device)
|
||||||
|
|
||||||
|
def wait_when_device_is_done(self, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
powered_on = self.get_turned_on_devices()
|
||||||
|
self.log('still powered on: {}'.format(powered_on))
|
||||||
|
if (len(powered_on) == 0):
|
||||||
|
# self.turn_off(self.args['water_valve'])
|
||||||
|
# for device in self.args['water_devices']:
|
||||||
|
# self.turn_off(device)
|
||||||
|
self.notify('Устройства требующие воды закончили свою работу.', name = self.args['notify'])
|
||||||
|
self.cancel_current_timer()
|
||||||
|
|
||||||
|
def get_turned_on_devices(self):
|
||||||
|
powered_on_devices = []
|
||||||
|
devices = self.args['water_devices']
|
||||||
|
for device in devices:
|
||||||
|
state = self.get_state(device)
|
||||||
|
attributes = self.get_state(device, attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
if 'load_power' not in attributes:
|
||||||
|
continue
|
||||||
|
load_power = attributes['load_power']
|
||||||
|
self.log('{} load power is: {}'.format(device, load_power))
|
||||||
|
if state == 'on' and load_power < self.standby_power_limit:
|
||||||
|
continue
|
||||||
|
powered_on_devices.append(device)
|
||||||
|
return powered_on_devices
|
||||||
|
|
||||||
|
def cancel_current_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.log('canceling timer')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
water_action:
|
||||||
|
module: water_action
|
||||||
|
class: WaterValveControl
|
||||||
|
water_valve: switch.water_valve
|
||||||
|
notify: telegram
|
||||||
|
constraint: input_boolean.water_control
|
||||||
|
water_devices:
|
||||||
|
- switch.plug_158d0001a68c64 # Посудомойка
|
||||||
|
- switch.plug_158d0001a4cfb4 # стиралка
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Work leave reminder
|
||||||
|
# Send reminder about 'time to leave your work'
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class WorkPresence(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
self.listen_event(self.office_enter, "office_enter")
|
||||||
|
self.listen_event(self.office_leave, "office_leave")
|
||||||
|
self.listen_event(self.office_leave_reminder, "office_leave_reminder")
|
||||||
|
|
||||||
|
def office_leave_reminder(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.log('leave_reminder')
|
||||||
|
enter_time = self.get_state('sensor.work_enter_time')
|
||||||
|
leave_time = self.get_state('sensor.work_plan_leave_time')
|
||||||
|
if 'payload' in event_args:
|
||||||
|
leave_time = event_args['payload']
|
||||||
|
message = "Ты пришел на работу в {}, работаешь до {}. Пора домой!".format(enter_time, leave_time)
|
||||||
|
self.call_service('notify/telegram', message=message)
|
||||||
|
|
||||||
|
def office_leave(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.log('leave')
|
||||||
|
work_to_home_time = self.get_state('sensor.work_to_home_time')
|
||||||
|
message="Время в пути до дома {} минут.".format(work_to_home_time)
|
||||||
|
self.call_service('notify/telegram', message=message)
|
||||||
|
|
||||||
|
def office_enter(self, event_id, event_args, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
self.log('enter')
|
||||||
|
enter_time = event_args.get('payload', None)
|
||||||
|
if enter_time == '' or enter_time == None:
|
||||||
|
return
|
||||||
|
self.log(enter_time)
|
||||||
|
self.call_service('notify/telegram', message='Добро пожаловать в офис. Приятного дня =)))')
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#################################
|
||||||
|
# WORK PRESENSE #
|
||||||
|
#################################
|
||||||
|
work_presence:
|
||||||
|
module: work
|
||||||
|
class: WorkPresence
|
||||||
|
constraint: input_boolean.work_presence_report
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
#
|
||||||
|
# Управление замком Xiaomi
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class Lock(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
if not globals.check_args(self, ['notify']):
|
||||||
|
return
|
||||||
|
locks = globals.get_group_entities(self, "group.all_locks")
|
||||||
|
for lock in locks:
|
||||||
|
self.listen_event_handle_list.append(self.listen_state(self.lock_state_changed, lock))
|
||||||
|
|
||||||
|
def lock_state_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
state = new
|
||||||
|
changed_by = self.get_state(entity, attribute = 'changed_by')
|
||||||
|
verified_wrong_times = self.get_state(entity, attribute = 'verified_wrong_times')
|
||||||
|
|
||||||
|
if verified_wrong_times > 0:
|
||||||
|
message = "Попытка №{} отрыть замок с неправильным пальцем или паролем!".format(verified_wrong_times)
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
|
|
||||||
|
if state == "unlocked":
|
||||||
|
user = changed_by
|
||||||
|
user_var = "user_{}".format(changed_by)
|
||||||
|
if user_var in self.args:
|
||||||
|
user = self.args[user_var]
|
||||||
|
message = "Открыта дверь пользователем: {}.".format(user)
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
lock:
|
||||||
|
module: lock
|
||||||
|
class: Lock
|
||||||
|
constraint: input_boolean.lock_report
|
||||||
|
user_65536: Данил (указ)
|
||||||
|
user_65537: Данил (сред)
|
||||||
|
user_65538: Света (указ)
|
||||||
|
user_65539: Мама (указ)
|
||||||
|
notify: telegram
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Security sensors alarm notification
|
||||||
|
# Send notification if some movement is detecter or door any is open
|
||||||
|
# while nobody is home
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# sensor - gorup of sensors to listen
|
||||||
|
# ha_panel - alarm panel entity
|
||||||
|
# notify - notify entity to send message
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class SecuritySensorsReport(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if 'sensor' not in self.args or 'ha_panel' not in self.args or 'notify' not in self.args:
|
||||||
|
self.error("Please provide sensor, ha_panel and notify in config!")
|
||||||
|
return
|
||||||
|
|
||||||
|
attributes = self.get_state(self.args['sensor'], attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
if 'entity_id' in attributes:
|
||||||
|
for entity_id in attributes['entity_id']:
|
||||||
|
self.listen_state(self.sensor_trigger, entity_id)
|
||||||
|
|
||||||
|
def sensor_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
if old != new and new == "on":
|
||||||
|
alarm_panel_state = self.get_state(self.args['ha_panel'])
|
||||||
|
if alarm_panel_state != "armed_away":
|
||||||
|
return
|
||||||
|
self.alarm_triggered(entity, old, new)
|
||||||
|
|
||||||
|
def alarm_triggered(self, entity, old, new):
|
||||||
|
self.log('alarm triggered')
|
||||||
|
self.call_service("alarm_control_panel/alarm_trigger", entity_id = self.args['ha_panel'])
|
||||||
|
|
||||||
|
friendly_name = self.friendly_name(entity)
|
||||||
|
entity = entity.replace('_', '-')
|
||||||
|
message = "В ваше отсутствие сработал датчик безопастности {} ({}) = {}->{}!".format(friendly_name, entity, old, new)
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
security_sensors:
|
||||||
|
module: security_sensors
|
||||||
|
class: SecuritySensorsReport
|
||||||
|
sensor: group.security_sensors
|
||||||
|
constraint: input_boolean.security_sensors_report
|
||||||
|
ha_panel: alarm_control_panel.ha_alarm
|
||||||
|
notify: telegram
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import print_function
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, tzinfo
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
class AuthException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmpServerException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Client(object):
|
||||||
|
"""
|
||||||
|
Клиента
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.token = kwargs.get('token')
|
||||||
|
self.guid = kwargs.get('guid')
|
||||||
|
|
||||||
|
#requests
|
||||||
|
self.verify = kwargs.get('verify', True)
|
||||||
|
self.timeout = kwargs.get('timeout', 3.0)
|
||||||
|
|
||||||
|
self.session_id = None # emp mos ru
|
||||||
|
self.user_agent = kwargs.get('user_agent')
|
||||||
|
self.dev_app_version = kwargs.get('dev_app_version')
|
||||||
|
self.dev_user_agent = kwargs.get('dev_user_agent')
|
||||||
|
self.headers = {'Cache-Control': 'no-cache',
|
||||||
|
'Host': 'emp.mos.ru',
|
||||||
|
'Connection': 'Keep-Alive',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'User-Agent': self.user_agent}
|
||||||
|
self.pheaders = deepcopy(self.headers)
|
||||||
|
self.pheaders.update({
|
||||||
|
'X-Cache-ov': '15552000',
|
||||||
|
'X-Cache-ov-mode': 'FORCE_NETWORK'
|
||||||
|
})
|
||||||
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
def raise_for_status(self, answer):
|
||||||
|
"""
|
||||||
|
:param answer: JSON
|
||||||
|
{
|
||||||
|
u'errorCode': 0, # 0-OK,
|
||||||
|
401-Auth error
|
||||||
|
u'execTime': 0.138262,
|
||||||
|
u'errorMessage': u'', # error message
|
||||||
|
u'session_id': u'6c3333333c33333e44e21e7d43c46e03',
|
||||||
|
u'result': None or JSON
|
||||||
|
}
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# print('Exec time:', answer['execTime'])
|
||||||
|
if answer['errorCode'] == 401:
|
||||||
|
raise AuthException()
|
||||||
|
|
||||||
|
if answer['errorCode'] != 0:
|
||||||
|
if answer['errorMessage']:
|
||||||
|
raise EmpServerException(u'{0} (code:{1})'.format(answer['errorMessage'], answer['errorCode']))
|
||||||
|
raise EmpServerException(u'code:{0}'.format(answer['errorCode']))
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
:return: True если уже залогинился
|
||||||
|
"""
|
||||||
|
return self.session_id is not None
|
||||||
|
|
||||||
|
def login(self, telephone, pwd):
|
||||||
|
"""
|
||||||
|
:param telephone: Телефон. Вид: 7xxxxxxxxxx
|
||||||
|
:param pwd: Пароль из приложения Госуслуги Москвы
|
||||||
|
:return: JSON
|
||||||
|
{
|
||||||
|
u'is_filled': True,
|
||||||
|
u'surname': u'x',
|
||||||
|
u'name': u'x',
|
||||||
|
u'session_id': u'6c3333333c33333e44e21e7d43c46e03'},
|
||||||
|
u'request_id': u'UN=PRO-12345678-1234-1234-1234-123456789123'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
login_data = {
|
||||||
|
'device_info': {
|
||||||
|
'guid': self.guid,
|
||||||
|
'user_agent': self.dev_user_agent,
|
||||||
|
'app_version': self.dev_app_version
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'login': telephone,
|
||||||
|
'password': pwd,
|
||||||
|
'guid': self.guid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/auth/virtualLogin',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers={'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
'Connection': 'Keep-Alive',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'User-Agent': self.user_agent,
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
'Host': 'emp.mos.ru',
|
||||||
|
'Accept': '*/*'},
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=login_data)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
self.session_id = response['session_id']
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def get_profile(self):
|
||||||
|
"""
|
||||||
|
:return: JSON
|
||||||
|
{
|
||||||
|
u'drive_license': None,
|
||||||
|
u'firstname': u'x',
|
||||||
|
u'middlename': u'x',
|
||||||
|
u'lastname': u'x',
|
||||||
|
u'birthdate': u'dd.mm.YYYY',
|
||||||
|
u'msisdn': u'71234567890', # your telephone
|
||||||
|
u'email_confirmed': True,
|
||||||
|
u'email': u'x@x.ru'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
|
||||||
|
ret = self.session.get('https://emp.mos.ru/v1.0/profile/get',
|
||||||
|
params={'token': self.token,
|
||||||
|
'info[guid]': self.guid,
|
||||||
|
'auth[session_id]': self.session_id
|
||||||
|
},
|
||||||
|
headers=self.pheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']['profile']
|
||||||
|
|
||||||
|
def get_flats(self):
|
||||||
|
"""
|
||||||
|
:return: JSON array
|
||||||
|
[{
|
||||||
|
u'name': u'x',
|
||||||
|
u'paycode': u'1234567890',
|
||||||
|
u'flat_id': u'1234567',
|
||||||
|
u'electro_account': None,
|
||||||
|
u'flat_number': u'0',
|
||||||
|
u'unad': u'0',
|
||||||
|
u'address': u'x',
|
||||||
|
u'electro_device': None,
|
||||||
|
u'unom': u'1234567' # Идентификатор дома
|
||||||
|
},{...}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
|
||||||
|
ret = self.session.get('https://emp.mos.ru/v1.0/flat/get',
|
||||||
|
params={'token': self.token,
|
||||||
|
'info[guid]': self.guid,
|
||||||
|
'auth[session_id]': self.session_id
|
||||||
|
},
|
||||||
|
headers=self.pheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout = self.timeout)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def get_watercounters(self, flat_id):
|
||||||
|
"""
|
||||||
|
:param flat_id: unicode string from flat_response
|
||||||
|
response[0]['flat_id']
|
||||||
|
:return:
|
||||||
|
{
|
||||||
|
u'stat_title': u'x',
|
||||||
|
u'archive': [
|
||||||
|
{
|
||||||
|
u'cold_indication': 1.55,
|
||||||
|
u'hot_indication': 1.2,
|
||||||
|
u'period': u'2017-08-31+03:00'
|
||||||
|
}, {
|
||||||
|
u'cold_indication': 1.3,
|
||||||
|
u'hot_indication': 0.39999999999998,
|
||||||
|
u'period': u'2017-09-30+03:00'
|
||||||
|
}, {
|
||||||
|
+10 эл-тов. Всего 12.
|
||||||
|
}],
|
||||||
|
u'counters': [
|
||||||
|
{
|
||||||
|
u'checkup': u'2021-08-08+03:00', # дата поверки
|
||||||
|
u'counterId': 123456, # id счетчика в emp.mos.ru
|
||||||
|
u'num': u'123456', # серийный номер счетчика
|
||||||
|
u'type': 1, # ХВС
|
||||||
|
u'indications': [
|
||||||
|
{
|
||||||
|
u'indication': u'200.24',
|
||||||
|
u'period': u'2018-07-31+03:00'
|
||||||
|
}, {
|
||||||
|
u'indication': u'200.6',
|
||||||
|
u'period': u'2018-06-30+03:00'
|
||||||
|
}, {
|
||||||
|
u'indication': u'200.5',
|
||||||
|
u'period': u'2018-05-31+03:00'
|
||||||
|
}, {
|
||||||
|
u'indication': u'200.5',
|
||||||
|
u'period': u'2018-04-30+03:00'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{..}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'WATER_COUNTERS',
|
||||||
|
'X-Cache-ov-mode': 'DEFAULT',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8' })
|
||||||
|
|
||||||
|
wcrequest = {
|
||||||
|
'flat_id': flat_id,
|
||||||
|
'is_widget': False,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/watercounters/get',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def send_watercounters(self, flat_id, counters_data):
|
||||||
|
"""
|
||||||
|
:param flat_id: flat_response['flat_id']
|
||||||
|
:param counters_data: array of
|
||||||
|
[{
|
||||||
|
'counter_id': u'123456', # ['counters'][0]['counterId']
|
||||||
|
'period': datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
'indication': 'xxx,xx'
|
||||||
|
}, {
|
||||||
|
...
|
||||||
|
}]
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'WATER_COUNTERS',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8'
|
||||||
|
})
|
||||||
|
|
||||||
|
wcrequest = {'flat_id': flat_id,
|
||||||
|
'counters_data': counters_data,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/watercounters/addValues',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def get_electrocounters(self, flat_id):
|
||||||
|
"""
|
||||||
|
:param flat_id: unicode string from flat_response
|
||||||
|
response[0]['flat_id']
|
||||||
|
:return:
|
||||||
|
{
|
||||||
|
"address": "город ...",
|
||||||
|
"electro_account": "851xxxx475",
|
||||||
|
"electro_device": "04xxx17",
|
||||||
|
"balance": 0,
|
||||||
|
"is_debt": false,
|
||||||
|
"description": "Внимание! Передача показаний возможна с 15 по 26 число месяца. При вводе текущих показаний обращайте внимание на значность счетчиков. В случае если показания введены с неверной значностью (большей, чем значность счетчика), они не будут загружены.",
|
||||||
|
"sh_znk": 5,
|
||||||
|
"zones": [{
|
||||||
|
"name": "T1",
|
||||||
|
"value": "6887"
|
||||||
|
}],
|
||||||
|
"intervals": [{
|
||||||
|
"name": "T1",
|
||||||
|
"value": "00:00-23:59"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'ELECTRO_COUNTERS',
|
||||||
|
'X-Cache-ov-mode': 'DEFAULT',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8' })
|
||||||
|
|
||||||
|
wcrequest = {
|
||||||
|
'flat_id': flat_id,
|
||||||
|
'is_widget': False,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid,
|
||||||
|
'user_agent': self.dev_user_agent,
|
||||||
|
'app_version': self.dev_app_version
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/electrocounters/get',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def send_electrocounters(self, flat_id, counters_data):
|
||||||
|
"""
|
||||||
|
:param flat_id: flat_response['flat_id']
|
||||||
|
:param counters_data: array of
|
||||||
|
[{
|
||||||
|
'counter_id': u'123456', # ['counters'][0]['counterId']
|
||||||
|
'period': datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
'indication': 'xxx,xx'
|
||||||
|
}, {
|
||||||
|
...
|
||||||
|
}]
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'ELECTRO_COUNTERS',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8'
|
||||||
|
})
|
||||||
|
|
||||||
|
wcrequest = {'flat_id': flat_id,
|
||||||
|
'counters_data': counters_data,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid,
|
||||||
|
'user_agent': self.dev_user_agent,
|
||||||
|
'app_version': self.dev_app_version
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}}
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/electrocounters/addValues',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def get_epd(self, flat_id, period, is_debt=True):
|
||||||
|
"""
|
||||||
|
:param flat_id: unicode string from flat_response
|
||||||
|
response[0]['flat_id']
|
||||||
|
:param period: unicode string represents date in 27.09.2018 format
|
||||||
|
:param is_debt: True/False
|
||||||
|
:return:
|
||||||
|
{
|
||||||
|
"is_debt": true,
|
||||||
|
"is_paid": true,
|
||||||
|
"amount": 2982.77,
|
||||||
|
"service_code": "emp.zkh",
|
||||||
|
"insurance": 61.93,
|
||||||
|
"ammount_insurance": 3044.7
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'EPD',
|
||||||
|
'X-Cache-ov-mode': 'DEFAULT',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8' })
|
||||||
|
|
||||||
|
wcrequest = {
|
||||||
|
'flat_id': flat_id,
|
||||||
|
'period': period,
|
||||||
|
'is_debt': is_debt,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.1/epd/get',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def get_car_fines(self, sts_number):
|
||||||
|
"""
|
||||||
|
:param sts_number: unicode string contains car sts_numer
|
||||||
|
:return:
|
||||||
|
{
|
||||||
|
"paid": [{
|
||||||
|
"seriesAndNumber": "xxxxx",
|
||||||
|
"date": "2018-08-30+03:00",
|
||||||
|
"offence_place": "МОСКВА Г. МКАД,xxxx, ВНЕШНЯЯ СТОРОНА",
|
||||||
|
"offenceType": "12.9ч.2 - Превышение установленной скорости движения транспортного средства на величину от 20 до 40 километров в час ",
|
||||||
|
"cost": "500",
|
||||||
|
"is_discount": false,
|
||||||
|
"drive_license": null,
|
||||||
|
"sts_number": "xxxxxxx",
|
||||||
|
"executionState": "Исполнено",
|
||||||
|
"is_fssp": false
|
||||||
|
}],
|
||||||
|
"unpaid": []
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assert self.session_id
|
||||||
|
wheaders = deepcopy(self.headers)
|
||||||
|
wheaders.update({
|
||||||
|
'X-Clears-tags': 'FORCE_NETWORK',
|
||||||
|
'X-Cache-ov-mode': 'DEFAULT',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8' })
|
||||||
|
|
||||||
|
wcrequest = {
|
||||||
|
'sts_number': sts_number,
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/offence/getOffence',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=wheaders,
|
||||||
|
verify=self.verify,
|
||||||
|
timeout=self.timeout,
|
||||||
|
json=wcrequest)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
def logout(self, timeout=None):
|
||||||
|
"""
|
||||||
|
Почему то очень долго выполняется (5 сек)
|
||||||
|
"""
|
||||||
|
if self.session_id:
|
||||||
|
logout_data = {
|
||||||
|
'info': {
|
||||||
|
'guid': self.guid
|
||||||
|
},
|
||||||
|
'auth': {
|
||||||
|
'session_id': self.session_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = self.session.post('https://emp.mos.ru/v1.0/auth/logout',
|
||||||
|
params={'token': self.token},
|
||||||
|
headers=self.headers,
|
||||||
|
timeout=timeout or self.timeout,
|
||||||
|
json=logout_data)
|
||||||
|
|
||||||
|
response = ret.json()
|
||||||
|
self.raise_for_status(response)
|
||||||
|
self.session_id = None
|
||||||
|
return response['result']
|
||||||
|
|
||||||
|
|
||||||
|
class MosAPI(object):
|
||||||
|
"""
|
||||||
|
kwargs:
|
||||||
|
|
||||||
|
token: уникальный ключ приложения
|
||||||
|
guid: некий уникальный ключ
|
||||||
|
https_verify: ключ verify в GET, POST запросах, по умолчанию 'False'
|
||||||
|
timeout: ключ timeout в GET, POST запросах
|
||||||
|
user_agent: версия веб клиента
|
||||||
|
dev_user_agent: 'Android' для ОС Android
|
||||||
|
dev_app_version: версия ОС
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self._clients = {'default': Client(**kwargs)}
|
||||||
|
|
||||||
|
def client(self, client_id='default', **kwargs):
|
||||||
|
if client_id and \
|
||||||
|
client_id not in self._clients:
|
||||||
|
k = self.kwargs
|
||||||
|
k.update(kwargs)
|
||||||
|
self._clients[client_id] = Client(**k)
|
||||||
|
return self._clients[client_id]
|
||||||
|
|
||||||
|
# if only one client
|
||||||
|
def is_active(self):
|
||||||
|
return self.client().is_active()
|
||||||
|
def login(self, *args):
|
||||||
|
return self.client().login(*args)
|
||||||
|
def logout(self, *args):
|
||||||
|
return self.client().logout(*args)
|
||||||
|
|
||||||
|
def get_profile(self):
|
||||||
|
return self.client().get_profile()
|
||||||
|
def get_flats(self):
|
||||||
|
return self.client().get_flats()
|
||||||
|
def get_watercounters(self, *args):
|
||||||
|
return self.client().get_watercounters(*args)
|
||||||
|
def send_watercounters(self, *args):
|
||||||
|
return self.client().send_watercounters(*args)
|
||||||
|
def get_electrocounters(self, *args):
|
||||||
|
return self.client().get_electrocounters(*args)
|
||||||
|
def send_electrocounters(self, *args):
|
||||||
|
return self.client().send_electrocounters(*args)
|
||||||
|
def get_epd(self, *args):
|
||||||
|
return self.client().get_epd(*args)
|
||||||
|
def get_car_fines(self, *args):
|
||||||
|
return self.client().get_car_fines(*args)
|
||||||
|
|
||||||
|
class Water():
|
||||||
|
COLD = 1
|
||||||
|
HOT = 2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def water_abbr(water):
|
||||||
|
if water == Water.COLD: return u'ХВС'
|
||||||
|
elif water == Water.HOT: return u'ГВС'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def name(water):
|
||||||
|
if water == Water.COLD: return u'холодная вода'
|
||||||
|
elif water == Water.HOT: return u'горячая вода'
|
||||||
|
|
||||||
|
|
||||||
|
class Watercounter():
|
||||||
|
"""
|
||||||
|
[{ 'counterId': 1437373, # внутреннее id счетчика
|
||||||
|
'type': 1, # тип воды
|
||||||
|
'num': '417944', # серийный номер счетчика
|
||||||
|
'checkup': '2023-09-25+03:00', # дата следующей поверки
|
||||||
|
'indications':
|
||||||
|
[{'period': '2018-08-31+03:00', 'indication': '21.38'},
|
||||||
|
{'period': '2018-07-31+03:00', 'indication': '20.7'},
|
||||||
|
{'period': '2018-06-30+03:00', 'indication': '19'}]
|
||||||
|
},
|
||||||
|
{...}
|
||||||
|
]
|
||||||
|
|
||||||
|
list(filter(lambda x: x['num'] == num, response['counters']))
|
||||||
|
list(filter(lambda x: x['counterId'] == id, response['counters']))
|
||||||
|
list(filter(lambda x: x['type'] == water_type_id, response['counters']))
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def last_value(counter):
|
||||||
|
"""
|
||||||
|
alphabetical sort data =)
|
||||||
|
:param counter: counter JSON
|
||||||
|
:return: float or None
|
||||||
|
"""
|
||||||
|
indications = counter['indications']
|
||||||
|
indications.sort(key=lambda x: x['period'])
|
||||||
|
if indications:
|
||||||
|
return float(indications[-1]['indication'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def water_title(counter):
|
||||||
|
return Watercounter.humanreadable_name(counter['type'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkup(counter):
|
||||||
|
"""
|
||||||
|
https://docs.python.org/3/library/datetime.html#datetime.timezone
|
||||||
|
Если нужно UTC d.replace(tzinfo=None)
|
||||||
|
|
||||||
|
:param counter: counter JSON
|
||||||
|
:return: datetime с временной зоной
|
||||||
|
"""
|
||||||
|
checkup = counter['checkup']
|
||||||
|
d = datetime.strptime(checkup, '%Y-%m-%d%z') #'checkup': '2023-09-25+03:00'
|
||||||
|
return d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize_for_send(counter, value):
|
||||||
|
"""
|
||||||
|
:param counterId: id счетчика. не номер из приложения!
|
||||||
|
:param value: значение float
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'counter_id': int(counter['counterId']),
|
||||||
|
'period': datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
'indication': '{:.2f}'.format(value).replace('.', ',')
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import globals
|
||||||
|
import datetime as dt
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
from automation import Automation, Base # type: ignore
|
||||||
|
|
||||||
|
class MosEnergoSbytSender(Automation):
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
self.run_daily(self.send_mosenergosbyt_counters, dt.time(12, 0, 0))
|
||||||
|
# self.send_mosenergosbyt_counters({})
|
||||||
|
|
||||||
|
def send_mosenergosbyt_counters(self, kwargs):
|
||||||
|
if not 'home_power_total' in self.entity_ids:
|
||||||
|
self.error('Please define home_power_total in entity_ids.')
|
||||||
|
return
|
||||||
|
|
||||||
|
current_counter_value = int(float(self.get_state(self.entity_ids['home_power_total'])))
|
||||||
|
self.log('Sending current counter value: {}'.format(current_counter_value))
|
||||||
|
self.app.send(current_counter_value)
|
||||||
|
|
||||||
|
|
||||||
|
class MosEnergoSbytUpdater(Automation):
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||||
|
# self.send_mosenergosbyt_counters({})
|
||||||
|
|
||||||
|
def update_sensors(self, args) -> None:
|
||||||
|
self.app.update()
|
||||||
|
counter_value = self.app.value
|
||||||
|
counter_date = self.app.date
|
||||||
|
balance = self.app.balance
|
||||||
|
self.log('Current counter value is: {} (@{}), balance is: {}'.format(counter_value, counter_date, balance))
|
||||||
|
|
||||||
|
if 'mosenergosbyt_total' in self.entity_ids:
|
||||||
|
entity_id = self.entity_ids['mosenergosbyt_total']
|
||||||
|
attributes = {
|
||||||
|
'unit_of_measurement': 'kWh',
|
||||||
|
'date': counter_date,
|
||||||
|
# 'checkup': checkup,
|
||||||
|
'icon': 'mdi:av-timer',
|
||||||
|
'friendly_name': 'Счетчик (мосэнергосбыт)',
|
||||||
|
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||||
|
}
|
||||||
|
self.set_state(entity_id, state = counter_value, attributes = attributes)
|
||||||
|
|
||||||
|
if 'mosenergosbyt_balance' in self.entity_ids:
|
||||||
|
entity_id = self.entity_ids['mosenergosbyt_balance']
|
||||||
|
attributes = {
|
||||||
|
'unit_of_measurement': '₽',
|
||||||
|
'date': counter_date,
|
||||||
|
'icon': 'mdi:bank-transfer',
|
||||||
|
'friendly_name': 'Баланс (мосэнергосбыт)',
|
||||||
|
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||||
|
}
|
||||||
|
self.set_state(entity_id, state = balance, attributes = attributes)
|
||||||
|
|
||||||
|
class MosEnergoSbytCounter(Base):
|
||||||
|
_version = 11
|
||||||
|
_device = "ios/12.1.2/2.14.14(221)"
|
||||||
|
_session = None
|
||||||
|
_profile = None
|
||||||
|
|
||||||
|
# def initialize(self):
|
||||||
|
# super().initialize()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._profile == None):
|
||||||
|
self.update()
|
||||||
|
return float(self._profile['SH_POK_STR'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self) -> float:
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._profile == None):
|
||||||
|
self.update()
|
||||||
|
balance = -1 * float(self._profile['SM_DEBT'])
|
||||||
|
if balance == -0:
|
||||||
|
balance = 0
|
||||||
|
return balance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self) -> str:
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._profile == None):
|
||||||
|
self.update()
|
||||||
|
return self._profile['DT_POK']
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
:return: True если уже залогинился
|
||||||
|
"""
|
||||||
|
return self._session is not None
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
if not self.is_active():
|
||||||
|
self._session = self._get_session()
|
||||||
|
url = 'https://lkkbyt.mosenergosbyt.ru/gate/do?api_version={}&id_session={}&process=refresh&type=1'.format(self._version, self._session)
|
||||||
|
response = requests.get(url).text
|
||||||
|
values_response = self._parse_response(response)
|
||||||
|
self._profile = values_response
|
||||||
|
# self.log(values_response)
|
||||||
|
|
||||||
|
def send(self, value) -> bool:
|
||||||
|
if not self.is_active():
|
||||||
|
self._session = self._get_session()
|
||||||
|
|
||||||
|
url = 'https://lkkbyt.mosenergosbyt.ru/gate/do?id_session={}&pr_check_ras=0&pr_flat_pu=0&process=dotranspok&type=3&vl_pok_t1={}&vl_pok_t2=0&vl_pok_t3=0'.format(self._session, value)
|
||||||
|
response = requests.get(url).text
|
||||||
|
send_response = self._parse_response(response)
|
||||||
|
result = send_response['KD_RESULT']
|
||||||
|
if result == 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_session(self) -> str:
|
||||||
|
login = str(self.properties['login'])
|
||||||
|
pass_md5 = hashlib.md5(self.properties['pwd'].encode('utf-8')).hexdigest()
|
||||||
|
ls = ''
|
||||||
|
phone = ''
|
||||||
|
if len(login) == 10:
|
||||||
|
ls = login
|
||||||
|
elif len(login) == 11:
|
||||||
|
phone = login
|
||||||
|
else:
|
||||||
|
self.error('unknown login type: {}'.format(login))
|
||||||
|
return
|
||||||
|
url = "https://lkkbyt.mosenergosbyt.ru/gate/do?api_version={}&id_device={}&phone={}&ls={}&process=login&pw_abn={}&type=1".format(self._version, self._device, phone, ls, pass_md5)
|
||||||
|
response = requests.get(url).text
|
||||||
|
session_response = self._parse_response(response)
|
||||||
|
session = session_response['ID_SESSION']
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _parse_response(self, response):
|
||||||
|
et = ElementTree.fromstring(response)
|
||||||
|
|
||||||
|
headers = []
|
||||||
|
values = []
|
||||||
|
for node in et.findall('./h/p'):
|
||||||
|
headers.append(node.text)
|
||||||
|
for node in et.findall('./b/p'):
|
||||||
|
values.append(node.text)
|
||||||
|
result = {}
|
||||||
|
for i in range(0, len(headers)):
|
||||||
|
header = headers[i]
|
||||||
|
value = values[i]
|
||||||
|
result[header] = value
|
||||||
|
return result
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
mosenergo_counter:
|
||||||
|
module: mosenergosbyt
|
||||||
|
class: MosEnergoSbytCounter
|
||||||
|
properties:
|
||||||
|
login: !secret mosenergo_main_login
|
||||||
|
pwd: !secret mosenergo_main_pwd
|
||||||
|
|
||||||
|
mosenergo_counter_updater:
|
||||||
|
module: mosenergosbyt
|
||||||
|
class: MosEnergoSbytUpdater
|
||||||
|
entity_ids:
|
||||||
|
mosenergosbyt_total: sensor.power_mosenergosbyt_total
|
||||||
|
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
|
||||||
|
app: mosenergo_counter
|
||||||
|
dependencies:
|
||||||
|
- mosenergo_counter
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
mosenergo_counter_sender:
|
||||||
|
module: mosenergosbyt
|
||||||
|
class: MosEnergoSbytSender
|
||||||
|
entity_ids:
|
||||||
|
home_power_total: sensor.power_main_total
|
||||||
|
app: mosenergo_counter
|
||||||
|
dependencies:
|
||||||
|
- mosenergo_counter
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
|
||||||
|
mosenergo_alt:
|
||||||
|
module: mosenergosbyt
|
||||||
|
class: MosEnergoSbytCounter
|
||||||
|
properties:
|
||||||
|
login: !secret mosenergo_alt_login
|
||||||
|
pwd: !secret mosenergo_alt_pwd
|
||||||
|
|
||||||
|
mosenergo_alt_updater:
|
||||||
|
module: mosenergosbyt
|
||||||
|
class: MosEnergoSbytUpdater
|
||||||
|
app: mosenergo_alt
|
||||||
|
dependencies:
|
||||||
|
- automation
|
||||||
|
- mosenergo_alt
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import globals
|
||||||
|
import datetime as dt
|
||||||
|
from automation import Automation, Base
|
||||||
|
from datetime import datetime, tzinfo, timedelta
|
||||||
|
from mos_lib import MosAPI, Watercounter, Water, EmpServerException
|
||||||
|
|
||||||
|
class WaterCounterSender(Automation):
|
||||||
|
timezone = '+03:00'
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
self.run_daily(self.send_mosru_counters, dt.time(12, 00, 0))
|
||||||
|
|
||||||
|
def send_mosru_counters(self, kwargs):
|
||||||
|
self.log('Starting sending counters value to mos.ru')
|
||||||
|
new_values = []
|
||||||
|
self.app.update_watercounters()
|
||||||
|
water_hot = self.app.water_hot
|
||||||
|
water_cold = self.app.water_cold
|
||||||
|
counters = [water_hot, water_cold]
|
||||||
|
|
||||||
|
for counter in counters:
|
||||||
|
counter_type = counter['type']
|
||||||
|
|
||||||
|
checkup = counter['checkup'].replace(self.timezone,'')
|
||||||
|
checkup_date = datetime.strptime(checkup, '%Y-%m-%d')
|
||||||
|
|
||||||
|
# подходит дата поверки
|
||||||
|
if checkup_date < datetime.now():
|
||||||
|
self.error('Counter {} checkup date is expired at {}!'.format(counter['counterId'], checkup))
|
||||||
|
continue
|
||||||
|
|
||||||
|
our_sensor = ""
|
||||||
|
if counter_type == 1:
|
||||||
|
our_sensor = self.entity_ids['cold']
|
||||||
|
elif counter_type == 2:
|
||||||
|
our_sensor = self.entity_ids['hot']
|
||||||
|
real_value = int(float(self.get_state(our_sensor)))
|
||||||
|
self.log("Updating counter {} ({}) to real value: {}".format(counter['counterId'], our_sensor, real_value))
|
||||||
|
new_values.append(Watercounter.serialize_for_send(counter, real_value))
|
||||||
|
|
||||||
|
if new_values:
|
||||||
|
try:
|
||||||
|
self.app.send_watercounters(new_values)
|
||||||
|
self.log('Counters values was sent to mos.ru')
|
||||||
|
except EmpServerException as err:
|
||||||
|
self.log('error sending: {}'.format(err))
|
||||||
|
|
||||||
|
|
||||||
|
class WaterCounterSensor(Automation):
|
||||||
|
timezone = '+03:00'
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||||
|
|
||||||
|
def update_sensors(self, args) -> None:
|
||||||
|
self.log('Updating water counters')
|
||||||
|
self.app.update_watercounters()
|
||||||
|
if 'hot' in self.entity_ids:
|
||||||
|
water_hot = self.app.water_hot
|
||||||
|
self.create_water_entity(self.entity_ids['hot'], water_hot)
|
||||||
|
if 'cold' in self.entity_ids:
|
||||||
|
water_cold = self.app.water_cold
|
||||||
|
self.create_water_entity(self.entity_ids['cold'], water_cold)
|
||||||
|
|
||||||
|
def create_water_entity(self, entity_id, counter):
|
||||||
|
indication_max = 0
|
||||||
|
indication_date = ""
|
||||||
|
for indication in counter['indications']:
|
||||||
|
current = float(indication['indication'])
|
||||||
|
if (current > indication_max):
|
||||||
|
indication_max = current
|
||||||
|
indication_date = indication['period']
|
||||||
|
indication_date = indication_date.replace(self.timezone,'')
|
||||||
|
|
||||||
|
counter_type = counter['type']
|
||||||
|
checkup = counter['checkup']
|
||||||
|
type_desc = ""
|
||||||
|
type_desc_ru = ""
|
||||||
|
if counter_type == 1:
|
||||||
|
type_desc = "cold"
|
||||||
|
type_desc_ru = "Холодная"
|
||||||
|
elif counter_type == 2:
|
||||||
|
type_desc = "hot"
|
||||||
|
type_desc_ru = "Горячая"
|
||||||
|
|
||||||
|
icon = "mdi:av-timer"
|
||||||
|
checkup = checkup.replace(self.timezone,'')
|
||||||
|
checkup_date = datetime.strptime(checkup, '%Y-%m-%d')
|
||||||
|
|
||||||
|
# подходит дата поверки
|
||||||
|
if (checkup_date - timedelta(days=31)) < datetime.now():
|
||||||
|
icon = "mdi:alert-octagon"
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
attributes['unit_of_measurement'] = 'm3'
|
||||||
|
attributes['date'] = indication_date
|
||||||
|
attributes['checkup'] = checkup
|
||||||
|
attributes['icon'] = icon
|
||||||
|
attributes['friendly_name'] = "{}(mos.ru)".format(type_desc_ru)
|
||||||
|
attributes['description'] = 'Created and updated from appdaemon ({})'.format(__name__)
|
||||||
|
|
||||||
|
self.set_state(entity_id, state = indication_max, attributes = attributes)
|
||||||
|
|
||||||
|
class EpdBalanceSensor(Automation):
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
self.timer = self.run_every(self.update_sensors, self.datetime(), 1*60*60)
|
||||||
|
|
||||||
|
def update_sensors(self, args) -> None:
|
||||||
|
if 'epd_balance' in self.entity_ids:
|
||||||
|
self.app.update_epd()
|
||||||
|
epd_balance = self.app.epd_balance
|
||||||
|
epd_description = self.app.epd_description
|
||||||
|
self.log('Current epd balance: {}'.format(epd_balance))
|
||||||
|
|
||||||
|
entity_id = self.entity_ids['epd_balance']
|
||||||
|
attributes = {
|
||||||
|
'unit_of_measurement': '₽',
|
||||||
|
'icon': 'mdi:bank-transfer',
|
||||||
|
'date': datetime.now().strftime('%d.%m.%Y'),
|
||||||
|
'unpaid': epd_description,
|
||||||
|
'friendly_name': 'Баланс (ЕПД)',
|
||||||
|
'description': 'Created and updated from appdaemon ({})'.format(__name__)
|
||||||
|
}
|
||||||
|
self.set_state(entity_id, state = epd_balance, attributes = attributes)
|
||||||
|
|
||||||
|
|
||||||
|
class MosruBase(Base):
|
||||||
|
_api = None
|
||||||
|
_flat_id = None
|
||||||
|
_guid='4421a3fb0567714b27dec84d3c3862dc'
|
||||||
|
_app_version = '3.5.0.195 (102)'
|
||||||
|
_user_agent = 'okhttp/3.8.1'
|
||||||
|
_dev_user_agent = 'Android'
|
||||||
|
_timezone = '+03:00'
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
if not globals.check_properties(self,['login', 'pwd', 'token', 'paycode']):
|
||||||
|
return
|
||||||
|
self._api = MosAPI(token=self.properties['token'],
|
||||||
|
user_agent=self._user_agent,
|
||||||
|
guid=self._guid,
|
||||||
|
dev_user_agent=self._dev_user_agent,
|
||||||
|
dev_app_version=self._app_version,
|
||||||
|
timeout=6)
|
||||||
|
self._set_flat_id()
|
||||||
|
|
||||||
|
def _set_flat_id(self):
|
||||||
|
''' Set flat id by properties or pay code '''
|
||||||
|
if 'flat_id' in self.properties:
|
||||||
|
self._flat_id = self.properties['flat_id']
|
||||||
|
return
|
||||||
|
if (self._flat_id != None):
|
||||||
|
return
|
||||||
|
self._check_login()
|
||||||
|
flats = self._api.get_flats()
|
||||||
|
for flat in flats:
|
||||||
|
if (int(flat['paycode']) != self.properties['paycode']):
|
||||||
|
continue
|
||||||
|
self._flat_id = flat['flat_id']
|
||||||
|
|
||||||
|
def _check_login(self):
|
||||||
|
if not self._api.is_active():
|
||||||
|
self._api.login(self.properties['login'], self.properties['pwd'])
|
||||||
|
|
||||||
|
class MosruPower(MosruBase):
|
||||||
|
_power_counters = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def power_total(self):
|
||||||
|
if (self._power_counters == None):
|
||||||
|
self.update_power()
|
||||||
|
return self._power_counters['zones'][0]['value']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def power_balance(self):
|
||||||
|
if (self._power_counters == None):
|
||||||
|
self.update_power()
|
||||||
|
return float(self._power_counters['balance'])
|
||||||
|
|
||||||
|
def update_power(self) -> None:
|
||||||
|
self._check_login()
|
||||||
|
self._power_counters = self._api.get_electrocounters(self._flat_id)
|
||||||
|
|
||||||
|
class MosruWater(MosruBase):
|
||||||
|
_water_counters = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def water_hot(self):
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._water_counters == None):
|
||||||
|
self.update_watercounters()
|
||||||
|
hots_json = list(filter(lambda x: x['type'] == Water.HOT, self._water_counters))
|
||||||
|
return hots_json[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def water_cold(self):
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._water_counters == None):
|
||||||
|
self.update_watercounters()
|
||||||
|
colds_json = list(filter(lambda x: x['type'] == Water.COLD, self._water_counters))
|
||||||
|
return colds_json[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def water_hot_value(self):
|
||||||
|
hot_value = Watercounter.last_value(self.water_hot)
|
||||||
|
return hot_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def water_cold_value(self):
|
||||||
|
cold_value = Watercounter.last_value(self.water_cold)
|
||||||
|
return cold_value
|
||||||
|
|
||||||
|
|
||||||
|
def send_watercounters(self, new_values) -> None:
|
||||||
|
self._check_login()
|
||||||
|
self._api.send_watercounters(self._flat_id, new_values)
|
||||||
|
|
||||||
|
def update_watercounters(self) -> None:
|
||||||
|
self._check_login()
|
||||||
|
self._water_counters = self._api.get_watercounters(self._flat_id)['counters']
|
||||||
|
|
||||||
|
class MosruEpd(MosruBase):
|
||||||
|
_epd_data = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def epd_description(self) -> str:
|
||||||
|
if (self._epd_data == None):
|
||||||
|
self.update_epd()
|
||||||
|
|
||||||
|
unpaid = []
|
||||||
|
for date in self._epd_data:
|
||||||
|
epd = self._epd_data[date]
|
||||||
|
if epd['is_paid']:
|
||||||
|
continue
|
||||||
|
unpaid.append('{}: {}'.format(date, epd['amount']))
|
||||||
|
return ", ".join(unpaid)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def epd_balance(self) -> float:
|
||||||
|
"""Return the current state of counter."""
|
||||||
|
if (self._epd_data == None):
|
||||||
|
self.update_epd()
|
||||||
|
balance = 0
|
||||||
|
for date in self._epd_data:
|
||||||
|
epd = self._epd_data[date]
|
||||||
|
epd_is_paid = epd['is_paid']
|
||||||
|
if epd_is_paid:
|
||||||
|
continue
|
||||||
|
balance = balance - epd['amount']
|
||||||
|
return balance
|
||||||
|
|
||||||
|
def update_epd(self) -> None:
|
||||||
|
self._check_login()
|
||||||
|
data = {}
|
||||||
|
date = datetime.now().date()
|
||||||
|
for i in range (0,6):
|
||||||
|
process_date = date - timedelta(i*365/12)
|
||||||
|
date_str = process_date.strftime('%d.%m.%Y')
|
||||||
|
epd = self._api.get_epd(self._flat_id, date_str, False)
|
||||||
|
for i in range(0, 10): #retries
|
||||||
|
if epd != []:
|
||||||
|
break
|
||||||
|
epd = self._api.get_epd(self._flat_id, date_str, False)
|
||||||
|
epd_first = epd[0]
|
||||||
|
data[date_str] = epd_first
|
||||||
|
# epd_total = epd_first['amount']
|
||||||
|
# epd_is_paid = epd_first['is_paid']
|
||||||
|
# self.log(" - Дата: {}, сумма: {}, оплачен: {}.".format(date_str, epd_total, epd_is_paid))
|
||||||
|
self._epd_data = data
|
||||||
|
|
||||||
|
class MosruClient(MosruEpd, MosruWater, MosruPower):
|
||||||
|
def initialize(self):
|
||||||
|
super().initialize()
|
||||||
|
# self.log('Flat id is: {}'.format(self._flat_id))
|
||||||
|
# self.log('epd balance: {}'.format(self.epd_balance))
|
||||||
|
# self.log('cold: {}'.format(self.water_cold))
|
||||||
|
# self.log('hot: {}'.format(self.water_hot))
|
||||||
|
# self.log('power_counter: {}, balance: {}'.format(self.power_total, self.power_balance))
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#################################
|
||||||
|
# MOS.RU DATA RETRIEVER #
|
||||||
|
#################################
|
||||||
|
mosru_client:
|
||||||
|
class: MosruClient
|
||||||
|
module: mosru
|
||||||
|
properties:
|
||||||
|
login: !secret mosru_login
|
||||||
|
pwd: !secret mosru_pwd
|
||||||
|
token: !secret mosru_token
|
||||||
|
paycode: !secret mosru_paycode
|
||||||
|
flat_id: !secret mosru_flat_id
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
epd_sensor:
|
||||||
|
class: EpdBalanceSensor
|
||||||
|
module: mosru
|
||||||
|
entity_ids:
|
||||||
|
epd_balance: sensor.epd_balance
|
||||||
|
app: mosru_client
|
||||||
|
dependencies:
|
||||||
|
- mosru_client
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
water_sensor:
|
||||||
|
class: WaterCounterSensor
|
||||||
|
module: mosru
|
||||||
|
entity_ids:
|
||||||
|
hot: sensor.counter_mosru_hot
|
||||||
|
cold: sensor.counter_mosru_cold
|
||||||
|
app: mosru_client
|
||||||
|
dependencies:
|
||||||
|
- mosru_client
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
water_sender:
|
||||||
|
class: WaterCounterSender
|
||||||
|
module: mosru
|
||||||
|
entity_ids:
|
||||||
|
hot: sensor.counter_hot
|
||||||
|
cold: sensor.counter_cold
|
||||||
|
app: mosru_client
|
||||||
|
dependencies:
|
||||||
|
- mosru_client
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Water leak detector
|
||||||
|
# close water valve and send a report
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# sensor = water leak sensor group
|
||||||
|
# valve = water valve - app will close it, when leak is detected
|
||||||
|
# notify = notify name to send notification
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class WaterLeak(hass.Hass):
|
||||||
|
ringtone = 2
|
||||||
|
gw_mac = ''
|
||||||
|
def initialize(self):
|
||||||
|
if 'sensor' not in self.args or 'valve' not in self.args or 'notify' not in self.args:
|
||||||
|
self.error("Please provide sensor, valve and notify in config!")
|
||||||
|
return
|
||||||
|
self.listen_state(self.water_leak, self.args['sensor'])
|
||||||
|
|
||||||
|
def water_leak(self, entity, attribute, old, new, kwargs):
|
||||||
|
if new == "on":
|
||||||
|
self.send_alarm()
|
||||||
|
|
||||||
|
def send_alarm(self):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
#close water valve
|
||||||
|
self.turn_off(self.args['valve'])
|
||||||
|
## to ensure that water is closed. It is important!!!
|
||||||
|
self.call_service("mqtt/publish", topic = 'home/bath/bath/valve/set', payload = 'close')
|
||||||
|
self.call_service("xiaomi_aqara/play_ringtone", gw_mac=self.gw_mac, ringtone_id=self.ringtone, ringtone_vol=100)
|
||||||
|
|
||||||
|
message = "ВНИМАНИЕ! В ваше отстутствие сработал датчик протечки воды!!! Выключаю подачу воды."
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
|
|
||||||
|
message = "Состояние датчиков протечки:\n"
|
||||||
|
attributes = self.get_state(self.args['sensor'], attribute = "all")
|
||||||
|
if 'attributes' in attributes:
|
||||||
|
attributes = attributes['attributes']
|
||||||
|
if 'entity_id' in attributes:
|
||||||
|
for entity_id in attributes["entity_id"]:
|
||||||
|
message += '{} ({}) = {}\n'.format(self.friendly_name(entity_id), entity_id, self.get_state(entity_id))
|
||||||
|
self.notify(message, name = self.args['notify'])
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#################################
|
||||||
|
# WATER LEAK #
|
||||||
|
#################################
|
||||||
|
water_leak:
|
||||||
|
module: water_leak
|
||||||
|
class: WaterLeak
|
||||||
|
constraint: input_boolean.water_leak_control
|
||||||
|
sensor: group.water_leak
|
||||||
|
valve: switch.water_valve
|
||||||
|
notify: telegram
|
||||||
|
gw_mac: !secret aqara1_mac
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
|
||||||
|
class Db(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
# Set up callback and listen for state change of desired switch
|
||||||
|
self.listen_state(self.entity_changed, self.args["entity_id"])
|
||||||
|
self.update_stats()
|
||||||
|
|
||||||
|
def entity_changed(self, entity, attribute, old, new, kwargs):
|
||||||
|
self.update_stats()
|
||||||
|
|
||||||
|
def update_stats(self):
|
||||||
|
engine = create_engine(self.args["connectionstring"])
|
||||||
|
session_obj = sessionmaker(bind=engine)
|
||||||
|
session = scoped_session(session_obj)
|
||||||
|
|
||||||
|
results = session.execute('SELECT state,last_changed FROM states where entity_id = "' + self.args["entity_id"] + '" and state<>\'unknown\' and last_changed>=(NOW() - INTERVAL 1 MONTH) order by last_changed limit 1')
|
||||||
|
row = results.first()
|
||||||
|
start_value = float(row['state'])
|
||||||
|
start_date = row['last_changed']
|
||||||
|
|
||||||
|
current_value = float(self.get_state(self.args["entity_id"]))
|
||||||
|
diff = round(current_value - start_value,2)
|
||||||
|
self.log('Counter start value: {} (@{}), current value: {}, diff: {}'.format(start_value, start_date, current_value, diff))
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
db:
|
||||||
|
module: db
|
||||||
|
class: Db
|
||||||
|
connectionstring: !secret mysql_url
|
||||||
|
entity_id: sensor.power_total
|
||||||
|
|
||||||
|
debug: true
|
||||||
|
global_dependencies:
|
||||||
|
- automation
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
# from automation import Automation # type: ignore
|
||||||
|
|
||||||
|
"""
|
||||||
|
Monitor events and output changes to the verbose_log. Nice for debugging purposes.
|
||||||
|
Arguments:
|
||||||
|
- events: List of events to monitor
|
||||||
|
"""
|
||||||
|
class Monitor(hass.Hass):
|
||||||
|
def initialize(self) -> None:
|
||||||
|
# super().initialize()
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
|
||||||
|
events = self.args['events']
|
||||||
|
|
||||||
|
if events != None:
|
||||||
|
for event in self.split_device_list(self.args["events"]):
|
||||||
|
self.log('watching event "{}" for state changes'.format(event))
|
||||||
|
self.listen_event_handle_list.append(self.listen_event(self.changed, event))
|
||||||
|
if len(self.listen_event_handle_list) == 0:
|
||||||
|
self.log('watching all events for state changes')
|
||||||
|
self.listen_event_handle_list.append(self.listen_event(self.changed))
|
||||||
|
|
||||||
|
def changed(self, event_name, data, kwargs):
|
||||||
|
self.log(event_name + ': ' + str(data))
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# eventMonitor:
|
||||||
|
# module: eventMonitor
|
||||||
|
# class: Monitor
|
||||||
|
# global_dependencies:
|
||||||
|
# - automation
|
||||||
|
# events:
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
#
|
||||||
|
# Entity timer
|
||||||
|
# Turn on and off this entities
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
#
|
||||||
|
# None
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class EntityTimer(hass.Hass):
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ["on_time", "off_time"]):
|
||||||
|
return
|
||||||
|
on_time = self.parse_time(self.args["on_time"])
|
||||||
|
off_time = self.parse_time(self.args["off_time"])
|
||||||
|
|
||||||
|
self.on_timer = self.run_daily(self.on_timer_tick, on_time)
|
||||||
|
self.off_timer = self.run_daily(self.off_timer_tick, off_time)
|
||||||
|
|
||||||
|
def on_timer_tick(self, kwargs):
|
||||||
|
self.log('on_timer')
|
||||||
|
if 'entity' in self.args:
|
||||||
|
self.turn_on(self.args['entity'])
|
||||||
|
|
||||||
|
def off_timer_tick(self, kwargs):
|
||||||
|
self.log('off_timer')
|
||||||
|
if 'entity' in self.args:
|
||||||
|
self.turn_off(self.args['entity'])
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
led_timer:
|
||||||
|
module: entity_timer
|
||||||
|
class: EntityTimer
|
||||||
|
on_time: "09:00:00"
|
||||||
|
off_time: "22:00:00"
|
||||||
|
entity: switch.ledstrip
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
led_window_timer:
|
||||||
|
module: entity_timer
|
||||||
|
class: EntityTimer
|
||||||
|
on_time: "09:00:00"
|
||||||
|
off_time: "23:00:00"
|
||||||
|
entity: switch.wifi_socket
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
xmas_tree_timer:
|
||||||
|
module: entity_timer
|
||||||
|
class: EntityTimer
|
||||||
|
on_time: "09:00:00"
|
||||||
|
off_time: "23:00:00"
|
||||||
|
entity: switch.broadlink_switch
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
# tv_timer:
|
||||||
|
# module: entity_timer
|
||||||
|
# class: EntityTimer
|
||||||
|
# on_time: "09:00:00"
|
||||||
|
# off_time: "22:30:00"
|
||||||
|
# entity: switch.plug_158d0001104a0c
|
||||||
|
# global_dependencies:
|
||||||
|
# - globals
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#
|
||||||
|
# Main arguments, all optional
|
||||||
|
#
|
||||||
|
title: Hello Panel
|
||||||
|
widget_dimensions: [120, 120]
|
||||||
|
widget_margins: [5, 5]
|
||||||
|
columns: 8
|
||||||
|
|
||||||
|
label:
|
||||||
|
widget_type: label
|
||||||
|
text: Hello World
|
||||||
|
|
||||||
|
layout:
|
||||||
|
- label(2x2)
|
||||||
Executable
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
while [ 1 ];
|
||||||
|
do
|
||||||
|
certbot renew --agree-tos --standalone --preferred-challenges http-01
|
||||||
|
sleep 1d
|
||||||
|
done
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
mosquitto:
|
||||||
|
image: "monster1025/alpine86-mosquitto:latest"
|
||||||
|
container_name: mosquitto
|
||||||
|
env_file: ./mosquitto/mqtt.env
|
||||||
|
ports:
|
||||||
|
- "1883:1883"
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mariadb
|
||||||
|
container_name: mysql
|
||||||
|
env_file: ./mysql/mysql.env
|
||||||
|
ports:
|
||||||
|
- "3306:3306/tcp"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /srv/mysql:/var/lib/mysql
|
||||||
|
|
||||||
|
hass:
|
||||||
|
image: homeassistant/home-assistant
|
||||||
|
container_name: hass
|
||||||
|
volumes:
|
||||||
|
- ./hass/settings:/config
|
||||||
|
- ./hass/settings/hosts.txt:/etc/hosts:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "8123:8123"
|
||||||
|
- "9898:9898/udp"
|
||||||
|
- "4321:4321/udp"
|
||||||
|
- "51827:51827" #Homekit
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- mosquitto
|
||||||
|
- influxdb
|
||||||
|
- broadlink
|
||||||
|
network_mode: "host"
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 50m
|
||||||
|
|
||||||
|
appdaemon:
|
||||||
|
image: monster1025/appdaemon
|
||||||
|
build:
|
||||||
|
context: ./appdaemon
|
||||||
|
args:
|
||||||
|
VERSION: 3.0.2
|
||||||
|
container_name: appdaemon
|
||||||
|
depends_on:
|
||||||
|
- hass
|
||||||
|
volumes:
|
||||||
|
- ./appdaemon/settings:/conf
|
||||||
|
- ./hass/settings/secrets.yaml:/conf/secrets.yaml:ro
|
||||||
|
- ./hass/settings/hosts.txt:/etc/hosts:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- "hass"
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx
|
||||||
|
hostname: nginx
|
||||||
|
restart: always
|
||||||
|
container_name: nginx
|
||||||
|
links:
|
||||||
|
- "hass"
|
||||||
|
- "appdaemon"
|
||||||
|
volumes:
|
||||||
|
- /srv/homeassistant/hosts.txt:/etc/hosts:ro
|
||||||
|
volumes:
|
||||||
|
- /srv/ssl:/config/keys
|
||||||
|
- ./nginx-config/site-confs:/etc/nginx/conf.d
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
certbot:
|
||||||
|
image: certbot/certbot
|
||||||
|
restart: always
|
||||||
|
container_name: certbot
|
||||||
|
volumes:
|
||||||
|
- ./certbot/docker/update_certs.sh:/update_certs.sh:ro
|
||||||
|
- /srv/ssl:/etc/letsencrypt
|
||||||
|
- /var/lib/letsencrypt:/var/lib/letsencrypt
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
entrypoint: ["/update_certs.sh"]
|
||||||
|
#command: certonly --standalone --agree-tos --standalone-supported-challenges http-01 --email admin@yandex5.ru -d home.yandex5.ru
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
broadlink:
|
||||||
|
image: monster1025/broadlink
|
||||||
|
container_name: broadlink
|
||||||
|
links:
|
||||||
|
- "mosquitto"
|
||||||
|
env_file: ./broadlink/broadlink.env
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 50m
|
||||||
|
###########################
|
||||||
|
# GRAFANA #
|
||||||
|
###########################
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana
|
||||||
|
container_name: grafana
|
||||||
|
volumes:
|
||||||
|
- "/srv/grafana:/var/lib/grafana"
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=grafanapass
|
||||||
|
links:
|
||||||
|
- "influxdb:influxdb"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 50m
|
||||||
|
|
||||||
|
influxdb:
|
||||||
|
image: influxdb
|
||||||
|
container_name: influxdb
|
||||||
|
volumes:
|
||||||
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
|
- "/srv/influxdb/db:/var/lib/influxdb"
|
||||||
|
- "/srv/influxdb/log:/var/log/influxdb"
|
||||||
|
ports:
|
||||||
|
- "8083:8083"
|
||||||
|
- "8086:8086"
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 50m
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
homeassistant:
|
||||||
|
name: Home
|
||||||
|
latitude: !secret home_latitude
|
||||||
|
longitude: !secret home_longitude
|
||||||
|
elevation: 124
|
||||||
|
unit_system: metric
|
||||||
|
time_zone: !secret timezone
|
||||||
|
customize: !include_dir_merge_named configuration/customize
|
||||||
|
customize_domain: !include customize_domain.yaml
|
||||||
|
packages: !include_dir_named packages/
|
||||||
|
|
||||||
|
http:
|
||||||
|
api_password: !secret ha_password
|
||||||
|
base_url: !secret base_url
|
||||||
|
use_x_forwarded_for: true
|
||||||
|
trusted_proxies:
|
||||||
|
- 172.19.0.0/24
|
||||||
|
|
||||||
|
# Enables the frontend
|
||||||
|
frontend:
|
||||||
|
lovelace:
|
||||||
|
mode: yaml
|
||||||
|
|
||||||
|
# Enables configuration UI
|
||||||
|
config:
|
||||||
|
discovery:
|
||||||
|
|
||||||
|
# Enables support for tracking state changes over time
|
||||||
|
recorder: !include configuration/recorder.yaml
|
||||||
|
history:
|
||||||
|
logbook:
|
||||||
|
|
||||||
|
# Components
|
||||||
|
automation: !include_dir_merge_list configuration/automation
|
||||||
|
apple_tv: !include_dir_merge_list configuration/appletv
|
||||||
|
alarm_control_panel: !include_dir_merge_list configuration/alarm_control_panel
|
||||||
|
binary_sensor: !include_dir_merge_list configuration/binary_sensor
|
||||||
|
camera: !include_dir_merge_list configuration/camera
|
||||||
|
#climate: !include_dir_merge_list configuration/climate
|
||||||
|
group: !include_dir_merge_named configuration/group
|
||||||
|
input_boolean: !include_dir_merge_named configuration/input/input_boolean
|
||||||
|
input_datetime: !include_dir_merge_named configuration/input/input_datetime
|
||||||
|
#input_number: !include_dir_merge_named configuration/input/input_number
|
||||||
|
light: !include_dir_merge_list configuration/light
|
||||||
|
notify: !include_dir_merge_list configuration/notify
|
||||||
|
media_player: !include_dir_merge_list configuration/media_player
|
||||||
|
script: !include_dir_merge_named configuration/script
|
||||||
|
sensor: !include_dir_merge_list configuration/sensor
|
||||||
|
switch: !include_dir_merge_list configuration/switch
|
||||||
|
tts: !include_dir_merge_list configuration/tts
|
||||||
|
|
||||||
|
#one item
|
||||||
|
influxdb: !include configuration/influxdb.yaml
|
||||||
|
mqtt: !include configuration/mqtt/mqtt.yaml
|
||||||
|
telegram_bot: !include configuration/bot/telegram_bot.yaml
|
||||||
|
vacuum: !include configuration/xiaomi_aqara/vacuum.yaml
|
||||||
|
xiaomi_aqara: !include configuration/xiaomi_aqara/xiaomi_aqara.yaml
|
||||||
|
nest: !include configuration/nest/nest.yaml
|
||||||
|
homekit: !include configuration/homekit/homekit.yaml
|
||||||
|
|
||||||
|
#power meter
|
||||||
|
modbus: !include configuration/modbus.yaml
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- platform: manual
|
||||||
|
pending_time: 5
|
||||||
|
trigger_time: 5
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- name: appletv4
|
||||||
|
host: !secret atv4_ip
|
||||||
|
login_id: !secret atv4_login
|
||||||
|
credentials: !secret atv4_credentials
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
- alias: cube_flip90
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: flip90
|
||||||
|
action:
|
||||||
|
event: cube_flip90
|
||||||
|
|
||||||
|
- alias: cube_flip180
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: flip180
|
||||||
|
action:
|
||||||
|
event: cube_flip180
|
||||||
|
|
||||||
|
- alias: cube_shake_air
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: shake_air
|
||||||
|
action:
|
||||||
|
event: cube_shake_air
|
||||||
|
|
||||||
|
- alias: cube_move
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: move
|
||||||
|
action:
|
||||||
|
event: cube_move
|
||||||
|
|
||||||
|
- alias: cube_swing
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: swing
|
||||||
|
action:
|
||||||
|
event: cube_swing
|
||||||
|
|
||||||
|
- alias: cube_tap_twice
|
||||||
|
trigger:
|
||||||
|
platform: event
|
||||||
|
event_type: cube_action
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.cube_158d00011065e3
|
||||||
|
click_type: tap_twice
|
||||||
|
action:
|
||||||
|
event: cube_tap_twice
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
- alias: 'Start HomeKit'
|
||||||
|
trigger:
|
||||||
|
- platform: homeassistant
|
||||||
|
event: start
|
||||||
|
action:
|
||||||
|
- delay: 00:00:30 # Wait 30 seconds
|
||||||
|
- service: homekit.start
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- alias: Domofon event
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/domofon/domofon/calltime
|
||||||
|
action:
|
||||||
|
- event: domofon_call
|
||||||
|
event_data_template:
|
||||||
|
payload: '{{ trigger.payload }}'
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#####################################################
|
||||||
|
# MAINROOM #
|
||||||
|
#####################################################
|
||||||
|
- alias: button_living_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/mainroom/status
|
||||||
|
payload: 'click'
|
||||||
|
action:
|
||||||
|
event: button_mainroom_click
|
||||||
|
|
||||||
|
- alias: button_living_double_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/mainroom/status
|
||||||
|
payload: 'double_click'
|
||||||
|
action:
|
||||||
|
event: button_mainroom_double_click
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# BATH #
|
||||||
|
#####################################################
|
||||||
|
- alias: button_bath_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/bath/status
|
||||||
|
payload: 'click'
|
||||||
|
action:
|
||||||
|
event: button_bath_click
|
||||||
|
|
||||||
|
- alias: button_bath_double_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/bath/status
|
||||||
|
payload: 'double_click'
|
||||||
|
action:
|
||||||
|
event: button_bath_double_click
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# HALL #
|
||||||
|
#####################################################
|
||||||
|
- alias: button_hall_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/hall/status
|
||||||
|
payload: 'click'
|
||||||
|
action:
|
||||||
|
event: button_hall_click
|
||||||
|
|
||||||
|
- alias: button_hall_double_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/hall/status
|
||||||
|
payload: 'double_click'
|
||||||
|
action:
|
||||||
|
event: button_hall_double_click
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# KITCHEN #
|
||||||
|
#####################################################
|
||||||
|
- alias: button_kitchen_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/kitchen/status
|
||||||
|
payload: 'click'
|
||||||
|
action:
|
||||||
|
event: button_kitchen_click
|
||||||
|
|
||||||
|
- alias: button_kitchen_double_click
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: home/switch/kitchen/status
|
||||||
|
payload: 'double_click'
|
||||||
|
action:
|
||||||
|
event: button_kitchen_double_click
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
- alias: cube_flip90
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: 'home/cube/mainroom/status'
|
||||||
|
payload: 'flip90'
|
||||||
|
action:
|
||||||
|
- event: cube_flip90
|
||||||
|
|
||||||
|
- alias: cube_flip180
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: 'home/cube/mainroom/status'
|
||||||
|
payload: 'flip180'
|
||||||
|
action:
|
||||||
|
- event: cube_flip180
|
||||||
|
|
||||||
|
- alias: cube_shake_air
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: 'home/cube/mainroom/status'
|
||||||
|
payload: 'shake_air'
|
||||||
|
action:
|
||||||
|
- event: cube_shake_air
|
||||||
|
|
||||||
|
- alias: cube_move
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: 'home/cube/mainroom/status'
|
||||||
|
payload: 'move'
|
||||||
|
action:
|
||||||
|
- event: cube_move
|
||||||
|
|
||||||
|
- alias: cube_tap_twice
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: 'home/cube/mainroom/status'
|
||||||
|
payload: 'tap_twice'
|
||||||
|
action:
|
||||||
|
- event: cube_tap_twice
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#####################################################
|
||||||
|
# MAINROOM #
|
||||||
|
#####################################################
|
||||||
|
- alias: ha_start
|
||||||
|
initial_state: 'on'
|
||||||
|
trigger:
|
||||||
|
platform: homeassistant
|
||||||
|
event: start
|
||||||
|
action:
|
||||||
|
event: ha_started
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
- alias: "Office welcome"
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: work/presence/office/enter_time
|
||||||
|
action:
|
||||||
|
- event: office_enter
|
||||||
|
event_data_template:
|
||||||
|
payload: '{{ trigger.payload }}'
|
||||||
|
|
||||||
|
- alias: "Work leave reminder"
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: work/presence/office/leave_reminder_event
|
||||||
|
action:
|
||||||
|
- event: office_leave_reminder
|
||||||
|
event_data_template:
|
||||||
|
payload: '{{ trigger.payload }}'
|
||||||
|
|
||||||
|
- alias: "Work leave report"
|
||||||
|
trigger:
|
||||||
|
platform: mqtt
|
||||||
|
topic: work/presence/office/leave_event
|
||||||
|
action:
|
||||||
|
- event: office_leave
|
||||||
|
event_data_template:
|
||||||
|
payload: '{{ trigger.payload }}'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/magnet/balcony/status"
|
||||||
|
name: "door_window_sensor_158d00010f7e19"
|
||||||
|
payload_on: "open"
|
||||||
|
payload_off: "close"
|
||||||
|
device_class: opening
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/magnet/bath/status"
|
||||||
|
name: "door_window_sensor_158d000111a469"
|
||||||
|
payload_on: "open"
|
||||||
|
payload_off: "close"
|
||||||
|
device_class: opening
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/magnet/hall/status"
|
||||||
|
name: "door_window_sensor_158d0001130141"
|
||||||
|
payload_on: "open"
|
||||||
|
payload_off: "close"
|
||||||
|
device_class: opening
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/motion/bath/status"
|
||||||
|
name: "motion_sensor_158d0000f9546b" #"bath_motion"
|
||||||
|
payload_on: "motion"
|
||||||
|
payload_off: "no_motion"
|
||||||
|
device_class: motion
|
||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/sensor_motion.aq2/bath2/status"
|
||||||
|
name: "motion_sensor_158d000165fed5" #"bath_motion2"
|
||||||
|
payload_on: "motion"
|
||||||
|
payload_off: "no_motion"
|
||||||
|
device_class: motion
|
||||||
|
- platform: mqtt
|
||||||
|
state_topic: "home/motion/bath3/status"
|
||||||
|
name: "motion_sensor_158d0001537e21" #"bath_motion3"
|
||||||
|
payload_on: "motion"
|
||||||
|
payload_off: "no_motion"
|
||||||
|
device_class: motion
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user