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