This commit is contained in:
monster
2018-12-30 14:13:57 +03:00
parent e311c31b88
commit 2033fe79e4
225 changed files with 6370 additions and 0 deletions
+17
View File
@@ -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
+129
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
#################################
# YANDEX.ALICE #
#################################
alice:
module: alice
class: YandexAlice
global_dependencies:
- globals
+47
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
#################################
# SCALE #
#################################
mi_scale:
module: mi_scale
class: MiScale
sensor: sensor.xiaomi_scale
friendly_name: "Весы"
global_dependencies:
- globals
+34
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
#################################
# TAGS #
#################################
tags:
module: tags
class: TagsList
secrets_file: /conf/secrets.yaml
global_dependencies:
- globals
+28
View File
@@ -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
+55
View File
@@ -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
+276
View File
@@ -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
+47
View File
@@ -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
+74
View File
@@ -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
+48
View File
@@ -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)
+107
View File
@@ -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
+42
View File
@@ -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
+628
View File
@@ -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
+281
View File
@@ -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
+28
View File
@@ -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))
+9
View File
@@ -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
+14
View File
@@ -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)
View File