updating configuration

This commit is contained in:
Your Name
2026-03-10 07:08:59 +00:00
commit 9498a076ee
238 changed files with 18119 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
FROM python:3.12
RUN apt update && apt -y install locales && locale-gen ru_RU.UTF-8
ENV LANG=ru_RU.UTF-8
ENV LANGUAGE=ru_RU:ru
ENV LC_ALL=ru_RU.UTF-8
ENV PYTHONIOENCODING=utf-8
VOLUME /conf
VOLUME /certs
EXPOSE 5050
# Copy appdaemon into image
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Download and build appdaemon, 4.0.0b2
# --branch=4.0.0b1
RUN git clone --recurse-submodules https://github.com/home-assistant/appdaemon.git .
RUN pip install --upgrade pip . && pip3 install requests python-dateutil beautifulsoup4
# Start script
RUN chmod +x /usr/src/app/dockerStart.sh
RUN pip3 install \
attrs==19.1.0 \
#emp_mos_api==0.12 \
packaging==16.8 \
#python-Levenshtein==0.12.0 \
voluptuous==0.13.1 \
tinytuya==1.13.1
ADD ./emp_mos_api /emp_mos_api
CMD [ "./dockerStart.sh" ]
+57
View File
@@ -0,0 +1,57 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# PyCharm
.idea
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+4
View File
@@ -0,0 +1,4 @@
include *.md
include LICENSE
recursive-include emp_mos_api/*.py
recursive-include emp_mos_api/examples/*.py
+110
View File
@@ -0,0 +1,110 @@
### Неофициальная библиотека для запросов к Единой мобильной платформе города Москвы
[Сайт](http://mosapps.mos.ru/dev)
[Единая мобильная платформа города Москвы](http://serviceprovider.if.emp.msk.ru/default/auth)
Для выполнения запросов требуются:
- уникальный ключ вашего приложения (token), [выданный Правительством Москвы](http://mosapps.mos.ru/dev).
- некий guid (guid)
- при работе с телефона указывается user-agent и версия приложения
Создаем объект api
```
from emp_mos_api.mos import MosAPI
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version)
```
## Поддержка вызовов
### Авторизация
Авторизируемся на сервере при помощи номера телефона и пароля, полученного из приложения
```
api.login(args.login, args.pwd)
```
### Завершение сессии
```
api.logout()
```
### Получить профиль и адрес
```
response = api.get_profile()
```
### Поиск адреса по подстроке
```
adresses = api.address_search(pattern)
```
### Получить список квартир
```
flats = api.get_flats()
```
### Удалить квартиру
```
api.flat_delete(flat_id)
```
### Добавить квартиру
```
api.flat_add(name, unom, unad, address, flat_number, paycode)
```
### Получить электронный платежный документ
```
api.get_eepd_wait_result(flat_id, period, timeout):
```
### Получить список счетчиков воды
```
water = api.get_watercounters(flat_id)
```
### Отправить новые показания воды
```
api.send_watercounters(flat_id, new_values)
```
### Получить список счетчиков электроэнергии
```
electro = api.get_electrocounters(flat_id)
```
### Отправить новые показания электроэнергии
```
api.send_electrocounters(flat_id, new_values)
```
### Получить епд
```
epd = api.get_epd(flat_id, begin_period, end_period)
```
### Получить штрафы
```
fines = api.get_car_fines(sts)
```
## Примеры:
[examples](https://github.com/dontsovcmc/emp_mos_ru/tree/master/emp_mos_api/examples)
## Установка:
```
[sudo] pip install -U emp-mos-api
```
Если нет pip, то ставим его:
#### linux
```
sudo apt-get install python-pip python-dev build-essential
```
#### windows
```
python https://bootstrap.pypa.io/get-pip.py
# добавляем пусть до pip в PATH (C:\Python27\Scripts)
перезагружаем терминал
```
## Благодарности
@monster1025
@@ -0,0 +1,3 @@
from emp_mos_api.mos import MosAPI, Water, Watercounter, \
AuthException, EmpServerException
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
from emp_mos_api import MosAPI, AuthException, EmpServerException
# Код написан общим для python2, python3 благодаря
# http://python-future.org/compatible_idioms.html
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', help='application token')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', help='some guid')
parser.add_argument('--dev_user_agent', default='Android', help='application user agent')
parser.add_argument('--dev_app_version', help='application version')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
parser.add_argument('--sts', help='your car sts')
args = parser.parse_args()
#
try:
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
timeout=6)
api.login(args.login, args.pwd)
car_fines = api.get_car_fines(args.sts)
unpaid = car_fines['unpaid']
print("Неоплаченных штрафов: {}".format(len(unpaid)))
for u in unpaid:
print("\nНомер: {}".format(u['seriesAndNumber']))
print("Дата: {}".format(u['date']))
print("Место: {}".format(u['offence_place']))
print("Статья: {}".format(u['offenceType']))
print("Сумма: {}".format(u['cost']))
print("is_discount: {}".format(u['is_discount']))
print("drive_license: {}".format(u['drive_license']))
print("sts_number: {}".format(u['sts_number']))
print("executionState: {}".format(u['executionState']))
print("is_fssp: {}".format(u['is_fssp']))
except AuthException as err:
print('Некорректный логин или пароль')
except EmpServerException as err:
print('Ошибка сервера: {}'.format(err))
except Exception as err:
print('Ошибка: {}'.format(err))
finally:
try:
api.logout()
except Exception as err:
print('Ошибка: {}'.format(err))
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
import time
from emp_mos_api import MosAPI, AuthException, EmpServerException
# Код написан общим для python2, python3 благодаря
# http://python-future.org/compatible_idioms.html
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', help='application token')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', help='some guid')
parser.add_argument('--dev_user_agent', default='Android', help='application user agent')
parser.add_argument('--dev_app_version', help='application version')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
args = parser.parse_args()
#
try:
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
timeout=6)
api.login(args.login, args.pwd)
p = api.get_profile()
flats = api.get_flats()
print('Кол-во квартир: ', len(flats))
assert flats, 'Добавьте квартиру в приложении Госуслуги Москвы'
for f in flats:
# print(f)
print('Квартира #', flats.index(f) + 1)
print('Название: ', f['name'])
print('Адрес: ', f['address'])
print('Номер кв: ', f['flat_number'])
print('Номер платежки: ', f['paycode'])
print('Счет электро: ', f['electro_account'])
print('Счетчик электро: ', f['electro_device'])
epds = api.get_epd(f['flat_id'], '01.01.2018', '31.12.2018')
for epd in epds:
period = epd['period']
epd_total = epd['amount']
epd_is_paid = epd['payment_status'] == 'PAID'
print(" - Дата: {}, сумма: {}, оплачен: {}.".format(period, epd_total, epd_is_paid))
if f['electro_account'] != "":
electro = api.get_electrocounters(f['flat_id'])
balance = electro['balance']
last_zone1 = electro['zones'][0]['value']
print("Баланс электро: {}, показания 1 зоны: {}".format(balance, last_zone1))
# data = [{'counter_id': 'T1', 'indication': 6887, 'period': '2018-12-27'}]
# send = api.send_electrocounters(f['flat_id'], data)
except AuthException as err:
print('Некорректный логин или пароль')
except EmpServerException as err:
print('Ошибка сервера: {}'.format(err))
except Exception as err:
print('Ошибка: {}'.format(err))
finally:
try:
api.logout()
except Exception as err:
print('Ошибка: {}'.format(err))
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
from emp_mos_api import MosAPI, AuthException, EmpServerException
# Код написан общим для python2, python3 благодаря
# http://python-future.org/compatible_idioms.html
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', help='application token')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', help='some guid')
parser.add_argument('--dev_user_agent', default='Android', help='application user agent')
parser.add_argument('--dev_app_version', help='3.5.0.195 (102)')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
args = parser.parse_args()
#
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
timeout=6)
try:
api.login(args.login, args.pwd)
flats = api.get_flats()
print('Кол-во квартир: ', len(flats))
assert flats, 'Добавьте квартиру в приложении Госуслуги Москвы'
for f in flats:
print('Квартира #', flats.index(f) + 1)
print('Название: ', f['name'])
print('Адрес: ', f['address'])
print('Номер кв: ', f['flat_number'])
print('Номер платежки: ', f['paycode'])
except AuthException as err:
print('Некорректный логин или пароль')
except EmpServerException as err:
print('Ошибка сервера: {}'.format(err))
except Exception as err:
print('Ошибка: {}'.format(err))
finally:
try:
api.logout()
except Exception as err:
print('Ошибка: {}'.format(err))
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
from emp_mos_api import MosAPI, AuthException, EmpServerException
# Код написан общим для python2, python3 благодаря
# http://python-future.org/compatible_idioms.html
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', help='application token')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', help='some guid')
parser.add_argument('--dev_user_agent', default='Android', help='application user agent')
parser.add_argument('--dev_app_version', help='application version')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
args = parser.parse_args()
#
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
timeout=6)
try:
api.login(args.login, args.pwd)
p = api.get_profile()
print('ФИО: ', p['firstname'],
' ', p['middlename'],
' ', p['lastname'])
print('Дата рождения: ', p['birthdate'])
print('Телефон: ', p['msisdn'])
print('Эл. почта: ', p['email'])
flats = api.get_flats()
print('Кол-во квартир: ', len(flats))
assert flats, 'Добавьте квартиру в приложении Госуслуги Москвы'
for f in flats:
print('Квартира #', flats.index(f) + 1)
print('Название: ', f['name'])
print('Адрес: ', f['address'])
print('Номер кв: ', f['flat_number'])
print('Номер платежки: ', f['paycode'])
except AuthException as err:
print('Некорректный логин или пароль')
except EmpServerException as err:
print('Ошибка сервера: {}'.format(err))
except Exception as err:
print('Ошибка: {}'.format(err))
finally:
try:
api.logout()
except Exception as err:
print('Ошибка: {}'.format(err))
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
from emp_mos_api import MosAPI, Water, Watercounter
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', help='application token')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', help='some guid')
parser.add_argument('--dev_user_agent', default='Android', help='application user agent')
parser.add_argument('--dev_app_version', help='application version')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
parser.add_argument('--flat_id', help='id квартиры на сервере. если не указывать, отправится в 1ю.')
parser.add_argument('--hot', type=float, help='Hot water new value')
parser.add_argument('--cold', type=float, help='Cold water new value')
args = parser.parse_args()
#
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
verify=True)
try:
api.login(args.login, args.pwd)
if not args.flat_id:
flats = api.get_flats()
assert flats, u'Добавьте квартиру в приложении Госуслуги Москвы'
f = flats[0]
print('Адрес: ', f['address'])
print('Номер кв: ', f['flat_number'])
print('Номер платежки: ', f['paycode'])
flat_id = f['flat_id']
else:
flat_id = args.flat_id
counters = api.get_watercounters(flat_id)['counters']
new_values = []
hots_json = list(filter(lambda x: x['type'] == Water.HOT, counters))
if hots_json:
hot_value = Watercounter.last_value(hots_json[0])
if hot_value:
print('Текущее показание горячей воды: {:.2f} m3'.format(hot_value))
else:
print('Показания горячей воды не передавались больше 3-х месяцев')
if args.hot:
new_values.append(Watercounter.serialize_for_send(hots_json[0], args.hot))
print('Новое показание горячей воды: {:.2f} m3'.format(args.hot))
else:
print('Не найден счетчик горячей воды')
#
colds_json = list(filter(lambda x: x['type'] == Water.COLD, counters))
if colds_json:
cold_value = Watercounter.last_value(colds_json[0])
if cold_value:
print('Текущее показание холодной воды: {:.2f} m3'.format(cold_value))
else:
print('Показания холодной воды не передавались больше 3-х месяцев')
if args.cold:
new_values.append(Watercounter.serialize_for_send(colds_json[0], args.cold))
print('Новое показание холодной воды: {:.2f} m3'.format(args.cold))
else:
print('Не найден счетчик холодной воды')
if new_values:
api.send_watercounters(flat_id, new_values)
print('Показания отправлены на сервер')
finally:
api.logout() #долго
@@ -0,0 +1,956 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import six
import time
from datetime import datetime, tzinfo
import requests
from copy import deepcopy
API_V1_0 = 'https://emp.mos.ru/v1.0'
API_V1_1 = 'https://emp.mos.ru/v1.1'
API_V1_2 = 'https://emp.mos.ru/v1.2'
class AuthException(Exception):
pass
class EmpServerException(Exception):
def __init__(self, message, code):
self.message = message
self.code = code
if six.PY2:
super(Exception, self).__init__('{0} (code:{1})'.format(self.message.encode('utf-8'), self.code))
else:
super().__init__('{0} (code:{1})'.format(self.message, self.code))
class EmpCounterNotVerifiedException(EmpServerException):
"""
'errorCode' 'errorMessage'
3454: 'Не удалось передать показания за сентябрь по счётчикам <серийный номер>: "Невозможно внести показание, поскольку не введены показания за три и более месяца, предшествующих текущему. Для возобновления
удалённой передачи показаний Вам следует обратиться в Центр госуслуг/ГКУ ИС района.
"""
pass
class EmpAlreadySendException(EmpServerException):
"""Не удалось передать показания за X по счётчикам 12345678:
"Вы не можете изменить показания, переданные через портал mos.ru или введенные сотрудниками" \
" Центров госуслуг/ГКУ ИС." 12345678: "Редактируемое показание принято к расчёту." '
"""
pass
class EmpHugeValueException(EmpServerException):
"""
Не удалось передать показания за февраль по счётчикам 123456:
"Не допускается внесение данных, в несколько раз превышающих нормативы водопотребления,
установленные Правительством Москвы." (code:3454)
"""
pass
class EmpValueLessException(EmpServerException):
"""
Не удалось передать показания за февраль по счётчикам 123456:
"Вносимое показание меньше предыдущего. Проверьте корректность вносимого показания.
В случае, если передаваемые данные корректны, Вам следует обратиться в
Центр госуслуг/ГКУ ИС района для уточнения причин ошибки." (code:3454)
"""
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', 'okhttp/3.8.1')
self.dev_app_version = kwargs.get('dev_app_version')
self.dev_user_agent = kwargs.get('dev_user_agent')
self.post_request_data = {
'info': {
'guid': self.guid,
'user_agent': self.dev_user_agent,
'app_version': self.dev_app_version
},
'auth': {
'session_id': self.session_id
}
}
# 3.8.1.216(108)
#self.dev_mobile = kwargs.get('dev_mobile')
#self.info_field = {
# 'app_version': self.dev_app_version,
# 'guid': self.guid,
# 'mobile': self.dev_mobile,
# 'session_id': None,
# 'user_agent': self.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'])
code = answer['errorCode']
msg = answer['errorMessage'] if 'errorMessage' in answer and answer['errorMessage'] else ''
if code == 401:
raise AuthException('Ошибка авторизации')
if code == 3454:
if u'показание принято к расчёту' in msg:
raise EmpAlreadySendException(msg, code)
elif u'Не допускается внесение данных, в несколько раз превышающих нормативы водопотребления' in msg:
raise EmpHugeValueException(msg, code)
elif u'Вносимое показание меньше предыдущего' in msg:
raise EmpValueLessException(msg, code)
elif u'Истёк срок поверки прибора учёта' in msg:
raise EmpCounterNotVerifiedException(msg, code)
if code != 0:
raise EmpServerException(msg, code)
def is_active(self):
"""
:return: True если уже залогинился
"""
return self.session_id is not None
def set_session(self, session_id):
self.session_id = session_id
self.post_request_data['auth']['session_id'] = self.session_id
self.post_request_data['info']['session_id'] = self.session_id
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,
#'mobile': self.dev_mobile # 3.8.1.216(108)
},
'auth': {
'login': telephone,
'password': pwd,
'guid': self.guid
}
}
ret = self.session.post(API_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']
self.post_request_data['auth']['session_id'] = self.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(API_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(API_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 address_search(self, pattern, limit=100):
"""
:param pattern: строка шаблон для поиска
:param limit: сколько ответов выводить
:return:
{
"address": "",
"description": "",
"district": "",
"fullMatch": False,
"unad": 1,
"unum": 123456
}
"""
assert self.session_id
wheaders = deepcopy(self.headers)
wheaders.update({'Content-Type': 'application/json; charset=UTF-8'})
wcrequest = deepcopy(self.post_request_data)
wcrequest.update({
'limit': limit,
'pattern': pattern,
})
ret = self.session.post(API_V1_1 + '/flat/addressSearch',
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 flat_delete(self, flat_id):
"""
Удалить квартиру
:param flat_id: unicode string from flat_response
response[0]['flat_id']
:return: Null
"""
assert self.session_id
wheaders = deepcopy(self.headers)
wheaders.update({
'X-Clears-tags': 'WIDGETS,EPD,ELECTRO_COUNTERS,WATER_COUNTERS,APARTMENT, EPD_WIDGET,ACCRUALS_WIDGET',
'Content-Type': 'application/json; charset=UTF-8' })
wcrequest = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id
})
ret = self.session.post(API_V1_0 + '/flat/delete',
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 flat_add(self, name, unom, unad, address, flat_number, paycode):
"""
Добавление квартиры. Перед добавлением надо узнать unom
:param name: имя квартиры. любое
:param unom: INT из запроса addressSearch
:param unad: INT из запроса addressSearch
:param address: Любой
:param flat_number: Номер квартиры. Любой. Но для отправки показаний нужен точный.
:param paycode: Код плательщика
:param electro_account: Номер счета Мосэнергосбыта
:param electro_device: Серийный номер счетчика. Если заполнен, должен и номер Мосэнергосбыта быть заполнен.
:return:
{
"flat_id":"23611975",
"name":"",
"address":"",
"flat_number":"111",
"unom":"3802879",
"unad":"1",
"paycode":"1234567890",
"electro_account":"",
"electro_device":"",
"intercom":"",
"floor":"",
"entrance_number":"",
"alias":"",
"epd":"1234567890",
"flat":"111",
"building":"",
"street": равно address
}
"""
assert self.session_id
wheaders = deepcopy(self.headers)
wheaders.update({
'X-Clears-tags': 'WIDGETS,EPD,ELECTRO_COUNTERS,WATER_COUNTERS,APARTMENT, EPD_WIDGET,ACCRUALS_WIDGET',
'Content-Type': 'application/json; charset=UTF-8'
})
wcrequest = deepcopy(self.post_request_data)
wcrequest.update({
'a': 0,
'address': address,
'can_update': False,
'flat_number': flat_number,
'name': name,
'paycode': paycode,
'unad': unad,
'unom': unom
})
ret = self.session.post(API_V1_0 + '/flat/add',
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_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 = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'is_widget': False
})
ret = self.session.post(API_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 = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'counters_data': counters_data
})
ret = self.session.post(API_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 = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'is_widget': False,
})
ret = self.session.post(API_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 = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'counters_data': counters_data
})
ret = self.session.post(API_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, begin_period, end_period):
"""
: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: array of
[{
"uin": "234234234234234234234234",
"insurance_amount": null,
"period": "01.12.2021",
"epd_type": "REGULAR",
"payment_amount": "3316,73",
"payment_date": "01.12.2021",
"payment_status": "PAID",
"initiator": "MFC",
"create_date": "11.12.2021 19:34",
"penalty_amount": "0,00",
"amount": "3316,73",
"amount_with_insurance": null
}]
"""
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 = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'begin_period': begin_period,
'end_period': end_period
})
ret = self.session.post(API_V1_2 + '/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_eepd(self, flat_id, period, epd_type='current', rid=None):
"""
Запросить электронный ЕПД (pdf). Первый запрос возвращает rid и документ начинает готовиться.
Последующие запросы будут либо пустые, либо содержать ссылку на PDF + расшифровку полей.
:param flat_id: unicode string from flat_response
response[0]['flat_id']
:param period: unicode string represents date in 27.09.2018 format
:param epd_type: current
:param rid: Если None, то это первый запрос. В ответе будет rid, который нужно добавить в последующие запросы,
пока документ готовится.
:return:
Первый ответ и последующие , пока не готов документ
{
"rid": GUID,
}
{
"pdf": Ссылка на pdf,
"title": "Платежный документ",
"sections": [
{
"title": "Начисления",
"elements": {
"address": "xxxx",
"epd": 123455,
"hint": "text",
"payment_elements": [
],
"period": "DD-месяц-YYYY",
"total_elements": [
]
}
},
{
"title": "Информация",
"elements": [
{
"title": "text",
"body": "text"
},
{
}
]
}
]
}
"""
assert self.session_id
wheaders = deepcopy(self.headers)
wheaders.update({
'Content-Type': 'application/json; charset=UTF-8'})
wcrequest = deepcopy(self.post_request_data)
wcrequest.update({
'flat_id': flat_id,
'period': period,
'type': epd_type,
})
if rid:
wcrequest.update({'rid': rid})
ret = self.session.post(API_V1_0 + '/eepd/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_eepd_wait_result(self, flat_id, period, timeout=10.0):
"""
Запросить электронный ЕПД (pdf) и дождаться результата
:param flat_id: unicode string from flat_response
response[0]['flat_id']
:param period: unicode string represents date in 27.09.2018 format
:param timeout: сколько ждем в секундах результата
"""
start = time.time()
rid = None
result = None
while not result and time.time() - start < timeout:
time.sleep(2.0)
ret = self.get_eepd(flat_id, period, 'current', rid)
if 'rid' in ret:
rid = ret['rid']
if 'pdf' in ret:
result = deepcopy(ret)
return 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 = deepcopy(self.post_request_data)
wcrequest.update({
'sts_number': sts_number
})
ret = self.session.post(API_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 = deepcopy(self.post_request_data)
ret = self.session.post(API_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 set_session(self, *args):
return self.client().set_session(*args)
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 flat_delete(self, *args):
return self.client().flat_delete(*args)
def flat_add(self, *args):
return self.client().flat_add(*args)
def address_search(self, *args):
return self.client().address_search(*args)
# Счетчики воды
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)
# Сформировать pdf Единый платежный документ
def get_eepd_wait_result(self, *args):
return self.client().get_eepd_wait_result(*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
None - когда показания не сданы более 3-х месяцев
"""
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'].split('+')[0] # python 3.6 support https://stackoverflow.com/questions/53291250/python-3-6-datetime-strptime-returns-error-while-python-3-7-works-well
# ValueError: time data '2023-08-08+03:00' does not match format '%Y-%m-%d%z'↵"
d = datetime.strptime(checkup, '%Y-%m-%d') #'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,81 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
from emp_mos_api import MosAPI, Water, Watercounter
if __name__ == "__main__":
# mosru_token: '887033d0649e62a84f80433e823526a1' #new: 887033d0649e62a84f80433e823526a1 #old
# mosru_session: 'a6727b620c79d9b2d24953de734e641e' #new: a3d3fdb831f1a88138abc5a096a06fce #old 3d88ede29eb74bd49fbaa54050fe8b5b
parser = argparse.ArgumentParser(description='emp.mos.ru API')
parser.add_argument('--token', default='705408acae4c705e9f66c9837288b3dd')
parser.add_argument('--user_agent', default='okhttp/3.8.1', help='User-Agent header')
parser.add_argument('--guid', default='80C3E12-A56F-C7A5-4174-14612ED63CF2')
parser.add_argument('--dev_user_agent', default='ios', help='application user agent')
parser.add_argument('--dev_app_version', help='application version', default='4.00.3 (245)')
parser.add_argument('--login', help='your login (phone number: 7xxxxxxxxxx)')
parser.add_argument('--pwd', help='your password')
parser.add_argument('--flat_id', help='id квартиры на сервере. если не указывать, отправится в 1ю.', default='16384751')
parser.add_argument('--hot', type=float, help='Hot water new value')
parser.add_argument('--cold', type=float, help='Cold water new value')
args = parser.parse_args()
#
api = MosAPI(token=args.token,
user_agent=args.user_agent,
guid=args.guid,
dev_user_agent=args.dev_user_agent,
dev_app_version=args.dev_app_version,
verify=True)
try:
api.set_session('a6727b620c79d9b2d24953de734e641e')
counters = api.get_watercounters('16384751')['counters']
print(counters)
new_values = []
hots_json = list(filter(lambda x: x['type'] == Water.HOT, counters))
if hots_json:
hot_value = Watercounter.last_value(hots_json[0])
if hot_value:
print('Текущее показание горячей воды: {:.2f} m3'.format(hot_value))
else:
print('Показания горячей воды не передавались больше 3-х месяцев')
if args.hot:
new_values.append(Watercounter.serialize_for_send(hots_json[0], args.hot))
print('Новое показание горячей воды: {:.2f} m3'.format(args.hot))
else:
print('Не найден счетчик горячей воды')
#
colds_json = list(filter(lambda x: x['type'] == Water.COLD, counters))
if colds_json:
cold_value = Watercounter.last_value(colds_json[0])
if cold_value:
print('Текущее показание холодной воды: {:.2f} m3'.format(cold_value))
else:
print('Показания холодной воды не передавались больше 3-х месяцев')
if args.cold:
new_values.append(Watercounter.serialize_for_send(colds_json[0], args.cold))
print('Новое показание холодной воды: {:.2f} m3'.format(args.cold))
else:
print('Не найден счетчик холодной воды')
if new_values:
api.send_watercounters(flat_id, new_values)
print('Показания отправлены на сервер')
finally:
print('done')
#api.logout() #долго
@@ -0,0 +1,2 @@
distro>=1.3.0
requests>=2.19.1
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Неофициальный Python клиент Единой мобильной платформы города Москвы.
"""
from __future__ import print_function
from setuptools import setup
VERSION_MAJOR = 0
VERSION_MINOR = 12
ver = '%d.%d' % (VERSION_MAJOR, VERSION_MINOR)
if __name__ == '__main__':
"""
Создание пакета
python setup.py sdist --formats=zip bdist_wheel # или --formats=gztar
twine upload dist/*
"""
with open('README.md', 'r') as fh:
long_description = fh.read()
setup(
name='emp_mos_api',
version=ver,
description=__doc__.replace('\n', '').strip(),
long_description=long_description,
long_description_content_type='text/markdown',
author='Dontsov E.',
author_email='dontsovcmc@gmail.com',
url='https://github.com/dontsovcmc/emp_mos_api',
py_modules=['emp_mos_api'],
include_package_data=True,
packages=[
'emp_mos_api',
'emp_mos_api.examples'
],
classifiers=(
"Programming Language :: Python :: 2.7",
'Programming Language :: Python :: 3',
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
),
license='MIT',
platforms=['linux2', 'win32'],
install_requires=[
'requests>=2.19.1'
],
)
+36
View File
@@ -0,0 +1,36 @@
logs:
main_log:
filename: STDOUT
appdaemon:
# threads: 10
latitude: !secret home_latitude
longitude: !secret home_longitude
elevation: 0
time_zone: !secret timezone
ascii_encode: False
plugins:
HASS:
type: hass
namespace: default
ha_url: !secret ha_url
token: !secret ha_token
MQTT:
type: mqtt
namespace: mqtt
verbose: True
client_host: !secret mqtt_ip
client_port: 1883
client_user: !secret mqtt_user
client_password: !secret mqtt_password
client_topics:
- home/presence/#
#http:
# url: !secret ha_url
admin:
api:
hadashboard:
mqtt:
@@ -0,0 +1,44 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import time
#
# 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 AliceIntents(hass.Hass):
entity_id = None
def initialize(self):
self.listen_event(self.yandex_intent, "yandex_intent")
def yandex_intent(self, event_id, event_args, kwargs):
self.log("{} {}".format(event_id, event_args))
text = event_args['text']
room = event_args['room']
alice = event_args['entity_id']
if text == 'Как дела':
self.entity_id = alice
self.run_in(self.run_in_say, 10, command="Кто здесь в {}?".format(room))
def run_in_say(self, kwargs):
if 'command' not in kwargs:
return
text = kwargs['command']
self.say(text)
def command(self, command):
self.call_service('yandex_station/send_command', command='sendText', text=command)
def say(self, command):
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=self.entity_id, media_content_type='text', media_content_id=command)
@@ -0,0 +1,8 @@
#################################
# HOME PRESENSE #
#################################
alice_intents:
module: alice_intents
class: AliceIntents
global_dependencies:
- globals
@@ -0,0 +1,49 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import time
#
# 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 PresenceAction(hass.Hass):
report_delay = 15
def initialize(self):
if 'alice' not in self.args:
self.error("Please provide alice 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
self.command('стоп')
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
command = 'Добро пожаловать домой.'
self.run_in(self.run_in_say, 59, command=command)
def run_in_say(self, kwargs):
if 'command' not in kwargs:
return
text = kwargs['command']
self.say(text)
def command(self, command):
self.call_service('yandex_station/send_command', command='sendText', text=command)
def say(self, command):
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=self.args['alice'], media_content_type='text', media_content_id=command)
@@ -0,0 +1,9 @@
#################################
# HOME PRESENSE #
#################################
presence_action:
module: presence_action
class: PresenceAction
alice: media_player.yandex_station_m00r46000m53db
global_dependencies:
- globals
+29
View File
@@ -0,0 +1,29 @@
#################################################################
## Global
#################################################################
global_modules:
- globals
- automation
- notify
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,277 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
#
# Purifier controller
# Turn on and off purifier by light sensor and timers
#
# Args:
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class BreaserControl(hass.Hass):
away_fan_speed = 8 #1-14
listen_event_handle_list = []
mode = 'auto'
heat_start_at = '23:00:00'
heat_stop_at = '07:00:00'
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.day_mode_timer_event, on_time)
self.off_timer = self.run_daily(self.night_mode_timer_event, off_time)
if 'heat_switch' in self.args:
self.heat_on_timer = self.run_daily(self.heat_mode_on_timer_event, self.heat_start_at)
self.heat_off_timer = self.run_daily(self.heat_mode_off_timer_event, self.heat_stop_at)
self.heat_mode_by_time()
self.listen_event_handle_list.append(self.listen_event(self.away_mode_event, "away_mode"))
self.listen_event_handle_list.append(self.listen_event(self.return_home_mode_event, "return_home_mode"))
if 'co2_sensor' in self.args:
self.listen_event_handle_list.append(self.listen_state(self.state_change, self.args['co2_sensor']))
def state_change(self, entity, attribute, old, new, kwargs):
self.heat_mode_by_time()
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
self.log('Nobody home, ignoring co2 changes.')
return
if new == 'unknown':
return
self.log("{} changed from {} to {}".format(attribute, old, new))
co2_level = int(new)
#day
if self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
self.set_mode_by_co2(co2_level, False)
#night
elif not self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
self.set_mode_by_co2(co2_level, True)
def set_mode_by_co2(self, co2_level, is_night):
self.heat_mode_by_time()
# === Настраиваемые коэффициенты ===
CFG = {
'co2_auto_threshold': 600, # Ниже этого значения — auto_mode
'co2_min_control': 700, # Начало плавного роста скорости
'co2_max_control': 1200, # Максимальная скорость достигается здесь
'speed_min': 30, # Мин. скорость вентиляции
'speed_max': 100, # Макс. скорость вентиляции
'night_speed_factor': 0.75, # Коэффициент снижения скорости ночью
'transition_smoothness': 0.3, # Плавность перехода (0-1)
}
# === Плавная функция перехода от auto к ручной скорости ===
def calculate_speed(co2, cfg, is_night):
# Нормализация CO2 в диапазон [0, 1]
t = (co2 - cfg['co2_min_control']) / (cfg['co2_max_control'] - cfg['co2_min_control'])
t = max(0.0, min(1.0, t))
# Плавная интерполяция (smoothstep для более мягкого старта)
if cfg['transition_smoothness'] > 0:
t = t * t * (3 - 2 * t) # Cubic smoothstep
# Базовая скорость
base_speed = cfg['speed_min'] + t * (cfg['speed_max'] - cfg['speed_min'])
# Ночной коэффициент
day_factor = 1.0 if not is_night else cfg['night_speed_factor']
final_speed = base_speed * day_factor
return int(round(max(cfg['speed_min'], min(cfg['speed_max'], final_speed))))
# === Автоматический режим при низком CO2 ===
if co2_level <= CFG['co2_auto_threshold']:
self.log('CO2 is excellent: {}. Set auto mode.'.format(co2_level))
self.auto_mode()
return
# === Плавный расчёт скорости ===
speed = calculate_speed(co2_level, CFG, is_night)
# Определение уровня для лога
if co2_level < CFG['co2_min_control']:
level_desc = 'good'
elif co2_level < CFG['co2_max_control'] * 0.7:
level_desc = 'moderate'
elif co2_level < CFG['co2_max_control']:
level_desc = 'high'
else:
level_desc = 'extremely high'
period = 'Night' if is_night else 'Day'
self.log('{}, CO2 is {}: {}. Set speed to {}.'.format(
period, level_desc, co2_level, speed))
self.set_speed(speed)
# def set_mode_by_co2(self, co2_level, is_night):
# self.heat_mode_by_time()
# if co2_level < 500:
# self.log('CO2 is fine: {}. Set auto mode.'.format(co2_level))
# self.auto_mode()
# if co2_level > 500 and co2_level < 800:
# if is_night:
# self.log('CO2 is normal: {}. Set night mode'.format(co2_level))
# self.night_mode()
# else:
# self.log('CO2 is normal: {}. Set auto mode.'.format(co2_level))
# self.auto_mode()
# return
# if co2_level > 800 and co2_level < 900:
# if is_night:
# self.log('Night, CO2 is high: {}. Set speed to 45.'.format(co2_level))
# self.set_speed(45)
# else:
# self.log('Day, CO2 is high: {}. Set speed to 65.'.format(co2_level))
# self.set_speed(65)
# return
# if co2_level > 900 and co2_level < 1000:
# if is_night:
# self.log('Night, CO2 is high: {}. Set speed to 65.'.format(co2_level))
# self.set_speed(65)
# else:
# self.log('Day, CO2 is high: {}. Set speed to 80.'.format(co2_level))
# self.set_speed(80)
# return
# if co2_level > 1000 and co2_level < 1200:
# if is_night:
# self.log('Night, CO2 is extremely high: {}. Set speed to 80.'.format(co2_level))
# self.set_speed(80)
# else:
# self.log('Day, CO2 is extremely high: {}. Set speed to 100.'.format(co2_level))
# self.set_speed(100)
# return
# if co2_level > 1200:
# if is_night:
# self.log('Night, CO2 is extremely high: {}. Set speed to 100.'.format(co2_level))
# self.set_speed(100)
# else:
# self.log('Day, CO2 is extremely high: {}. Set speed to 100.'.format(co2_level))
# self.set_speed(100)
# return
def set_speed(self, speed):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.log('Set purifier mode to favorite, speed: {}'.format(speed))
self.call_service("fan/set_preset_mode", entity_id = self.args['entity'], preset_mode='Favorite')
self.call_service("fan/set_percentage", entity_id = self.args['entity'], percentage=speed)
def return_home_mode_event(self, event_id, event_args, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.auto_mode()
def away_mode_event(self, event_id, event_args, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.turn_on(self.args['entity'])
self.away_mode()
def night_mode_timer_event(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
self.log('Nobody home, ignoring automations.')
return
self.night_mode()
def day_mode_timer_event(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
self.log('Nobody home, ignoring automations.')
return
self.auto_mode()
def away_mode(self):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.mode='away'
self.log('Set purifier mode to favorite, speed: {}'.format(self.away_fan_speed))
if 'entity' in self.args:
self.call_service("fan/set_preset_mode", entity_id = self.args['entity'], preset_mode='Favorite')
self.call_service("fan/set_percentage", entity_id = self.args['entity'], percentage=100)
def auto_mode(self):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.mode='auto'
self.log('Set purifier mode to auto.')
if 'entity' in self.args:
self.call_service("fan/set_preset_mode", entity_id = self.args['entity'], preset_mode='Auto')
def night_mode(self):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.mode='night'
self.log('Set purifier mode to silent.')
if 'entity' in self.args:
self.call_service("fan/set_preset_mode", entity_id = self.args['entity'], preset_mode='Sleep')
#heating control
def heat_mode_by_time(self):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint_heat']):
return
if self.time_is_between(self.datetime(), self.heat_stop_at, self.heat_start_at):
self.log('Expensive tariff. Set heat off.')
self.heat_mode_off_timer_event(None)
else:
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
self.log('Nobody home, ignoring automations.')
return
self.log('Cheap tariff. Set heat on.')
self.heat_mode_on_timer_event(None)
def heat_mode_on_timer_event(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint_heat']):
return
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
self.log('Nobody home, ignoring automations.')
return
self.turn_on(self.args['heat_switch'])
def heat_mode_off_timer_event(self, kwargs):
self.turn_off(self.args['heat_switch'])
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,14 @@
breaser_control:
module: breaser_control
class: BreaserControl
on_time: "09:00:00"
off_time: "22:00:00"
entity: fan.ventiliatsionnaia_sistema
ha_panel: alarm_control_panel.ha_alarm
co2_sensor: sensor.co2_bedroom_co2
heat_switch: switch.ventiliatsionnaia_sistema_auxiliary_heat
constraint: input_boolean.vent_control
constraint_heat: input_boolean.vent_control_heat
global_dependencies:
- globals
@@ -0,0 +1,212 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import time, datetime, timedelta
class ClimateControl(hass.Hass):
def initialize(self):
# Настройки из YAML
self.thermostat = self.args.get("thermostat", "climate.virtual_thermostat")
self.ac = self.args.get("ac", "climate.ac_bedroom")
self.radiators = self.args.get("radiators", ["climate.radiator_bedroom", "climate.radiator_bedroom_balcony"])
self.radiator_enabled = self.args.get("radiator_enabled", "input_boolean.radiator_enabled")
self.presence_sensors = self.args.get("presence_sensors", None)
self.no_presence_delay = int(self.args.get("no_presence_delay", 1800)) # секунд
self.window_sensors = self.args.get("window_sensors", None)
self.window_open_delay = int(self.args.get("window_open_delay", 300)) # секунд
self.alarm = self.args.get("alarm", "alarm_control_panel.ha_alarm")
self.listen_state(self.thermostat_changed, self.thermostat)
self.listen_state(self.thermostat_temp_changed, self.thermostat, attribute="temperature")
self.listen_state(self.thermostat_mode_changed, self.thermostat, attribute="hvac_mode")
if self.presence_sensors is not None:
self.listen_state(self.presence_changed, self.presence_sensors)
if self.window_sensors is not None:
self.listen_state(self.window_changed, self.window_sensors)
self.listen_state(self.radiator_enabled_changed, self.radiator_enabled)
self.listen_state(self.alarm_changed, self.alarm)
self.run_every(self.periodic_check, self.datetime() + timedelta(seconds=5), 60)
self.no_presence_handle = None
self.window_open_handle = None
self.last_presence = self.datetime()
self.last_window_open = None
self.updating_thermostat = False
self.listen_state(self.ac_changed, self.ac)
self.listen_state(self.ac_temp_changed, self.ac, attribute="temperature")
self.auto_off_reason = None
def thermostat_changed(self, entity, attribute, old, new, kwargs):
self.log(f"Термостат изменён: {new}")
self.control_climate()
def thermostat_temp_changed(self, entity, attribute, old, new, kwargs):
self.log(f"Изменилась температура термостата: {old} → {new}")
self.control_climate()
def thermostat_mode_changed(self, entity, attribute, old, new, kwargs):
self.log(f"Изменился режим термостата: {old} → {new}")
self.control_climate()
def presence_changed(self, entity, attribute, old, new, kwargs):
if new == "on":
self.last_presence = self.datetime()
if self.no_presence_handle:
self.cancel_timer(self.no_presence_handle)
self.no_presence_handle = None
self.control_climate()
else:
if not self.no_presence_handle:
self.no_presence_handle = self.run_in(self.no_presence_timeout, self.no_presence_delay)
def no_presence_timeout(self, kwargs):
self.log("В комнате долго нет людей — отключаю климат.")
self.turn_off_all(reason="no_presence")
def window_changed(self, entity, attribute, old, new, kwargs):
if new == "on":
if not self.window_open_handle:
self.window_open_handle = self.run_in(self.window_open_timeout, self.window_open_delay)
else:
if self.window_open_handle:
self.cancel_timer(self.window_open_handle)
self.window_open_handle = None
self.auto_off_reason = None # сбросить флаг после закрытия окна
self.control_climate()
def window_open_timeout(self, kwargs):
self.log("Окно открыто более 5 минут — отключаю климат.")
self.turn_off_all(reason="window")
def radiator_enabled_changed(self, entity, attribute, old, new, kwargs):
self.control_climate()
def alarm_changed(self, entity, attribute, old, new, kwargs):
self.control_climate()
def periodic_check(self, kwargs):
self.control_climate()
def control_climate(self):
self.log(f"Проверка климата: alarm={self.get_state(self.alarm)}, окна={self.window_sensors}, присутствие={self.presence_sensors}")
mode = self.get_state(self.thermostat, attribute="hvac_mode")
if mode is None:
mode = self.get_state(self.thermostat)
target_temp = self.get_state(self.thermostat, attribute="temperature")
self.log(f"Режим термостата (hvac_mode/state): {mode}, температура: {target_temp}")
# Проверка сигнализации
if self.get_state(self.alarm) != "disarmed":
self.log("Сигнализация активна — автоматика отключена.")
self.turn_off_all()
return
# Проверка окон
if self.window_sensors:
if any(self.get_state(s) == "on" for s in self.window_sensors):
self.log("Окно открыто — автоматика отключена.")
self.turn_off_all()
return
# Проверка присутствия
if self.presence_sensors:
if all(self.get_state(s) == "off" for s in self.presence_sensors):
if (self.datetime() - self.last_presence).total_seconds() > self.no_presence_delay:
self.log("Нет людей — автоматика отключена.")
self.turn_off_all()
return
# Проверка режима термостата
if mode == "off":
self.log("Термостат в off — автоматика отключена.")
self.turn_off_all()
return
elif mode == "cool":
self.log("Режим охлаждения.")
self.set_ac_mode("cool", target_temp)
self.set_radiators_off()
return
elif mode == "heat":
# Определяем сезон
radiator_on = self.get_state(self.radiator_enabled) == "on"
now = datetime.now().time()
self.log(f"radiator_enabled: {radiator_on}, время: {now}")
if not radiator_on:
self.log("Лето — только сплит-система.")
self.set_ac_mode("heat", target_temp)
self.set_radiators_off()
else:
if time(23,0) <= now or now < time(7,0):
self.log("Зима, ночь — только сплит-система в режиме обогрева.")
self.set_ac_mode("heat", target_temp)
self.set_radiators_off()
else:
self.log("Зима, день — только радиаторы.")
self.set_ac_off()
self.set_radiators_on(target_temp)
def set_ac_mode(self, mode, temp):
current_mode = self.get_state(self.ac)
current_temp = self.get_state(self.ac, attribute="temperature")
current_temp_f = int(current_temp)
temp_f = int(temp)
self.log(f"Текущий режим AC: {current_mode}, температура: {current_temp_f} (требуется: {temp_f})")
if current_mode == mode and isinstance(current_temp_f, int) and isinstance(temp_f, int) and abs(current_temp_f - temp_f) < 0.1:
self.log(f"AC {self.ac}: режим {mode}, температура {temp} — уже установлены, не отправляю команду.")
return
self.updating_thermostat = True
self.call_service("climate/set_temperature", entity_id=self.ac, temperature=temp, hvac_mode=mode)
self.log(f"AC {self.ac}: режим {mode}, температура {temp}")
self.updating_thermostat = False
def set_ac_off(self):
current_mode = self.get_state(self.ac)
if current_mode == "off":
self.log(f"AC {self.ac}: уже выключен, не отправляю команду.")
return
self.updating_thermostat = True
self.call_service("climate/set_hvac_mode", entity_id=self.ac, hvac_mode="off")
self.log(f"AC {self.ac}: выключен")
self.updating_thermostat = False
def set_radiators_on(self, temp):
for r in self.radiators:
self.updating_thermostat = True
self.call_service("climate/set_hvac_mode", entity_id=r, hvac_mode="heat")
self.call_service("climate/set_temperature", entity_id=r, temperature=temp)
self.log(f"Радиатор {r}: режим heat, температура {temp}")
self.updating_thermostat = False
def set_radiators_off(self):
for r in self.radiators:
self.updating_thermostat = True
self.call_service("climate/set_hvac_mode", entity_id=r, hvac_mode="off")
self.log(f"Радиатор {r}: выключен")
self.updating_thermostat = False
def turn_off_all(self, reason=None):
self.auto_off_reason = reason
self.set_ac_off()
self.set_radiators_off()
def ac_changed(self, entity, attribute, old, new, kwargs):
if self.updating_thermostat:
return
if self.auto_off_reason:
self.log(f"AC выключен автоматикой ({self.auto_off_reason}), не синхронизирую термостат в off")
return
mode = self.get_state(self.ac)
temp = self.get_state(self.ac, attribute="temperature")
self.log(f"AC изменён вручную: режим {mode}, температура {temp}")
# Синхронизируем виртуальный термостат
self.updating_thermostat = True
self.call_service("climate/set_hvac_mode", entity_id=self.thermostat, hvac_mode=mode)
self.call_service("climate/set_temperature", entity_id=self.thermostat, temperature=temp)
self.updating_thermostat = False
def ac_temp_changed(self, entity, attribute, old, new, kwargs):
if self.updating_thermostat:
return
self.log(f"AC температура изменена вручную: {old} → {new}")
self.updating_thermostat = True
self.call_service("climate/set_temperature", entity_id=self.thermostat, temperature=new)
self.updating_thermostat = False
@@ -0,0 +1,17 @@
climate_control:
module: climate_control
class: ClimateControl
thermostat: climate.bedroom_climate
ac: climate.ac_bedroom
radiators:
- climate.radiator_bedroom
- climate.radiator_bedroom_balcony
radiator_enabled: input_boolean.radiator_enabled
#presence_sensors:
# - binary_sensor.presence_bedroom
no_presence_delay: 1800 # секунд без присутствия для отключения
window_sensors:
- binary_sensor.door_bedroom_big_window_contact
- binary_sensor.door_balcony_contact
window_open_delay: 300 # секунд открытого окна для отключения
alarm: alarm_control_panel.ha_alarm
+83
View File
@@ -0,0 +1,83 @@
import appdaemon.plugins.hass.hassapi as hass
#
# Cover 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 CoverByPresence(hass.Hass):
def initialize(self):
if 'covers' not in self.args:
self.error("Please provide covers in config!")
return
self.listen_event(self.away_mode, "away_mode")
self.listen_event(self.return_home_mode, "return_home_mode")
sunrise_offset = int(self.args.get('sunrise_offset', '0'))
self.run_at_sunrise(self.run_at_sunrise_func, offset=sunrise_offset)
sunset_offset = int(self.args.get('sunset_offset', '0'))
self.run_at_sunset(self.run_at_sunset_func, offset=sunset_offset)
def run_at_sunrise_func(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('execution is disabled.')
return
if 'guest_mode' in self.args and self.constrain_input_boolean(self.args['guest_mode']):
self.log('guest mode, no run at sunrise.')
return
if 'open_at_sunrise' not in self.args:
self.log('no covers to open at sunrise.')
return
for entity_id in self.args["open_at_sunrise"]:
self.log('opening cover {} at sunrise'.format(entity_id))
self.turn_on_custom(entity_id)
def run_at_sunset_func(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('execution is disabled.')
return
if 'close_at_sunset' not in self.args:
self.log('no covers to close at sunset.')
return
for entity_id in self.args["close_at_sunset"]:
self.log('closing cover {} at sunset'.format(entity_id))
self.turn_off_custom(entity_id)
def away_mode(self, event_id, event_args, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('execution is disabled.')
return
for entity_id in self.args["covers"]:
self.turn_off_custom(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']):
self.log('execution is disabled.')
return
if self.sun_down():
self.log('Sun is down, skip opening covers.')
return
entities = self.args["covers"]
for entity_id in entities:
self.turn_on_custom(entity_id)
def turn_on_custom(self, entity_id):
if 'cover.' in entity_id:
self.call_service("cover/open_cover", entity_id=entity_id)
return
self.turn_on(entity_id)
def turn_off_custom(self, entity_id):
if 'cover.' in entity_id:
self.call_service("cover/close_cover", entity_id=entity_id)
return
self.turn_off(entity_id)
@@ -0,0 +1,29 @@
#################################
# HOME PRESENSE #
#################################
cover_action:
module: cover
class: CoverByPresence
sunrise_offset: 0
sunset_offset: 0
guest_mode: input_boolean.guest_mode
covers:
- cover.curtain_bedroom
- cover.curtain_bedroom_balcony
#- cover.curtain_bedroom_bobbinet_balcony
- cover.curtain_kitchen
- cover.curtain_mainroom
#- cover.curtain_mainroom_bobbinet
close_at_sunset:
- cover.curtain_bedroom
- cover.curtain_bedroom_balcony
- cover.curtain_bedroom_bobbinet_balcony
- cover.curtain_kitchen
- cover.curtain_mainroom
- cover.curtain_mainroom_bobbinet
open_at_sunrise:
- cover.curtain_kitchen
- cover.curtain_mainroom
global_dependencies:
- globals
@@ -0,0 +1,73 @@
import hassapi as hass
import datetime
import adbase as ad
#
# entity: sensor.temperature_bath_humidity
# control_entity: switch.switch_mirror_bath_heater
# constraint: input_boolean.bath_mirror_heat
# threshold: 75
class MirrorHeater(hass.Hass):
timer = None
entity = None
threshold = 75
blocked = False
def initialize(self):
self.log('starting initialize')
sensor = self.args['entity']
self.log('listen [{0}] state'.format(sensor))
self.listen_state(self.humidity_changed, sensor, duration=10)
if 'threshold' in self.args:
self.threshold = self.args['threshold']
self.log('setting threshold to: {}'.format(self.threshold))
def humidity_changed(self, entity, attribute, old_state, new_state, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.log('[{0}] [humidity changed] : {1}, isnumeric: {2}, type: {3}'.format(entity, new_state, new_state.isnumeric(), type(new_state)))
if float(new_state) > self.threshold:
self.log('Control Entity On and running timer...')
self.control_entity_on()
self.run_timer()
else:
self.log('Control Entity Off and stopping timer...')
self.control_entity_off(None)
############ 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' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
for entity in self.args['control_entities']:
state = self.get_state(entity)
if state == 'off':
self.turn_on(entity)
self.log("Turn on ({}).".format(entity))
def control_entity_off(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
for entity in self.args['control_entities']:
state = self.get_state(entity)
if state == 'on':
self.turn_off(entity)
self.log("Turn off ({}).".format(entity))
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,39 @@
#################################
# Контроль подогрева зеркал #
#################################
mirror_heater_control:
# Название класса
class: MirrorHeater
module: mirror_heater_control
entity: sensor.temperature_bath_humidity
control_entities:
- switch.switch_mirror_bath_heater
constraint: input_boolean.bath_mirror_heat
threshold: 75
timeout: 1800 #3600=1hr
global_dependencies:
- globals
# #################################
# # Контроль подогрева зеркал #
# #################################
# mirror_heater_control_shower:
# # Название класса
# class: MirrorHeater
# module: mirror_heater_control
# entity: sensor.temperature_shower_humidity
# control_entities:
# - switch.shower_mirror
# - switch.switch_mirror_shower_heater
# constraint: input_boolean.shower_mirror_heat
# threshold: 75
# timeout: 3600 #3600=1hr
# global_dependencies:
# - globals
@@ -0,0 +1,46 @@
import hassapi as hass
import datetime
import adbase as ad
class TRVUpdater(hass.Hass):
entities = []
def initialize(self):
self.log('starting initialize')
self.entities = self.args['entities']
for sensor in self.entities.keys():
self.log('listen [{0}] state'.format(sensor))
self.listen_state(self.temperature_changed, sensor, duration=10)
current_temperature = self.get_state(sensor)
self.temperature_changed(sensor, 'temperature', '20.0', current_temperature, None)
def temperature_changed(self, entity, attribute, old_state, new_state, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('Отправка температуры отключена в связи с отключением отопления.')
return
self.log('[{0}] [temperature changed] : {1}, isnumeric: {2}, type: {3}'.format(entity, new_state, new_state.isnumeric(), type(new_state)))
topic = self.entities[entity].get('topic_prefix', None)
if topic is not None:
sensor_topic = topic.replace('/sensor_temp', '')
sensor_payload = '{"sensor":"external"}'
self.log('[mqtt publish] topic: "{0}" value: {1}'.format(sensor_topic, sensor_payload))
self.call_service("mqtt/publish", topic=sensor_topic, payload=sensor_payload)
temperature_payload = '{"external_temperature_input":"' + new_state + '"}'
self.log('[mqtt publish] topic: "{0}" value: {1}'.format(sensor_topic, temperature_payload))
self.call_service("mqtt/publish", topic=sensor_topic, payload=temperature_payload)
topics = self.entities[entity].get('topic_prefixes', None)
if topics is not None:
for topic in topics:
sensor_topic = topic.replace('/sensor_temp', '')
sensor_payload = '{"sensor":"external"}'
self.log('[mqtt publish] topic: "{0}" value: {1}'.format(sensor_topic, sensor_payload))
self.call_service("mqtt/publish", topic=sensor_topic, payload=sensor_payload)
temperature_payload = '{"external_temperature_input":' + new_state + '}'
self.log('[mqtt publish] topic: "{0}" value: {1}'.format(sensor_topic, temperature_payload))
self.call_service("mqtt/publish", topic=sensor_topic, payload=temperature_payload)
@@ -0,0 +1,22 @@
#################################
# Обновление температуры на термоголовках #
#################################
trv_updater:
# Название класса
class: TRVUpdater
module: trv_updater
constraint: input_boolean.radiator_enabled
entities:
'sensor.temperature_mainroom_temperature':
topic_prefix: z2m-tcp/radiator_mainroom/set/sensor_temp
'sensor.temperature_bedroom_temperature':
topic_prefixes:
- z2m-tcp/radiator_bedroom/set/sensor_temp
- z2m-tcp/radiator_bedroom_balcony/set/sensor_temp
'sensor.temperature_kitchen_temperature':
topic_prefix: z2m-tcp/radiator_kitchen/set/sensor_temp
'sensor.temperature_childroom_temperature':
topic_prefix: z2m-tcp/radiator_childroom/set/sensor_temp
global_dependencies:
- globals
@@ -0,0 +1,43 @@
import appdaemon.plugins.hass.hassapi as hass
import os
#
# 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):
def initialize(self):
if "notify" not in self.args:
self.error("Please provide notify, sensor, camera in config!")
return
self.listen_event(self.ha_event, "doorbell_call")
def ha_event(self, event_name, data, kwargs):
self.log('{}:{}'.format(event_name, data))
self.notify("Звонок в дверь!!!", name = self.args['notify'])
if 'doorbell_pic' in data:
extra_data = {'photo': {'url':data['doorbell_pic']}}
self.notify("Фото звонившего", name = self.args['notify'], data=extra_data)
self.say('Звонок в дверь')
def command(self, command):
self.call_service('yandex_station/send_command', command='sendText', text=command)
def say(self, command):
if 'alices' in self.args:
for alice in self.args['alices']:
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=alice, media_content_type='text', media_content_id=command, extra={'volume_level': 0.8})
@@ -0,0 +1,17 @@
#################################
# INTERCOM #
#################################
doorbell:
module: doorbell
class: Doorbell
sensor: binary_sensor.doorbell_ring
camera: camera.domofon
notify: telegram
alices:
- media_player.yandex_station_m00r46000m53db
- media_player.yandex_station_m00fhe2003seqr
- media_player.yandex_station_ly00000000000008674600007209d3f5
- media_player.yandex_station_ff98f029ab48468d28c557be
- media_player.yandex_station_u003jc100phn9k
global_dependencies:
- globals
+97
View File
@@ -0,0 +1,97 @@
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 check_constaint(self, name='constraint'):
self.log('checking constaint: {}'.format(name))
if name not in self.args:
self.log('script is not constrained')
return True;
constaint_input = self.args[name]
constaint_state = self.get_state(constaint_input)
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")
def get_arg(args, key):
key = args[key]
return key
def get_arg_list(args, key):
arg_list = []
if isinstance(args[key], list):
arg = args[key]
else:
arg = (args[key]).split(",")
for key in arg:
arg_list.append(key)
return arg_list
+79
View File
@@ -0,0 +1,79 @@
import appdaemon.plugins.hass.hassapi as hass
import re
#
# This app will create groups in runtime by user defined params
# Based on Rene Tode groups plugin ( hass@reot.org )
#
# Args:
# device_type: sensor # or any devicetype
# entity_part: "any_part"
# entities: # list of entities
# - sensor.any_entity
# hidden: False # or True
# view: True # or False
# assumed_state: False # or True
# friendly_name: Your Friendly Name
# nested_view: True # or False
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Groups(hass.Hass):
def initialize(self):
self.run_in(self.create_groups_timer, 10)
def create_groups_timer(self, kwargs):
entitylist = []
expand_group = self.args.get("expand_group", False)
device_type = self.args.get("device_type", None)
all_entities = self.get_state(device_type)
if "entities" in self.args:
for entity in self.args["entities"]:
found = False
for real_entity in all_entities:
if real_entity.lower() == entity.lower():
self.append_entity(entitylist, entity, expand_group)
found = True
if not found:
for real_entity in all_entities:
if re.match(entity.lower(), real_entity.lower()):
self.append_entity(entitylist, real_entity, expand_group)
if 'entity_part' in self.args:
for entity in all_entities:
if self.args["entity_part"] in entity and entity.lower() not in entitylist:
self.append_entity(entitylist, entity, expand_group)
hidden = self.args.get("hidden", False)
view = self.args.get("view", False)
assumed_state = self.args.get("assumed_state",False)
friendly_name = self.args.get("friendly_name","")
name = "group." + self.name
if self.args.get('orderby', None) == 'friendly_name':
entitylist.sort(key=self.sortByFriendlyName)
self.log("Creating group {} with entities: {}".format(name, entitylist))
self.set_state(name,state="on",attributes={"view": view,"hidden": hidden,"assumed_state": assumed_state,"friendly_name": friendly_name,"entity_id": entitylist, 'description': 'Created and updated from appdaemon ({})'.format(__name__)})
def sortByFriendlyName(self, entity):
return self.get_state(entity, attribute='friendly_name')
def append_entity(self, entitylist, entity, expand_group):
entity = entity.lower()
if (('group.' not in entity and expand_group) or not expand_group) and entity not in entitylist:
entitylist.append(entity)
return
if expand_group and 'group.' in entity:
group_entities = self.get_group_entities(entity)
for group_entity in group_entities:
if group_entity not in entitylist:
entitylist.append(group_entity)
def get_group_entities(self, group):
group = self.get_state(group, attribute = "all")
return group["attributes"]["entity_id"]
@@ -0,0 +1,8 @@
groups:
module: groups
class: Groups
friendly_name: Автоматические группы
entities: # list of entities
- media_player.lg_webos_smart_tv
# device_type: light
# entity_part: "_"
@@ -0,0 +1,24 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
class ChildDoorNotifier(hass.Hass):
isLocked = False
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_state(self.state_change, self.args['sensor'])) #, attribute='action'
def state_change(self, entity, attribute, old, new, kwargs):
self.log('state = {}'.format(new))
if new == "on":
self.log('open')
for i in range(1, 7):
self.run_in(self.switch_light, i)
def switch_light(self, kwargs):
for entity in self.args['entities']:
self.toggle(entity)
@@ -0,0 +1,14 @@
#################################
# Door Notifier #
#################################
child_door_notifier:
module: child_door_notifier
class: ChildDoorNotifier
sensor: binary_sensor.door_childroom_contact
entities:
- light.svetlana_bedside_lamp_rgbww_light
- light.danil_bedside_lamp_rgbww_light
global_dependencies:
- globals
@@ -0,0 +1,46 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to control balcony room light based on lux and door sensor
#
# Args:
# door_sensor: door contact sensor
# lux_sensor: xiaomi lux sensor
# light: light entity_id
# lux_min: min lux value to turn light on
#
# Release Notes
#
# Version 1.0:
# Initial Version
class BalconyLightControl(hass.Hass):
def initialize(self):
self.door_sensor = self.args.get("door_sensor", "binary_sensor.door_balcony_contact")
self.light = self.args.get("light", "light.light_balcony_spotlight")
self.lux_sensor = self.args.get("lux_sensor", "sensor.brightness_balcony_illuminance")
self.lux_min = self.args.get("lux_min", 100)
self.log("Initializing balcony light control application")
self.log(f"Door sensor: {self.door_sensor}")
self.log(f"Lux sensor: {self.lux_sensor}")
self.log(f"Lux min: {self.lux_min}")
self.log(f"Light: {self.light}")
# Listen for presence events
self.listen_state(self.state_change, self.args['door_sensor'])
self.log("Presence event handler registered")
def state_change(self, entity, attribute, old, new, kwargs):
self.log(f"door entity: {entity}, attribute: {attribute}, old: {old}, new: {new}")
lux = int(self.get_state(self.lux_sensor))
if new == "on":
self.log("Door is open, lux: {}, lux_min: {}".format(lux, self.lux_min))
if lux < self.lux_min:
self.turn_on(self.light)
else:
self.log("high lux value, skip turn light on")
elif new == "off":
self.log("Door is closed, lux: {}, lux_min: {}".format(lux, self.lux_min))
self.turn_off(self.light)
@@ -0,0 +1,7 @@
light_balcony:
module: light_balcony
class: BalconyLightControl
light: light.light_balcony_spotlight
door_sensor: binary_sensor.door_balcony_contact
lux_sensor: sensor.brightness_balcony_illuminance
lux_min: 150
@@ -0,0 +1,36 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to control main room light based on Aqara FP1 presence sensor
#
# Args:
# presence_sensor: presence sensor entity_id (default: sensor.presence_aqara_mainroom_presence_event)
# light: light entity_id (default: light.light_mainroom_pc_spotlight)
#
# Release Notes
#
# Version 1.0:
# Initial Version
class MainRoomLightControl(hass.Hass):
def initialize(self):
self.presence_sensor = self.args.get("presence_sensor", "sensor.presence_aqara_mainroom_presence_event")
self.light = self.args.get("light", "light.light_mainroom_pc_spotlight")
self.log("Initializing light control application")
self.log(f"Presence sensor: {self.presence_sensor}")
self.log(f"Light: {self.light}")
# Listen for presence events
self.listen_state(self.state_change, self.args['presence_sensor'])
self.log("Presence event handler registered")
def state_change(self, entity, attribute, old, new, kwargs):
self.log(f"entity: {entity}, attribute: {attribute}, old: {old}, new: {new}")
if new == "away":
self.log("Status 'away' - turning off light")
self.turn_off(self.light)
else:
self.log("Presence detected - turning on light")
self.turn_on(self.light)
@@ -0,0 +1,5 @@
light_mainroom:
module: light_mainroom
class: MainRoomLightControl
presence_sensor: sensor.presence_aqara_mainroom_presence_event11
light: light.light_mainroom_pc_spotlight
@@ -0,0 +1,29 @@
#################################
# LIGHT BUTTONS #
#################################
# light_button_bath:
# module: lightbutton
# class: LightButton
# sensor: binary_sensor.wall_switch_bath
# entities: light.bath
# # control_input: input_boolean.bath_light_control
# global_dependencies:
# - globals
light_button_main:
module: lightbutton
class: LightButton
sensor: sensor.bed_button
entities: light.yeelight_color1_34ce008fe328,light.yeelight_color1_286c071065c7,light.yeelight_mono1_34ce0087ff33,light.yeelight_mono1_f0b4290e5444,light.yeelight_mono1_f0b429a8f6df,light.yeelight_mono1_286c07f12bed,light.yeelight_mono1_f0b429a8d936,light.lamp1
global_dependencies:
- globals
light_button_hall:
module: lightbutton
class: LightButton
sensor: sensor.0x00158d000183a5fe_action
entities: switch.plug_light_hall
global_dependencies:
- globals
@@ -0,0 +1,133 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
class LightButton(hass.Hass):
isLocked = False
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"))
self.listen_event_handle_list.append(self.listen_state(self.state_change, self.args['sensor'])) #, attribute='action'
def state_change(self, entity, attribute, old, new, kwargs):
if new == "single":
self.log('button_click')
self.make_action_by_mode("off")
if new == "double":
self.log('button_dblclick')
self.make_action_by_mode("on")
def event_detected(self, event_name, data, kwargs):
if data["entity_id"] != self.args["sensor"] or self.isLocked:
return
self.isLocked = True
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")
self.run_in(self.clear_lock, 1)
def clear_lock(self, kwargs):
self.isLocked = False
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)
#light
def set_on(self, entity_id):
brightness = 1 #night mode
if self.time_is_between(self.datetime(), '07:00:00', '23:00:00'):
brightness=255 #day mode
if entity_id.startswith("light") and 'color' in entity_id:
self.call_service("homeassistant/turn_on", entity_id = entity_id, transition = 1, brightness = brightness, color_temp=204)
elif entity_id.startswith("light"):
self.call_service("homeassistant/turn_on", entity_id = entity_id, transition = 1, brightness = brightness)
else:
self.turn_on(entity_id)
def set_off(self, entity_id):
if entity_id.startswith("light"):
self.call_service("homeassistant/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)
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,25 @@
light_control_hall:
module: lightcontrol
class: LightControl
sensor: binary_sensor.motion_sensor_hall
control_entity: switch.plug_light_hall
control_night_entity: light.gateway
timeout: 240
constraint: input_boolean.hall_light_control
ha_panel: alarm_control_panel.ha_alarm
vacuum: vacuum.xiaomi_vacuum_cleaner
global_dependencies:
- globals
light_control_kitchen:
module: lightcontrol
class: LightControl
sensor: group.kitchen_motion_sensors
control_entity: switch.light_kitchen
timeout: 600
constraint: input_boolean.kitchen_light_control
ha_panel: alarm_control_panel.ha_alarm
vacuum: vacuum.xiaomi_vacuum_cleaner
global_dependencies:
- globals
@@ -0,0 +1,123 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
#
# 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
vacuum_state = "docked"
ha_panel_state = ""
if "vacuum" in self.args:
vacuum_state = self.get_state(self.args["vacuum"])
if "ha_panel" in self.args:
ha_panel_state = self.get_state(self.args["ha_panel"])
#Если работает пылесос и сработал датчик движения
if ha_panel_state == "armed_away" and vacuum_state == 'cleaning':
self.log('Motion sensor is triggered, but vacuum is cleaning.')
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
entity = self.args['control_entity']
if not self.time_is_between(self.datetime(), '07:00:00', '23:00:00') and 'control_night_entity' in self.args:
entity=self.args['control_night_entity'] #day mode
if self.get_state(entity) == 'off':
self.turn_on(entity)
self.log("Light on ({}).".format(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
entity = self.args['control_entity']
if not self.time_is_between(self.datetime(), '07:00:00', '23:00:00') and 'control_night_entity' in self.args:
entity=self.args['control_night_entity'] #day mode
if self.get_state(entity) == 'on':
self.turn_off(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
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,136 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
import time
#
# 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
entity = self.args['control_entity']
if not self.time_is_between(self.datetime(), '07:00:00', '23:00:00') and 'control_night_entity' in self.args:
entity=self.args['control_night_entity'] #day mode
if self.get_state(entity) == 'off':
self.turn_on(entity)
self.log("Light on.")
if 'control_mini_light_entity' in self.args:
self.turn_on(self.args['control_mini_light_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.door_state_changed == False:
self.log('Motion is detected inside and door is still closed - dont turn off the light')
return
is_night = False
entity = self.args['control_entity']
if not self.time_is_between(self.datetime(), '07:00:00', '23:00:00') and 'control_night_entity' in self.args:
entity=self.args['control_night_entity'] #day mode
is_night = True
if self.get_state(entity) == 'on':
self.turn_off(entity)
self.log("Light off.")
if 'control_mini_light_entity' in self.args:
self.turn_off(self.args['control_mini_light_entity'])
if not is_night and 'control_night_entity' in self.args:
self.turn_off(self.args['control_night_entity'])
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
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,39 @@
#################################
# LIGHT CONTROL #
#################################
light_control_bath:
module: lightcontrol_bath
class: LightControlBath
sensor: binary_sensor.motion_bath
door_sensor: binary_sensor.door_bath_contact
control_entity: switch.light_bath
control_night_entity: switch.light_bath_mirror
control_mini_light_entity: light.light_bath_shelf
timeout: 240
constraint: input_boolean.bath_light_control
global_dependencies:
- globals
light_control_shower:
module: lightcontrol_bath
class: LightControlBath
sensor: binary_sensor.motion_shower_center_occupancy
door_sensor: binary_sensor.door_shower_contact
control_entity: switch.light_shower_main_spotlights
control_night_entity: switch.shower_mirror
control_mini_light_entity: light.light_shower_shelf_light
timeout: 240
constraint: input_boolean.shower_light_control
global_dependencies:
- globals
light_control_shower_shower:
module: lightcontrol_bath
class: LightControlBath
sensor: binary_sensor.motion_shower_shower_occupancy
door_sensor: binary_sensor.door_shower_contact
control_entity: switch.light_shower_shower_spotlights
timeout: 360
constraint: input_boolean.shower_light_control
global_dependencies:
- globals
@@ -0,0 +1,131 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
import time
#
# 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 LightControlHall(hass.Hass):
timer = None
turn_on_by_door = None
def initialize(self):
if not globals.check_args(self, ["motion_sensors", "door_sensors", "timeout", "control_entity"]):
return
self.listen_event_handle_list = []
for sensor in self.args['door_sensors']:
self.listen_event_handle_list.append(self.listen_state(self.sensor_trigger, sensor))
for sensor in self.args['motion_sensors']:
self.listen_event_handle_list.append(self.listen_state(self.motion_trigger, sensor))
for event in self.args['events']:
self.listen_event(self.event_trigger, event)
def motion_trigger(self, entity, attribute, old, new, kwargs):
#sensor is off
if new == 'off' and old == 'on' and self.turn_on_by_door != None:
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':
self.stop_timer()
def sensor_trigger(self, entity, attribute, old, new, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
#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.turn_on_by_door = True
self.stop_timer()
def event_trigger(self, event_name, data, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
#turnin on control entity and wait while sensor will be off, than run timer.
self.control_entity_on()
self.turn_on_by_door = True
self.stop_timer()
self.run_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
entity = self.args['control_entity']
state = self.get_state(entity)
self.turn_on(entity)
self.log("Light on ({}).".format(entity))
def control_entity_off(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.turn_on_by_door = None
entity = self.args['control_entity']
state = self.get_state(entity)
self.turn_off(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
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,18 @@
light_control_hall:
module: lightcontrol_hall
class: LightControlHall
motion_sensors:
- binary_sensor.motion_hall_1_occupancy
- binary_sensor.motion_hall_2_occupancy
door_sensors:
- binary_sensor.door_locker_hall_right_contact
- binary_sensor.door_locker_hall_left_contact
- binary_sensor.door_locker2_hall_left_contact
- binary_sensor.door_locker2_hall_right_contact
events:
- door_opened
control_entity: light.light_hall_long
timeout: 120
constraint: input_boolean.locker_light_control
global_dependencies:
- globals
@@ -0,0 +1,149 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
import time
#
# 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 LightControlKitchenset(hass.Hass):
timer = None
def initialize(self):
if "sensors" not in self.args or "timeout" not in self.args or "control_entity" not in self.args:
self.error("Please provide sensor, control_entity and timeout in config!")
return
for sensor in self.args['sensors']:
self.listen_state(self.sensor_trigger, sensor)
if 'night_sensors' in self.args:
for sensor in self.args['night_sensors']:
self.listen_state(self.sensor_night_trigger, sensor)
def sensor_trigger(self, entity, attribute, old, new, kwargs):
#sensor is on
if new == 'on' and old == 'off':
#turnin on control entity and wait while sensor will be off, than run timer.
self.log('motion detected: {}'.format(entity))
self.control_entity_on()
self.stop_timer()
#sensor is off
if new == 'off' and old == 'on':
self.log('motion stop detected:{}'.format(entity))
state = self.get_motion_state()
if state == 'off':
self.run_timer()
def sensor_night_trigger(self, entity, attribute, old, new, kwargs):
door_state = 'on'
if 'door_sensors' in self.args:
for door in self.args['door_sensors']:
door_state = self.get_state(door)
self.log('door_state: {}'.format(door_state))
if door_state == 'off':
door_state = 'off'
if new == 'on' and old == 'off' and door_state == 'on':
if self.is_night():
self.log('night motion detected: {}'.format(entity))
self.control_entity_on()
self.stop_timer()
if new == 'off' and old == 'on':
self.log('night motion stop detected:{}'.format(entity))
state = self.get_motion_state()
if state == 'off':
self.run_timer()
############ SENSOR GROUP ########################
def get_motion_state(self):
sensors = self.args['sensors']
night_sensors = []
if 'night_sensors' in self.args and self.is_night():
night_sensors = self.args['night_sensors']
all_sensors = sensors + night_sensors
self.log('all sensors: {}'.format(all_sensors))
for sensor in all_sensors:
state = self.get_state(sensor)
if state == 'on':
return 'on'
return 'off'
def is_night(self):
return not self.time_is_between(self.datetime(), '07:00:00', '23:00:00')
############ 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
entity = self.args['control_entity']
if self.get_state(entity) == 'off':
self.turn_on(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
entity = self.args['control_entity']
if self.get_state(entity) == 'on':
self.turn_off(entity)
self.log("Light off.")
self.stop_timer()
def run_timer(self):
if self.timer != None:
self.stop_timer()
timeout = self.args['timeout']
if 'night_timeout' in self.args and self.is_night():
timeout = self.args['night_timeout']
self.log('Sensor is off - running timer for {}s'.format(timeout))
self.timer = self.run_in(self.control_entity_off, timeout)
def stop_timer(self):
if (self.timer == None):
return
self.log('stopping timer.')
self.cancel_timer(self.timer)
self.timer = None
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,20 @@
#################################
# LIGHT CONTROL #
#################################
light_control_kitchenset:
module: lightcontrol_kitchenset
class: LightControlKitchenset
constraint: input_boolean.kitchenset_light_control
sensors:
- binary_sensor.motion_kitchenset_left_occupancy
- binary_sensor.motion_kitchenset_right_occupancy
night_sensors:
- binary_sensor.motion_hall_1_occupancy
- binary_sensor.motion_hall_2_occupancy
door_sensors:
- binary_sensor.door_kitchen_contact
control_entity: switch.light_kitchen_set
timeout: 900 #1200 - 20 mins
night_timeout: 300 #300 - 5 min
global_dependencies:
- globals
@@ -0,0 +1,55 @@
import appdaemon.plugins.hass.hassapi as hass
#
# Aqara lock notifications
# Send notification to telegram of lock state
#
# Args:
# lock_state_entity - entity of aqara lock state
# notify - notify entity to send message
#
# Release Notes
#
# Version 1.0:
# Initial Version
class AqaraLock(hass.Hass):
def initialize(self):
if 'lock_state_entity' not in self.args or 'notify' not in self.args:
self.error("Please provide lock_state_entity and notify in config!")
return
lock_state_entity = self.args['lock_state_entity']
self.log(' [+] listening for lock_state_entity sensor {}'.format(lock_state_entity))
self.listen_state(self.sensor_trigger, lock_state_entity)
def sensor_trigger(self, entity, attribute, old, new, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
#(old != "" and old != "unknown") or
if new == "unknown" or new == "" or new == "unavailable":
self.log('skiping event. {} changed from {} to {}'.format(entity, old, new))
return
self.log('{} changed from {} to {}'.format(entity, old, new))
action = new
self.process_event(action)
def process_event(self, eventname):
if eventname == "ring_bell":
self.fire_event('doorbell_call')
elif eventname == 'lock_opened_outside':
self.log("Дверь открыта снаружи, событие: {}.".format(eventname))
elif 'finger_open_' in eventname:
self.fire_event('door_opened')
self.log("Дверь открыта отпечатком пальца, событие: {}.".format(eventname))
self.notify_tg("Дверь открыта отпечатком пальца, событие: {}.".format(eventname))
else:
self.log("Неизвестное событие замка: {}.".format(eventname))
self.notify_tg("Неизвестное событие замка: {}".format(eventname))
def notify_tg(self, msg):
extra_data = {'parse_mode': 'html'}
self.notify(msg, name = self.args['notify'], data=extra_data)
@@ -0,0 +1,8 @@
aqara_lock:
module: aqara_lock
class: AqaraLock
lock_state_entity: sensor.door_lock_mqtt_action
constraint: input_boolean.aqara_lock_report
notify: telegram_monster
global_dependencies:
- globals
@@ -0,0 +1,65 @@
import datetime
from automation import Automation, Base # type: ignore
#
# App to send email report for low balance
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Balance(Base):
def initialize(self):
super().initialize()
self.check_balance({"force": 1})
self.run_daily(self.check_balance, datetime.time(10, 0, 0))
def check_balance(self, kwargs):
# if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
# return
values = {}
low = []
for key in self.entity_ids:
device = self.entity_ids[key]
balance = self.get_state(device)
if balance != None and self.is_number(balance):
if float(balance) < self.args["threshold"]:
low.append(device)
values[device] = balance
message=""
name = "Алтуфьево"
if 'name' in self.args:
name = self.args['name']
if low:
message += "Пожалуйста пополните баланс ({}): (&lt; {}) \n".format(name, self.args["threshold"])
for device in low:
message += " - {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if low or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.log(message)
self.notify_tg(message) #name = "telegram_monster"
def is_number(self, s):
try:
float(s)
return True
except ValueError:
return False
def notify_tg(self, msg):
if not 'notify' in self.args:
return
extra_data = {'parse_mode': 'html'}
self.notify(msg, name = self.args['notify'], data=extra_data)
@@ -0,0 +1,61 @@
#################################
# SENSOR BATTERY LOW REMINDER #
#################################
balance_low_report:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Алтуфьево
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_balance
epd_balance: sensor.epd_balance
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
balance_low_report_belorechenskaya:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Белореченская
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_belorechenskaya_balance
epd_balance: sensor.epd_balance_belorechenskaya
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
balance_low_report_fonvizina:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Фонвизина
entity_ids:
fonvizina_epd_balance: sensor.fonvizina_epd_balance
fonvizina_trash_balance: sensor.fonvizina_trash_balance
fonvizina_kladovka_balance: sensor.fonvizina_kladovka_balance
fonvizina_epd_mosru_balance: sensor.epd_balance_mosru_fonvizina
always_send: 0
threshold: -100
inverted: true
notify: telegram
global_dependencies:
- globals
balance_low_report_milashenkova:
class: Balance
module: balance
constraint: input_boolean.balance_report
name: Милашенкова
entity_ids:
mosenergosbyt_balance: sensor.power_mosenergosbyt_milashenkova_balance
epd_balance: sensor.epd_balance_milashenkova
always_send: 0
threshold: -100
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,65 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to send email report for devices running low on battery
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Battery(hass.Hass):
def initialize(self):
#self.check_batteries({"force": 1})
time = datetime.time(10, 0, 0)
self.run_daily(self.check_batteries, time)
self.check_batteries(None)
def check_batteries(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
devices = self.get_state()
values = {}
low = []
for deviceName in devices:
device = devices[deviceName]
if device == None:
continue
battery = None
if "attributes" in device:
if "battery" in device["attributes"]:
battery = device["attributes"]["battery"]
elif "battery_level" in device["attributes"]:
battery = device["attributes"]["battery_level"]
elif '_battery' in device['entity_id']:
battery = self.get_state(device['entity_id'])
if not battery.isdigit():
continue
battery = int(battery)
print('{} = {}'.format(device['entity_id'], battery))
if battery != None:
print('{} = {}'.format(device['entity_id'], battery))
if battery < self.args["threshold"] and battery != 0:
low.append(deviceName)
values[deviceName] = battery
message=""
if low:
message += "У следующих датчиков садится батарея: (< {}) \n".format(self.args["threshold"])
for device in low:
message += "{}: {}%\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if low: # or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
@@ -0,0 +1,12 @@
#################################
# SENSOR BATTERY LOW REMINDER #
#################################
sensor_battery_low_report:
class: Battery
module: battery
constraint: input_boolean.battery_report
always_send: 0
threshold: 10
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,165 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
class BMSMonitor(hass.Hass):
def initialize(self):
# Получаем настройки из apps.yaml
self.chat_id = self.args.get("chat_id")
self.power_entity = self.args.get("power_sensor")
self.soc_entity = self.args.get("soc_sensor")
self.current_entity = self.args.get("current_sensor")
self.voltage_entity = self.args.get("voltage_sensor")
self.threshold = float(self.args.get("power_threshold", 5))
self.interval = int(self.args.get("discharge_interval", 300))
self.notification_target = self.args.get("notification_target", 'telegram')
# Внутренние переменные состояния
self.current_state = "IDLE" # IDLE, CHARGING, DISCHARGING
self.timer_handle = None
self.was_charging = False
# Переменная для хранения времени начала отключения
self.outage_start_time = None
# Слушаем изменения мощности
self.listen_state(self.on_power_change, self.power_entity)
# Слушаем SOC для детектирования 100%
self.listen_state(self.on_soc_change, self.soc_entity)
self.log("BMS Monitor запущен. Ожидание изменений...")
def on_power_change(self, entity, attribute, old, new, kwargs):
try:
power = float(new)
except (ValueError, TypeError):
return
# Определяем новый режим
if power > self.threshold:
new_state = "CHARGING"
elif power < -self.threshold:
new_state = "DISCHARGING"
else:
new_state = "IDLE"
# Логика переходов состояний
if self.current_state != new_state:
self.log(f"Смена состояния BMS: {self.current_state} -> {new_state}")
# 1. Переход в разрядку (Отключили питание)
if new_state == "DISCHARGING" and self.current_state != "DISCHARGING":
# Фиксируем время начала отключения
self.outage_start_time = datetime.datetime.now()
self.send_telegram("⚠️ <b>ВНИМАНИЕ: Отключено питание!</b>\nБатарея перешла в режим разрядки.", "html")
self.start_discharge_timer()
# 2. Выход из разрядки (Питание восстановлено)
# Срабатывает при переходе DISCHARGING -> IDLE или DISCHARGING -> CHARGING
elif self.current_state == "DISCHARGING" and new_state in ("IDLE", "CHARGING"):
duration_msg = ""
if self.outage_start_time:
duration = datetime.datetime.now() - self.outage_start_time
duration_msg = f"\n⏱ <b>Время без электричества: {self._format_timedelta(duration)}</b>"
self.outage_start_time = None # Сбрасываем таймер
self.send_telegram(f"✅ <b>Питание восстановлено!</b>\nБатарея вышла из режима разрядки.{duration_msg}", "html")
self.stop_discharge_timer()
# 3. Переход в простой после зарядки (Зарядка окончена)
elif new_state == "IDLE" and self.current_state == "CHARGING":
self.send_telegram("🔋 <b>Зарядка завершена!</b>\nБатарея полностью заряжена или нагрузка отключена.", "html")
# 4. Переход IDLE -> CHARGING (если не было разрядки перед этим)
elif new_state == "CHARGING" and self.current_state == "IDLE":
# Можно добавить отдельное уведомление, если нужно
pass
self.current_state = new_state
self.was_charging = (new_state == "CHARGING")
def on_soc_change(self, entity, attribute, old, new, kwargs):
# Дополнительная проверка на 100% SOC, если мощность еще не упала в ноль
try:
soc = float(new)
if soc >= 100 and self.current_state == "CHARGING" and not self.was_charging:
# Небольшая защита от повторных уведомлений
self.was_charging = True
except (ValueError, TypeError):
pass
def start_discharge_timer(self):
if self.timer_handle is None:
# Запускаем таймер: сразу первое сообщение, потом каждые N секунд
self.send_status_report()
self.timer_handle = self.run_every(self.periodic_status, datetime.datetime.now(), self.interval)
self.log("Таймер статуса разрядки запущен")
def stop_discharge_timer(self):
if self.timer_handle is not None:
self.cancel_timer(self.timer_handle)
self.timer_handle = None
self.log("Таймер статуса разрядки остановлен")
def periodic_status(self, kwargs):
# Проверяем, все ли еще в режиме разрядки перед отправкой
if self.current_state == "DISCHARGING":
self.send_status_report()
else:
self.stop_discharge_timer()
def _format_timedelta(self, td):
"""Преобразует timedelta в строку вида 'X ч Y мин'"""
total_seconds = int(td.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, _ = divmod(remainder, 60)
parts = []
if hours > 0:
parts.append(f"{hours} ч")
parts.append(f"{minutes} мин")
return " ".join(parts)
def send_status_report(self):
try:
# Получаем значения напрямую
power_raw = self.get_state(self.power_entity)
soc_raw = self.get_state(self.soc_entity)
current_raw = self.get_state(self.current_entity)
voltage_raw = self.get_state(self.voltage_entity)
# Преобразуем и округляем средствами Python
power = round(float(power_raw), 1) if power_raw not in (None, 'unknown', 'unavailable') else 0
soc = round(float(soc_raw), 1) if soc_raw not in (None, 'unknown', 'unavailable') else 0
current = round(float(current_raw), 2) if current_raw not in (None, 'unknown', 'unavailable') else 0
voltage = round(float(voltage_raw), 2) if voltage_raw not in (None, 'unknown', 'unavailable') else 0
# Определяем цвет/иконку для направления тока
direction = "🔻 Разряд" if power < 0 else "🔺 Заряд"
# Добавляем время без электричества, если известно
outage_info = ""
if self.outage_start_time:
duration = datetime.datetime.now() - self.outage_start_time
outage_info = f"\n⏳ Без света: <b>{self._format_timedelta(duration)}</b>"
msg = (
f"📊 <b>Статус батареи (JK BMS)</b>\n\n"
f"🔋 SOC: <b>{soc}%</b>\n"
f"⚡ Мощность: <b>{power} Вт</b> ({direction})\n"
f"🔌 Ток: <b>{current} А</b>\n"
f"🔋 Напряжение: <b>{voltage} В</b>"
f"{outage_info}\n"
f"🕒 Время: {datetime.datetime.now().strftime('%H:%M:%S')}"
)
self.send_telegram(msg, "html")
except Exception as e:
self.log(f"Ошибка при отправке статуса: {e}")
def send_telegram(self, message, parse_mode="html"):
try:
extra_data = {'parse_mode': parse_mode}
self.notify(message, name = self.notification_target, data=extra_data)
except Exception as e:
self.log(f"Ошибка отправки Telegram: {e}")
@@ -0,0 +1,14 @@
bms_monitor:
module: bms_monitor
class: BMSMonitor
# Telegram настройки
notification_target: telegram_monster
# Сущности из вашей карточки
power_sensor: sensor.jk_bms_jk_bms_power
soc_sensor: sensor.jk_bms_jk_bms_state_of_charge
current_sensor: sensor.jk_bms_jk_bms_current
voltage_sensor: sensor.jk_bms_jk_bms_total_voltage
# Порог мощности (Вт) для определения режима (чтобы избежать шумов)
power_threshold: 5
# Интервал оповещений при разрядке (секунды)
discharge_interval: 300
@@ -0,0 +1,75 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
#
# App to show dishwasher state
#
# Args:
#
# door_sensor = dishwasher door sensor
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Dishwasher(hass.Hass):
listen_event_handle_list = []
run_state = ""
def initialize(self):
if 'run_state' not in self.args or 'door_sensor' not in self.args or 'counter' not in self.args:
self.log("Please define all args")
return
self.listen_event_handle_list.append(self.listen_state(self.door_state_change, self.args['door_sensor'])) #, attribute='action'
self.listen_event_handle_list.append(self.listen_state(self.run_state_change, self.args['run_state'])) #, attribute='action'
def door_state_change(self, entity, attribute, old, new, kwargs):
if (new == "on"):
self.call_service("counter/increment", entity_id = self.args['counter'])
if (self.run_state == "finished"):
self.run_state = "collecting"
self.set_light_state_by_counter()
def run_state_change(self, entity, attribute, old, new, kwargs):
self.log("event: {}: {}->{}".format(attribute,old,new))
if (new == "Run"):
self.log('resetting counter')
self.call_service("counter/reset", entity_id = self.args['counter'])
self.run_state = "washing"
if (new == "Finished"):
self.log('resetting counter')
self.call_service("counter/reset", entity_id = self.args['counter'])
self.run_state = "finished"
self.log('Current state is: {}'.format(self.run_state))
self.set_light_state_by_counter()
def set_light_state_by_counter(self):
state = self.get_state(self.args['counter'])
open_count = int(state)
self.log('counter state={}, machine state = {}'.format(state, self.run_state))
if (self.run_state == "finished"):
# Есть посуда, которую нужно разобрать
# red color
self.log('Time to take dishes out.')
if 'lamp' in self.args:
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[255, 0, 0]) #red
elif (self.run_state == "washing"):
self.log('washing in progress')
self.call_service("light/turn_off", entity_id = self.args['lamp'])
elif (open_count > 10):
self.log('Time to run dishwasher.')
if 'lamp' in self.args:
self.call_service("light/turn_on", entity_id = self.args['lamp'], rgb_color=[245, 236, 0]) #yellow
else:
self.call_service("light/turn_off", entity_id = self.args['lamp'])
@@ -0,0 +1,13 @@
#################################
# TEETH BRUSHING REMINDER #
#################################
dishwasher:
class: Dishwasher
module: dishwasher
constraint: input_boolean.dishwasher_state_light
counter: counter.dishwasher_door_open_count
door_sensor: binary_sensor.hc_dishwasher_door
run_state: sensor.hc_dishwasher_state
lamp: light.espresense_kitchen_led_1
global_dependencies:
- globals
@@ -0,0 +1,28 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# App to send reports about unsuccessfull auths
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Security(hass.Hass):
def initialize(self):
self.log('---------------------')
self.listen_event_handle_list = []
self.listen_event_handle_list.append(self.listen_state(self.state_change, 'persistent_notification.http_login', attribute='message'))
def state_change(self, entity, attribute, old, new, kwargs):
notifier = self.args.get('notify', 'telegram')
self.notify(new, name = notifier)
self.log(new)
@@ -0,0 +1,10 @@
#################################
# INVALID AUTH REPORT #
#################################
invalid_auth_report:
class: Security
module: security
alice: media_player.yandex_station_m00r46000m53db
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,82 @@
import appdaemon.plugins.hass.hassapi as hass
from datetime import timedelta
import datetime
#
# App to send email report for devices running low on battery
#
# Args:
#
# threshold = value below which battery levels are reported and email is sent
# always_send = set to 1 to override threshold and force send
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Teeth(hass.Hass):
listen_event_handle_list = []
def initialize(self):
# Устанавливаем сброс в 10 утра
reset_time = datetime.time(10, 0, 0)
self.run_daily(self.reset_action, reset_time)
# Запускаем проверку напоминаний
self.timer = self.run_every(self.remind_if_needed, self.datetime()+timedelta(seconds=1), self.args['remind_interval'])
# Слушаем изменения состояния кнопки
self.listen_event_handle_list.append(self.listen_state(self.button_state_change, self.args['mute_button']))
def button_state_change(self, entity, attribute, old, new, kwargs):
self.log("event: {}={}".format(attribute,new))
if attribute == 'state' and new == 'single':
self.log('Muting notifications')
self.set_state(self.args['mute_boolean'], state='on')
self.say("Принято")
def say(self, command):
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=self.args['alice'], media_content_type='text', media_content_id=command)
def reset_action(self, kwargs):
self.log('Resetting mute state')
self.set_state(self.args['mute_boolean'], state='off')
def remind_if_needed(self, kwargs):
# Проверяем состояние input_boolean
if self.get_state(self.args['mute_boolean']) == 'on':
return
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
self.log('disabled by constraint')
return
home_sensors = []
if 'home_sensors' in self.args:
home_sensors = self.args['home_sensors']
at_home = False
is_any = False
for sensor in home_sensors:
is_any = True
state = self.get_state(sensor)
if state != 'not_home':
at_home = True
if not is_any:
at_home = True
if not at_home:
self.log('User is away, skipping notification')
return
notify = "telegram"
if "notify" in self.args:
notify = self.args["notify"]
now = datetime.datetime.now()+timedelta(hours=3)
message="Уже {}, а ты еще не почистил зубы. Самое время =)".format(now.strftime("%H:%M"))
self.notify(message, name = notify)
@@ -0,0 +1,15 @@
#################################
# TEETH BRUSHING REMINDER #
#################################
teeth_danil_brush_reminder:
class: Teeth
module: teeth_brushing
constraint: input_boolean.teeth_brushing_reminder
mute_boolean: input_boolean.teeth_muted
notify: telegram_monster
remind_interval: 1800
mute_button: sensor.button_shower_sink_action
home_sensors: [person.danil]
alice: media_player.yandex_station_ff98f029ab48468d28c557be
global_dependencies:
- globals
@@ -0,0 +1,109 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
import globals
#
# App to send notifications when counters state needs attention
#
# Args:
#
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class WaterCountersAlarm(hass.Hass):
checkup_threasold = 31
send_threasold = 31
def initialize(self):
if not globals.check_args(self, ['checkup_threasold', 'send_threasold']):
return
self.checkup_threasold = self.args['checkup_threasold']
self.send_threasold = self.args['send_threasold']
self.check_send_dates({"force": 1})
self.run_daily(self.check_checkup_dates, datetime.time(10, 0, 0))
self.run_daily(self.check_send_dates, datetime.time(10, 1, 0))
#self.check_checkup_dates([])
def check_send_dates(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
devices = self.get_state()
if devices == None:
return
values = {}
send_devices = []
for deviceName in devices:
device = devices[deviceName]
if (device == None):
continue
send_date = None
if "attributes" in device:
if "date" in device["attributes"]:
send_date = device["attributes"]["date"]
if send_date != None:
self.log('found device {} with send date {}'.format(deviceName, send_date))
if send_date == "":
send_devices.append(deviceName)
else:
send_date_date = None
if '-' in send_date:
send_date_date = datetime.datetime.strptime(send_date, '%Y-%m-%d')
else:
send_date_date = datetime.datetime.strptime(send_date, '%d.%m.%Y')
if send_date_date < (datetime.datetime.now() - datetime.timedelta(days=self.send_threasold)):
send_devices.append(deviceName)
values[deviceName] = send_date
message=""
if send_devices:
message += "ВНИМАНИЕ! У вас есть счетчики, показания по которым давно не передавались:\n"
for device in send_devices:
message += "- {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if send_devices or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
def check_checkup_dates(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
entities = self.get_state()
if entities == None:
return
values = {}
checkup_devices = []
for entity_id in entities:
entity = entities[entity_id]
if entity == None:
# self.log('not found {} in entities list.'.format(entity_id))
continue
checkup = None
if "attributes" in entity:
if "checkup" in entity["attributes"]:
checkup = entity["attributes"]["checkup"]
if checkup != None:
self.log('found device {} with checkup date {}'.format(entity_id, checkup))
checkup_date = datetime.datetime.strptime(checkup, '%Y-%m-%d')
if (checkup_date - datetime.timedelta(days=self.checkup_threasold)) < datetime.datetime.now():
checkup_devices.append(entity_id)
values[entity_id] = checkup
message=""
if checkup_devices:
message += "ВНИМАНИЕ! У вас есть счетчик, нуждающийся в поверке:\n"
for device in checkup_devices:
message += "- {}: {}\n".format(self.friendly_name(device), values[device])
message += "\n\n"
if checkup_devices or ("always_send" in self.args and self.args["always_send"] == 1) or ("force" in kwargs and kwargs["force"] == 1):
if message != "":
self.notify(message, name = "telegram")
@@ -0,0 +1,12 @@
#################################
# WATER COUNTERS ALARM #
#################################
water_counters:
class: WaterCountersAlarm
module: water_counters
constraint: input_boolean.water_counters_report
notify: telegram
checkup_threasold: 31
send_threasold: 31
global_dependencies:
- globals
+63
View File
@@ -0,0 +1,63 @@
import appdaemon.plugins.hass.hassapi as hass
#
# Notification system
#
# Args:
#
# Release Notes
#
# Version 1.0:
# Initial Version
class BaseNotifyTarget():
def notify(self, hass, message, person='*'):
hass.log('notifying person {} with message: {}'.format(person, message))
class AliceNotifyTarget(BaseNotifyTarget):
alice = 'media_player.yandex_station'
def notify(self, hass, message, person='*'):
msg = message
if person == 'danil':
msg = "Данил, " + msg
elif person == 'sveta':
msg = "Света, " + msg
hass.log('notifying over alice person {} with message: {}'.format(target, message))
self.say(hass, msg)
def say(self, hass, message):
hass.log('message = {}'.format(message))
hass.call_service('media_player/play_media', entity_id=self.alice, media_content_type='text', media_content_id=message)
class TelegramNotifyTarget(BaseNotifyTarget):
target_default = 'telegram'
target_sveta = 'telegram_sveta'
target_danil = 'telegram_monster'
def notify(self, hass, message, person='*'):
target = self.target_default
if person == 'danil':
target = self.target_danil
elif person == 'sveta':
target = self.target_sveta
hass.log('notifying over telegram person {} with message: {}'.format(target, message))
hass.notify(message, name=target)
class Notification(hass.Hass):
home_targets = [AliceNotifyTarget()]
away_targets = [TelegramNotifyTarget()]
def initialize(self):
self.log('Init')
def notify_person(self, message, target='*', repeat = 1, delay = 60, onEvent = '', at=None):
notifyTarget = None
away_mode = True
if (away_mode):
notifyTarget = self.away_targets[0]
else:
notifyTarget = self.home_targets[0]
self.log('message: {}'.format(message))
notifyTarget.notify(self, message, person=target)
+3
View File
@@ -0,0 +1,3 @@
notify:
module: notify
class: Notification
@@ -0,0 +1,63 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
from datetime import timedelta
#
# POwer theory
# Power theory tariff switch
#
# Args:
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class PowerTheory(hass.Hass):
def initialize(self):
self.run_daily(self.timer_tick, self.parse_time("07:00:00"))
self.run_daily(self.timer_tick, self.parse_time("10:00:00"))
self.run_daily(self.timer_tick, self.parse_time("17:00:00"))
self.run_daily(self.timer_tick, self.parse_time("21:00:00"))
self.run_daily(self.timer_tick, self.parse_time("23:00:00"))
self.timer_tick(None)
def timer_tick(self, kwargs):
t1 = self.time_is_between(self.datetime(), '07:00:00', '09:59:59') or self.time_is_between(self.datetime(), '17:00:00', '20:59:59')
t2 = self.time_is_between(self.datetime(), '23:00:00', '06:59:00')
t3 = self.time_is_between(self.datetime(), '10:00:00', '16:59:59') or self.time_is_between(self.datetime(), '21:00:00', '22:59:59')
if t1:
self.switch_to_tariff('t1')
if t2:
self.switch_to_tariff('t2')
if t3:
self.switch_to_tariff('t3')
def switch_to_tariff(self, tariff):
self.log('switching to power tariff: {}'.format(tariff))
self.call_service("select/select_option", entity_id='select.power_theory', option=tariff)
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -0,0 +1,6 @@
power_theory:
module: power_theory
class: PowerTheory
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: sensor.presence_home
ha_panel: alarm_control_panel.ha_alarm
constraint: input_boolean.presence_control
global_dependencies:
- globals
@@ -0,0 +1,77 @@
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):
extended_timeout = 5*60
timers = []
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'])
extended_timeout = self.args.get('extended_timeout', self.extended_timeout)
self.log('extended_timeout: {}'.format(self.extended_timeout))
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')
if 'presence_extended' in self.args:
self.fire_event('presence_extended_just_left')
self.set_mode('Just Left')
self.cancel_current_timer()
self.timers.append(self.run_in(self.run_in_set_mode, self.extended_timeout, mode='Away'))
def return_home_mode(self):
self.log('return_home_mode')
self.fire_event('return_home_mode')
if 'presence_extended' in self.args:
self.set_mode('Just Arrived')
self.fire_event('presence_extended_just_arrived')
self.cancel_current_timer()
self.timers.append(self.run_in(self.run_in_set_mode, self.extended_timeout, mode='Home'))
def set_mode(self, mode):
if 'presence_extended' not in self.args:
return
entity_id = self.args['presence_extended']
self.log('setting mode direct: {}={}'.format(entity_id, mode))
self.call_service("input_select/select_option", entity_id = entity_id, option = mode)
def run_in_set_mode(self, kwargs):
if not 'mode' in kwargs:
return
mode = kwargs['mode']
entity_id = self.args['presence_extended']
event = 'presence_extended_{}'.format(mode.lower())
self.log('firing event: {}'.format(event))
self.fire_event(event)
self.log('setting mode by timer: {}={}'.format(entity_id, mode))
self.call_service("input_select/select_option", entity_id = entity_id, option = mode)
def cancel_current_timer(self):
for timer in self.timers:
self.log('canceling timer')
self.cancel_timer(timer)
self.timers = []
@@ -0,0 +1,11 @@
#################################
# HOME PRESENSE #
#################################
alarm_panel_action:
module: alarm_panel_action
class: AlarmPanelAction
ha_panel: alarm_control_panel.ha_alarm
presence_extended: input_select.presence_extended
extended_timeout: 300
global_dependencies:
- globals
@@ -0,0 +1,80 @@
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):
mode = None
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")
self.determine_current_mode()
self.set_thermostat_by_mode(None)
self.timer = self.run_every(self.set_thermostat_by_mode, 'now', 5*60)
def determine_current_mode(self):
state = self.get_state('input_select.presence_extended')
if state == 'Home' or state == 'Just Arrived':
self.mode = 'home'
else:
self.mode = 'away'
def away_mode(self, event_id, event_args, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
self.mode = 'away'
for entity_id in self.args["away_entities"]:
self.turn_off_custom(entity_id)
self.set_thermostat_by_mode(None)
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.mode = 'home'
entities = self.args["return_entities"]
if entities == None:
entities=[]
for entity_id in entities:
self.turn_on_custom(entity_id)
self.set_thermostat_by_mode(None)
def set_thermostat_by_mode(self, kwargs):
if "thremostats" not in self.args:
return
preset = 'away'
if self.mode == 'home':
preset = 'manual'
for entity_id in self.args["thremostats"]:
state = self.get_state(entity_id, 'preset_mode')
if state != preset:
self.log('Set thermostat ({}), current state = {}, set mode to {}'.format(entity_id, state, preset))
self.call_service("climate/set_preset_mode", entity_id=entity_id, preset_mode = preset)
def turn_on_custom(self, entity_id):
if 'cover.' in entity_id:
self.call_service("cover/open_cover", entity_id=entity_id)
return
if 'climate.' in entity_id:
self.call_service("climate/turn_on", entity_id=entity_id)
return
self.turn_on(entity_id)
def turn_off_custom(self, entity_id):
if 'cover.' in entity_id:
self.call_service("cover/close_cover", entity_id=entity_id)
return
if 'climate.' in entity_id:
self.call_service("climate/turn_off", entity_id=entity_id)
return
self.turn_off(entity_id)
@@ -0,0 +1,82 @@
#################################
# HOME PRESENSE #
#################################
entities_action:
module: entities_action
class: EntitiesByPresence
away_entities:
# air conditioners
- climate.ac_bedroom
- climate.ac_childroom
- climate.ac_kitchen
- climate.ac_mainroom
# din rail lights
- light.light_mainroom_mainlamp
- light.light_mainroom_spotlights
- light.light_mainroom_pc_spotlight
# balcony
- light.light_balcony_spotlight
# bedroom
- light.light_bedroom_spotlights
- light.light_bedroom_nightlight_left
- light.light_bedroom_nightlight_right
- light.light_bedroom_pc_spotlight
- light.light_bedroom_mainlight
- light.light_bedroom_pc_nightlight
# bath
- light.light_bath
- light.light_bath_shelf
# shower
- light.light_shower_main_spotlights
- light.light_shower_shower_spotlights
- light.light_shower_shelf_light
# hall
- light.light_hall_long
# childroom
- light.light_childroom_main
- light.light_childroom_spotlights
- light.light_childroom_bed
# kitchen
- light.light_kitchen_spotlights
- light.light_kitchen_table_lamp
- light.light_kitchen_bar_lamp
- light.light_kitchen_set
# ventilation
- fan.ventilation_shower
- fan.ventilation_bath
# bedside lamps
- light.danil_bedside_lamp_rgbww_light
- light.svetlana_bedside_lamp_rgbww_light
# mirrors and heaters
- switch.shower_mirror
- switch.switch_mirror_shower_heater
- switch.light_bath_mirror
- switch.switch_mirror_bath_heater
# media
- media_player.bedroom_tv
- media_player.kitchen_tv
- media_player.mainroom_tv
# alice
- media_player.yandex_station_m00r46000m53db
- media_player.yandex_station_m00fhe2003seqr
- media_player.yandex_station_ly00000000000008674600007209d3f5
- media_player.yandex_station_ff98f029ab48468d28c557be
- media_player.yandex_station_u003jc100phn9k
return_entities:
# - light.light_kitchen_set
thremostats:
- climate.radiator_childroom
- climate.radiator_mainroom
- climate.radiator_kitchen
- climate.radiator_bedroom
- climate.radiator_bedroom_balcony
global_dependencies:
- globals
@@ -0,0 +1,9 @@
purifier_timer:
module: purifier
class: Purifier
on_time: "06:45:00"
off_time: "23:00:00"
entity: fan.xiaomi_miio_device
ha_panel: alarm_control_panel.ha_alarm
global_dependencies:
- globals
@@ -0,0 +1,107 @@
import appdaemon.plugins.hass.hassapi as hass
import globals
import datetime
#
# Purifier controller
# Turn on and off purifier by light sensor and timers
#
# Args:
#
# None
#
# Release Notes
#
# Version 1.0:
# Initial Version
class Purifier(hass.Hass):
away_fan_speed = 8 #1-14
listen_event_handle_list = []
mode = 'auto'
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.day_mode_timer_event, on_time)
self.off_timer = self.run_daily(self.night_mode_timer_event, off_time)
self.listen_event_handle_list.append(self.listen_event(self.away_mode_event, "away_mode"))
self.listen_event_handle_list.append(self.listen_event(self.return_home_mode_event, "return_home_mode"))
self.listen_event_handle_list.append(self.listen_state(self.state_change, self.args['entity'], attribute='illuminance'))
def state_change(self, entity, attribute, old, new, kwargs):
presence_state = self.get_state(self.args['ha_panel'])
if presence_state != "disarmed":
# self.log('Nobody home, ignoring illuminance changes.')
return
self.log("{} changed from {} to {}".format(attribute, old, new))
if old > 0 and new == 0 and self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
self.night_mode()
elif old == 0 and new > 0 and self.time_is_between(self.datetime(), self.args["on_time"], self.args["off_time"]):
self.auto_mode()
#self.auto_mode()
def return_home_mode_event(self, event_id, event_args, kwargs):
self.auto_mode()
def away_mode_event(self, event_id, event_args, kwargs):
self.away_mode()
def night_mode_timer_event(self, kwargs):
self.night_mode()
def day_mode_timer_event(self, kwargs):
self.auto_mode()
def away_mode(self):
self.mode='away'
self.log('Set purifier mode to favorite, speed: {}'.format(self.away_fan_speed))
if 'entity' in self.args:
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='favorite')
self.call_service("fan/xiaomi_miio_set_favorite_level", entity_id = self.args['entity'], level=self.away_fan_speed)
self.call_service("fan/xiaomi_miio_set_led_on", entity_id = self.args['entity'])
# self.call_service("fan/xiaomi_miio_set_buzzer_on", entity_id = self.args['entity'])
def auto_mode(self):
self.mode='auto'
self.log('Set purifier mode to auto.')
if 'entity' in self.args:
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='auto')
self.call_service("fan/xiaomi_miio_set_led_on", entity_id = self.args['entity'])
# self.call_service("fan/xiaomi_miio_set_buzzer_on", entity_id = self.args['entity'])
def night_mode(self):
self.mode='night'
self.log('Set purifier mode to silent.')
if 'entity' in self.args:
self.call_service("fan/xiaomi_miio_set_buzzer_off", entity_id = self.args['entity'])
self.call_service("fan/set_speed", entity_id = self.args['entity'], speed='silent')
self.call_service("fan/xiaomi_miio_set_led_off", entity_id = self.args['entity'])
def time_is_between(
hass, target_dt: datetime, start_time: str,
end_time: str) -> bool:
"""Generalization of AppDaemon's now_is_between method."""
start_time_dt = hass.parse_time(start_time) # type: datetime
end_time_dt = hass.parse_time(end_time) # type: datetime
start_dt = target_dt.replace(
hour=start_time_dt.hour,
minute=start_time_dt.minute,
second=start_time_dt.second)
end_dt = target_dt.replace(
hour=end_time_dt.hour,
minute=end_time_dt.minute,
second=end_time_dt.second)
if end_dt < start_dt:
# Spans midnight
if target_dt < start_dt and target_dt < end_dt:
target_dt = target_dt + timedelta(days=1)
end_dt = end_dt + timedelta(days=1)
return start_dt <= target_dt <= end_dt
@@ -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,136 @@
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)
tags_by_person = self.get_tags_state_by_person(self.tags, self.constraint_list)
homePresence = 'off'
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')
homePresence = 'on'
else:
self.create_sensor(sensor, 'off')
for person in tags_by_person:
if len(tags_by_person[person])>0 and tags_by_person[person] != 'not home':
attr = self.get_state(person, attribute='all').get('attributes', None)
# self.log(attr)
# attr['state'] = tags_by_person[person][0]
# disable set person state
# self.set_state(person, state = tags_by_person[person][0], attributes = attr)
homeSensor = '{}{}'.format(self.args['sensor_prefix'],'home')
self.create_sensor(homeSensor, homePresence)
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 get_tags_state_by_person(self, tags, constraint_list):
tags_by_person = {}
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
tagName = tag.replace(self.args['tag_prefix'],'')
if '_' in tagName:
tagName = tagName.split('_')[0]
person = '{}{}'.format(self.args['person_prefix'], tagName)
person_state = self.get_state(person)
if person_state != None:
# self.log('{}: {}'.format(person, person_state))
if person not in tags_by_person:
tags_by_person[person] = []
tags_by_person[person].append(self.get_state(tag))
return tags_by_person
# if person_state in self.get_state('person'):
# self.log('{} found'.format(person))
# else:
# self.log('{} not found'.format(person))
# self.log('{}: {}'.format(person, person_state))
# tag_state = 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,16 @@
#################################
# HOME PRESENSE #
#################################
tags_presence:
module: tags_presence
class: TagsPresence
tag_prefix: sensor.tag_
sensor_prefix: sensor.presence_
constraint_prefix: input_boolean.tag_
person_prefix: person.
rooms:
- kladovka
- bedroom
- mainroom
global_dependencies:
- globals
@@ -0,0 +1,107 @@
import appdaemon.plugins.hass.hassapi as hass
import datetime
#
# 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
current_wait_cycle = 0
wait_cycles = 2
timer = None
listen_event_handle_list = []
timers = []
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_handle_list.append(self.listen_event(self.away_mode, "away_mode"))
self.listen_event_handle_list.append(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()
self.log('away entities: {}'.format(powered_on))
if (len(powered_on) > 0):
device_names = ""
for device in powered_on:
if device_names != '':
device_names += ", "
name = self.friendly_name(device)
if name is None:
name = device
device_names += name
self.notify('В доме остались требующие воды устройства ({}), подача воды не будет отключена.'.format(device_names), name = self.args['notify'])
self.cancel_current_timer()
self.timers.append(self.run_every(self.wait_when_device_is_done, self.datetime()+datetime.timedelta(seconds=5), 5*60))
else:
self.notify('Отключаю подачу воды.', name = self.args['notify'])
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) == 1:
self.current_wait_cycle = 0
elif len(powered_on) == 0 and self.current_wait_cycle<self.wait_cycles:
self.log("Current wait cycle: {}".format(self.current_wait_cycle))
self.current_wait_cycle = self.current_wait_cycle+1
elif len(powered_on) == 0 and self.current_wait_cycle>=self.wait_cycles:
self.current_wait_cycle = 0
self.notify('Устройства требующие воды закончили свою работу, отключаю подачу воды.', name = self.args['notify'])
self.turn_off_water_devices()
self.cancel_current_timer()
def turn_off_water_devices(self):
self.log("Turning off water devices.")
self.turn_off(self.args['water_valve'])
for device in self.args['water_devices']:
if 'switch' in device:
self.log('switching off: {}'.format(device))
self.turn_off(device)
def get_turned_on_devices(self):
powered_on_devices = []
devices = self.args['water_devices']
for device in devices:
state = self.get_state(device)
self.log('{} state: {}'.format(device, state))
if state == 'off' or state == None:
continue
powered_on_devices.append(device)
return powered_on_devices
def cancel_current_timer(self):
for timer in self.timers:
self.log('canceling timer')
self.cancel_timer(timer)
self.timers = []
@@ -0,0 +1,14 @@
#################################
# HOME PRESENSE #
#################################
water_action:
module: water_action
class: WaterValveControl
water_valve: switch.watercontrol_valves
notify: telegram
constraint: input_boolean.water_control
water_devices:
- binary_sensor.washer_running # стиралка
- binary_sensor.hc_dishwasher_running # посудомойка
global_dependencies:
- globals
+265
View File
@@ -0,0 +1,265 @@
import appdaemon.plugins.hass.hassapi as hass
import requests
from datetime import datetime
class TelegramInternetControl(hass.Hass):
"""
Управление интернетом через Telegram команды.
Поддерживает множественные MAC-адреса.
"""
def initialize(self):
"""
Инициализация приложения.
"""
self.log("Telegram Internet Control initialized")
# Конфигурация из apps.yaml
self.api_url = self.args.get("api_url", "http://localhost:8000/run")
self.api_user = self.args.get("api_user", "api_user")
self.api_password = self.args.get("api_password", "strong_password")
# Команды Telegram
self.block_command = self.args.get("block_command", "Коля наказан").lower()
self.unblock_command = self.args.get("unblock_command", "Включи Коле интернет").lower()
# MAC адреса для управления (массив)
self.target_macs = self.args.get("target_macs", [])
if isinstance(self.target_macs, str):
self.target_macs = [self.target_macs]
# Разрешенные user_id (безопасность)
self.allowed_users = self.args.get("allowed_users", [])
if isinstance(self.allowed_users, int):
self.allowed_users = [self.allowed_users]
# Entity для прослушивания событий Telegram
self.telegram_event_entity = self.args.get(
"telegram_event_entity",
"event.home_assistant_fonvizina_update_event"
)
# Сервис для отправки ответов в Telegram
self.notify_service = self.args.get("notify_service", "notify/telegram")
# Сохраняем последнее обработанное время (для пропуска дублей)
self.last_processed_time = None
# Слушаем изменения состояния события Telegram
self.listen_state(
self.on_telegram_event,
self.telegram_event_entity
)
self.log(f"Listening to: {self.telegram_event_entity}")
self.log(f"Block command: '{self.block_command}'")
self.log(f"Unblock command: '{self.unblock_command}'")
self.log(f"Target MACs: {self.target_macs}")
self.log(f"Allowed users: {self.allowed_users}")
self.listen_event(self.yandex_intent, "yandex_intent")
def yandex_intent(self, event_id, event_args, kwargs):
self.log("{} {}".format(event_id, event_args))
text = event_args['text']
room = event_args['room']
alice = event_args['entity_id']
if text == self.block_command:
self.handle_block_command('Monster1025')
def say(self, entity_id, command):
self.log('command = {}'.format(command))
self.call_service('media_player/play_media', entity_id=entity_id, media_content_type='text', media_content_id=command)
def on_telegram_event(self, entity, attribute, old, new, kwargs):
"""
Обработка события Telegram.
"""
try:
# Получаем полный объект состояния с атрибутами
state_data = self.get_state(entity, attribute="all")
if state_data is None:
self.log("State data is None")
return
# Получаем атрибуты
attributes = state_data.get("attributes", {})
if not attributes:
self.log("No attributes in state data")
return
# Проверяем тип события (должно быть telegram_text)
event_type = attributes.get("event_type", "")
if event_type != "telegram_text":
return
# Получаем текст сообщения
text = attributes.get("text", "").strip()
if not text:
return
# Получаем user_id отправителя
user_id = attributes.get("user_id")
from_first = attributes.get("from_first", "Unknown")
# Получаем время события для защиты от дублей
event_time = attributes.get("date", "")
# Пропускаем если уже обработали это событие
if event_time == self.last_processed_time:
self.log(f"Skipping duplicate event: {event_time}")
return
self.last_processed_time = event_time
self.log(f"Telegram message from {from_first} (user_id: {user_id}): '{text}'")
# Проверяем, разрешен ли пользователь
if user_id not in self.allowed_users:
self.log(f"User {user_id} not in allowed list. Ignoring.")
return
# Обрабатываем команды (регистронезависимо)
text_lower = text.lower()
if text_lower == self.block_command:
self.handle_block_command(from_first)
elif text_lower == self.unblock_command:
self.handle_unblock_command(from_first)
except Exception as e:
self.log(f"Error processing Telegram event: {str(e)}")
self.log(f"State data: {state_data if 'state_data' in locals() else 'N/A'}")
def handle_block_command(self, user_name):
"""
Блокировка интернета для всех MAC-адресов.
"""
self.log(f"Blocking internet for {user_name}")
if not self.target_macs:
self.send_telegram_message(f"❌ {user_name}, не настроены MAC-адреса для блокировки!")
self.log("No MAC addresses configured")
return
success_count = 0
failed_macs = []
for mac in self.target_macs:
# Формируем команду для роутера
command = f"ip hotspot host {mac} schedule schedule1"
self.log(f"Sending command: {command}")
if self.send_to_router(command):
success_count += 1
else:
failed_macs.append(mac)
command = "system configuration save"
self.log(f"Sending command: {command}")
cmd = self.send_to_router(command)
# Формируем отчет
if success_count == len(self.target_macs):
self.send_telegram_message(
f"✅ {user_name}, интернет заблокирован для {success_count} устр-в!"
)
self.log(f"Internet blocked successfully for {success_count} devices")
elif success_count > 0:
self.send_telegram_message(
f"⚠️ {user_name}, частично заблокировано: {success_count}/{len(self.target_macs)}. "
f"Не удалось: {', '.join(failed_macs)}"
)
self.log(f"Partial block: {success_count}/{len(self.target_macs)}")
else:
self.send_telegram_message(f"❌ {user_name}, ошибка при блокировке интернета!")
self.log("Failed to block internet")
def handle_unblock_command(self, user_name):
"""
Разблокировка интернета для всех MAC-адресов.
"""
self.log(f"Unblocking internet for {user_name}")
if not self.target_macs:
self.send_telegram_message(f"❌ {user_name}, не настроены MAC-адреса для разблокировки!")
self.log("No MAC addresses configured")
return
success_count = 0
failed_macs = []
for mac in self.target_macs:
# Формируем команду для роутера
command = f"ip hotspot host {mac} schedule schedule0"
self.log(f"Sending command: {command}")
cmd = self.send_to_router(command)
if cmd:
success_count += 1
else:
failed_macs.append(mac)
command = "system configuration save"
self.log(f"Sending command: {command}")
cmd = self.send_to_router(command)
# Формируем отчет
if success_count == len(self.target_macs):
self.send_telegram_message(
f"✅ {user_name}, интернет включен для {success_count} устр-в!"
)
self.log(f"Internet unblocked successfully for {success_count} devices")
elif success_count > 0:
self.send_telegram_message(
f"⚠️ {user_name}, частично включено: {success_count}/{len(self.target_macs)}. "
f"Не удалось: {', '.join(failed_macs)}"
)
self.log(f"Partial unblock: {success_count}/{len(self.target_macs)}")
else:
self.send_telegram_message(f"❌ {user_name}, ошибка при включении интернета!")
self.log("Failed to unblock internet")
def send_to_router(self, command):
"""
Отправка команды на роутер через API.
"""
try:
response = requests.post(
self.api_url,
json={"command": command},
auth=(self.api_user, self.api_password),
timeout=10
)
if response.status_code == 200:
result = response.json()
exit_code = result.get("exit_code", -1)
if exit_code == 0:
return True
else:
stderr = result.get('stderr', '')
self.log(f"Router command failed with exit code: {exit_code}")
self.log(f"Stderr: {stderr}")
return False
else:
self.log(f"API request failed with status: {response.status_code}")
self.log(f"Response: {response.text}")
return False
except Exception as e:
self.log(f"Error sending command to router: {str(e)}")
return False
def send_telegram_message(self, message):
"""
Отправка ответа в Telegram.
"""
try:
self.call_service(
self.notify_service,
message=message
)
self.log(f"Telegram message sent: {message}")
except Exception as e:
self.log(f"Error sending Telegram message: {str(e)}")
@@ -0,0 +1,27 @@
keenetic:
module: keenetic
class: TelegramInternetControl
# URL вашего API (адрес контейнера с API)
api_url: "http://192.168.1.6:8001/run"
api_user: !secret router_username
api_password: !secret router_password
# Команды Telegram (регистронезависимые)
block_command: "Коля наказан"
unblock_command: "Включи Коле интернет"
# MAC адреса для управления (массив)
target_macs:
- "cc:4b:73:04:ed:34"
- "b8:41:d9:d8:3f:58"
# Разрешенные user_id из Telegram (безопасность!)
allowed_users:
- 123622180
# Entity для прослушивания событий
telegram_event_entity: "event.home_assistant_fonvizina_update_event"
# Сервис для отправки уведомлений в Telegram
notify_service: "notify/telegram"
@@ -0,0 +1,48 @@
import appdaemon.plugins.hass.hassapi as hass
import fnmatch
#
# Fire 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 FireSensorsReport(hass.Hass):
def initialize(self):
if 'sensor_parts' not in self.args or 'notify' not in self.args:
self.error("Please provide sensor_parts, ha_panel and notify in config!")
return
sensor_parts = self.args['sensor_parts']
all_sensors = self.get_state()
for entity_id in all_sensors:
for part in sensor_parts:
if fnmatch.fnmatch(entity_id, part):
self.log(' [+] listening for security sensor {}'.format(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":
self.log('Fire sensor is triggered!!!')
self.alarm_triggered(entity, old, new)
def alarm_triggered(self, entity, old, new):
self.log('alarm triggered')
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 @@
fire_sensors:
module: fire_sensors
class: FireSensorsReport
sensor_parts:
- binary_sensor.smoke_detector_*_smoke
constraint: input_boolean.fire_sensors_report
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,59 @@
import appdaemon.plugins.hass.hassapi as hass
import fnmatch
#
# 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_parts' not in self.args or 'ha_panel' not in self.args or 'notify' not in self.args:
self.error("Please provide sensor_parts, ha_panel and notify in config!")
return
sensor_parts = self.args['sensor_parts']
all_sensors = self.get_state()
for entity_id in all_sensors:
for part in sensor_parts:
if fnmatch.fnmatch(entity_id, part):
self.log(' [+] listening for security sensor {}'.format(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
vacuum_state = "docked"
if "vacuum" in self.args:
vacuum_state = self.get_state(self.args["vacuum"])
#Если работает пылесос и сработал датчик движения
if vacuum_state == 'cleaning' and 'binary_sensor.motion' in entity:
self.log('Motion sensor is triggered, but vacuum is cleaning.')
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,14 @@
security_sensors:
module: security_sensors
class: SecuritySensorsReport
sensor_parts:
- binary_sensor.motion_*_occupancy
- binary_sensor.door_*_contact
- binary_sensor.smoke_detector_*_smoke
- binary_sensor.water_leak_*_leak
constraint: input_boolean.security_sensors_report
ha_panel: alarm_control_panel.ha_alarm
vacuum: vacuum.dreame_p2027_6b1e_robot_cleaner
notify: telegram
global_dependencies:
- globals
@@ -0,0 +1,62 @@
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 = ''
sensors = []
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.sensors = self.get_water_leak_sensors()
self.log("leak sensors: {}".format(self.sensors))
for sensor in self.sensors:
self.listen_state(self.water_leak, sensor)
def get_water_leak_sensors(self):
sensors = []
devices = self.get_state()
for sensor in devices:
if not sensor.startswith(self.args['sensor_prefix']):
continue
sensors.append(sensor)
return sensors
def water_leak(self, entity, attribute, old, new, kwargs):
if new == "on":
self.send_alarm(entity)
def send_alarm(self, entity_id):
self.log('water leak alarm triggered!')
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/watercontrol/bath/valve/set', payload = 'close')
message = "ВНИМАНИЕ! В ваше отстутствие сработал датчик протечки воды ({})!!! Выключаю подачу воды.".format(self.friendly_name(entity_id))
self.notify(message, name = self.args['notify'])
message = "Состояние датчиков протечки:\n"
for entity_id in self.sensors:
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,14 @@
#################################
# WATER LEAK #
#################################
water_leak:
module: water_leak
class: WaterLeak
constraint: input_boolean.water_leak_control
sensor: group.water_leak_sensors
valve: switch.water_valve
sensor_prefix: binary_sensor.water_leak_
notify: telegram
gw_mac: !secret aqara1_mac
global_dependencies:
- globals
+370
View File
@@ -0,0 +1,370 @@
import sys
sys.path.insert(0, '/emp_mos_api')
import globals
import datetime as dt
from automation import Automation, Base
from datetime import datetime, tzinfo, timedelta
from emp_mos_api.mos import MosAPI, Watercounter, Water, EmpServerException
#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):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
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 PowerCounterSender(Automation):
timezone = '+03:00'
def initialize(self):
super().initialize()
if 'power_total' not in self.entity_ids:
self.log('Please specify power_total for PowerCounterSender.')
return
self.run_daily(self.send_mosru_counters, dt.time(14, 00, 0))
# self.send_mosru_counters("")
def send_mosru_counters(self, kwargs):
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
today = datetime.today()
if today.day < 15 or today.day > 26:
self.log("We can send power values between days 15 and 26.")
return
power_total = round(float(self.get_state(self.entity_ids['power_total'])))
period = datetime.now().strftime('%Y-%m-%d')
if 'threshold' in self.args and 'power_mosenergosbyt_total' in self.args:
power_mosenergosbyt_total = float(self.get_state(self.args['power_mosenergosbyt_total']))
threshold = self.args['threshold']
diff = power_total-power_mosenergosbyt_total
if diff<threshold:
self.log('power_total: {}, power_mosenergosbyt_total: {}, diff: {}, threshold: {}'.format(power_total, power_mosenergosbyt_total, diff, threshold))
self.log('skipping send')
return
self.log('Starting sending power counters value ({}) to mosru.'.format(power_total))
new_values = [{'counter_id': 'T1', 'indication': power_total, 'period': period}]
try:
result = self.app.send_electrocounters(new_values)
self.log(result)
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()+timedelta(seconds=10), 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 PowerCounterSensor(Automation):
timezone = '+03:00'
def initialize(self):
super().initialize()
self.timer = self.run_every(self.update_sensors, self.datetime()+timedelta(seconds=30), 1*60*60)
def update_sensors(self, args) -> None:
self.log('Updating power counters')
self.app.update_power()
self.log('power data: {}'.format(self.app._power_counters))
if 'power_total' in self.entity_ids:
power_total = self.app.power_total
self.create_power_entity(self.entity_ids['power_total'], power_total, 'kWh', 'Счетчик')
if 'power_balance' in self.entity_ids:
power_balance = self.app.power_balance
self.create_power_entity(self.entity_ids['power_balance'], power_balance, '₽', 'Баланс')
def create_power_entity(self, entity_id, value, unit, description):
icon = "mdi:av-timer"
attributes = {}
attributes['unit_of_measurement'] = unit
attributes['icon'] = icon
attributes['friendly_name'] = "{}(mos.ru)".format(description)
attributes['description'] = 'Created and updated from appdaemon ({})'.format(__name__)
self.set_state(entity_id, state = value, attributes = attributes)
class EpdBalanceSensor(Automation):
def initialize(self):
super().initialize()
self.timer = self.run_every(self.update_sensors, self.datetime()+timedelta(seconds=5), 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']
friendly_name = self.args.get('friendly_name', 'Баланс (ЕПД)')
attributes = {
'unit_of_measurement': '₽',
'icon': 'mdi:bank-transfer',
'date': datetime.now().strftime('%d.%m.%Y'),
'unpaid': epd_description,
'friendly_name': friendly_name,
'description': 'Created and updated from appdaemon ({})'.format(__name__)
}
self.log('setting state: {}, state = {}, attributes = {}'.format(entity_id, epd_balance, attributes))
self.set_state(entity_id, state = str(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', 'session_id']):
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):
self._api.set_session(self.properties['session_id'])
# 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)
def send_electrocounters(self, new_values) -> None:
self._check_login()
self._api.send_electrocounters(self._flat_id, new_values)
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']
self.log(self._water_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['payment_status'] == '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['payment_status'] == 'PAID'
if epd_is_paid:
continue
balance = balance - float(epd['amount'].replace(',','.'))
return balance
def update_epd(self) -> None:
self._check_login()
data = {}
dateEnd = datetime.now().date()
dateStart = dateEnd - timedelta(6*365/12)
date_start = dateStart.strftime('%d.%m.%Y')
date_end = dateEnd.strftime('%d.%m.%Y')
epds = self._api.get_epd(self._flat_id, date_start, date_end)
for i in range(0, 3): #retries
if epds != []:
break
epds = self._api.get_epd(self._flat_id, date_start, date_end)
if len(epds) == 0:
self.log('epd not found for date: {}'.format(date_str))
return
for epd in epds:
if epd['epd_type'] == 'DEBT':
continue
date_str = epd['period']
data[date_str] = epd
# epd_total = epd['amount']
# epd_is_paid = epd['is_paid']
# self.log(" - Дата: {}, данные: {}.".format(date_str, epd))
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))
+205
View File
@@ -0,0 +1,205 @@
#################################
# 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
session_id: !secret mosru_session
global_dependencies:
- globals
epd_sensor:
class: EpdBalanceSensor
module: mosru
friendly_name: Баланс (ЕПД) Алтуфьево
entity_ids:
epd_balance: sensor.epd_balance
app: mosru_client
dependencies:
- mosru_client
global_dependencies:
- globals
power_sensor:
class: PowerCounterSensor
module: mosru
entity_ids:
power_balance: sensor.power_mosenergosbyt_balance
power_total: sensor.power_mosenergosbyt_total
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
# power_sender:
# class: PowerCounterSender
# module: mosru
# power_mosenergosbyt_total: sensor.power_mosenergosbyt_total
# threshold: 100
# entity_ids:
# power_total: sensor.power_main_total
# app: mosru_client
# dependencies:
# - mosru_client
# global_dependencies:
# - globals
#---------------------------------------------------------
mosru_client_belorechenskaya:
class: MosruClient
module: mosru
properties:
login: !secret mosru_login
pwd: !secret mosru_pwd
token: !secret mosru_token
paycode: !secret mosru_belorechenskaya_paycode
flat_id: !secret mosru_belorechenskaya_flat_id
session_id: !secret mosru_session
global_dependencies:
- globals
epd_sensor_belorechenskaya:
class: EpdBalanceSensor
module: mosru
friendly_name: Баланс (ЕПД) Белореченская
entity_ids:
epd_balance: sensor.epd_balance_belorechenskaya
app: mosru_client_belorechenskaya
dependencies:
- mosru_client_belorechenskaya
global_dependencies:
- globals
power_sensor_belorechenskaya:
class: PowerCounterSensor
module: mosru
entity_ids:
power_balance: sensor.power_mosenergosbyt_belorechenskaya_balance
power_total: sensor.power_mosenergosbyt_belorechenskaya_total
app: mosru_client_belorechenskaya
dependencies:
- mosru_client_belorechenskaya
global_dependencies:
- globals
water_sensor_belorechenskaya:
class: WaterCounterSensor
module: mosru
entity_ids:
hot: sensor.counter_mosru_belorechenskaya_hot
cold: sensor.counter_mosru_belorechenskaya_cold
app: mosru_client_belorechenskaya
dependencies:
- mosru_client_belorechenskaya
global_dependencies:
- globals
#---------------------------------------------------------
mosru_client_fonvizina:
class: MosruClient
module: mosru
properties:
login: !secret mosru_login
pwd: !secret mosru_pwd
token: !secret mosru_token
paycode: !secret mosru_fonvizina_paycode
flat_id: !secret mosru_fonvizina_flat_id
session_id: !secret mosru_session
global_dependencies:
- globals
epd_sensor_mosru_fonvizina:
class: EpdBalanceSensor
module: mosru
friendly_name: Баланс (ЕПД) Фонвизина
entity_ids:
epd_balance: sensor.epd_balance_mosru_fonvizina
app: mosru_client_fonvizina
dependencies:
- mosru_client_fonvizina
global_dependencies:
- globals
#---------------------------------------------------------
mosru_client_milashenkova:
class: MosruClient
module: mosru
properties:
login: !secret mosru_login
pwd: !secret mosru_pwd
token: !secret mosru_token
paycode: !secret mosru_milashenkova_paycode
flat_id: !secret mosru_milashenkova_flat_id
session_id: !secret mosru_session
global_dependencies:
- globals
epd_sensor_milashenkova:
class: EpdBalanceSensor
module: mosru
friendly_name: Баланс (ЕПД) Милашенкова
entity_ids:
epd_balance: sensor.epd_balance_milashenkova
app: mosru_client_milashenkova
dependencies:
- mosru_client_milashenkova
global_dependencies:
- globals
power_sensor_milashenkova:
class: PowerCounterSensor
module: mosru
entity_ids:
power_balance: sensor.power_mosenergosbyt_milashenkova_balance
power_total: sensor.power_mosenergosbyt_milashenkova_total
app: mosru_client_milashenkova
dependencies:
- mosru_client_milashenkova
global_dependencies:
- globals
water_sensor_milashenkova:
class: WaterCounterSensor
module: mosru
entity_ids:
hot: sensor.counter_mosru_milashenkova_hot
cold: sensor.counter_mosru_milashenkova_cold
app: mosru_client_milashenkova
dependencies:
- mosru_client_milashenkova
global_dependencies:
- globals
#---------------------------------------------------------
@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
import os
import sys
import requests
import globals
import datetime as dt
import appdaemon.plugins.hass.hassapi as hass
from datetime import datetime, tzinfo, timedelta
from bs4 import BeautifulSoup
class EpdBalanceSensor(hass.Hass):
login = ''
pwd = ''
accounts = []
def initialize(self):
# if not globals.check_properties(self,['login', 'pwd', 'accounts']):
# self.log('not enought params')
# return
self.login = self.args.get('login', '')
self.pwd = self.args.get('pwd', '')
self.accounts = self.args.get('accounts', [])
self.timer = self.run_every(self.update_sensors, self.datetime()+timedelta(seconds=1), 1*60*60)
self.run_daily(self.send_counters_callback, "11:00:00", constrain_days = "sun")
self.send_counters_callback(None)
def to_balance(self, value_str):
value_str = value_str.replace(" ", "").replace(",", ".").replace("₽", "")
return -1 * float(value_str)
def update_sensors(self, args) -> None:
entity_ids = self.args.get('entity_ids',[])
session = self.get_auth_session(self.login, self.pwd)
payment_amounts = self.get_payment_amounts(session, self.accounts)
self.log(payment_amounts)
fonvizina_epd_balance = self.to_balance(payment_amounts[0]['service_to_pay'])
if 'fonvizina_epd_balance' in entity_ids:
entity_id = entity_ids['fonvizina_epd_balance']
attributes = {
'unit_of_measurement': '₽',
'icon': 'mdi:bank-transfer',
'date': datetime.now().strftime('%d.%m.%Y'),
'address': payment_amounts[0]['address'],
'friendly_name': 'Баланс (ЕПД)',
'description': 'Created and updated from appdaemon ({})'.format(__name__)
}
self.set_state(entity_id, state = str(fonvizina_epd_balance), attributes = attributes)
fonvizina_trash_balance = self.to_balance(payment_amounts[0]['trash_to_pay'])
if 'fonvizina_trash_balance' in entity_ids:
entity_id = entity_ids['fonvizina_trash_balance']
attributes = {
'unit_of_measurement': '₽',
'icon': 'mdi:bank-transfer',
'date': datetime.now().strftime('%d.%m.%Y'),
'address': payment_amounts[0]['address'],
'friendly_name': 'Баланс Выввоз мусора',
'description': 'Created and updated from appdaemon ({})'.format(__name__)
}
self.set_state(entity_id, state = str(fonvizina_trash_balance), attributes = attributes)
fonvizina_kladovka_balance = self.to_balance(payment_amounts[1]['service_to_pay'])
if 'fonvizina_kladovka_balance' in entity_ids:
entity_id = entity_ids['fonvizina_kladovka_balance']
attributes = {
'unit_of_measurement': '₽',
'icon': 'mdi:bank-transfer',
'date': datetime.now().strftime('%d.%m.%Y'),
'address': payment_amounts[1]['address'],
'friendly_name': 'Баланс Кладовка',
'description': 'Created and updated from appdaemon ({})'.format(__name__)
}
self.set_state(entity_id, state = str(fonvizina_kladovka_balance), attributes = attributes)
def get_auth_session(self, phone, password):
data={
'AUTH_FORM': 'Y',
'TYPE':'AUTH',
'backurl': '/',
'USER_LOGIN': phone,
'USER_PASSWORD': password
}
r = requests.post('https://lkcab.ru/?login=yes', data=data, verify=False, allow_redirects=False)
print(r.cookies)
return r.cookies['PHPSESSID']
def get_payment_amounts(self, session, accounts):
cookies = dict(PHPSESSID=session)
results = []
for account in accounts:
url = 'https://lkcab.ru/?accountID={}'.format(account)
r = requests.get(url, cookies=cookies, verify=False)
result = {}
result['account'] = account
doc = BeautifulSoup(r.content, 'html.parser')
address = doc.find("div", string="Вы собственник по адресу:").parent.find_all('div')[1].text
result['address'] = address
service_to_pay = doc.find('div', string="ЖКУ, к оплате:").parent.find_all('div')[1].text.replace(' Р.', '')
result['service_to_pay'] = service_to_pay
trash_to_pay = '0'
trash_to_pay_item = doc.find('div', string="Обращение с ТКО, к оплате:")
self.log(trash_to_pay_item)
if not trash_to_pay_item is None:
trash_to_pay = trash_to_pay_item.parent.find_all('div')[3].text.replace(' Р.', '')
result['trash_to_pay'] = trash_to_pay
results.append(result)
return results
def send_counters_callback(self, kwargs) -> None:
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
return
current_day = dt.datetime.now().strftime("%a").lower()
if current_day != "sun":
self.log(f'Today is {current_day}, but we run only on sunday.')
return
self.log('sending counters....')
session = self.get_auth_session(self.login, self.pwd)
self.send_counters(session)
return
def send_counters(self, session) -> None:
cookies = dict(PHPSESSID=session)
account = self.accounts[0]
url = 'https://lkcab.ru/?accountID={}'.format(account)
r = requests.get(url, cookies=cookies, verify=False)
url = 'https://lkcab.ru/counters/'
r = requests.get(url, cookies=cookies, verify=False)
doc = BeautifulSoup(r.content, 'html.parser')
bitrix_session = doc.find("input", id="sessid").get('value')
self.log('session: {}'.format(bitrix_session))
data = {
'action':'set_meters',
'sessid': bitrix_session,
}
needToSend = False
if 'heat_total' in self.args:
needToSend = True
value = self.get_state(self.args['heat_total'])
if value != None:
data['indiccur1[294174]'] = value
if 'counter_cold_outer' in self.args:
needToSend = True
value = self.get_state(self.args['counter_cold_outer'])
if value != None:
data['indiccur1[294175]'] = value
if 'counter_hot_outer' in self.args:
needToSend = True
value = self.get_state(self.args['counter_hot_outer'])
if value != None:
data['indiccur1[294173]'] = value
if needToSend:
self.log('sending: {}'.format(data))
r = requests.post("https://lkcab.ru/counters/", cookies=cookies, data=data, verify=False)
self.notify_tg("Показания отправлены в юнисервис. {}".format(data))
self.log('done.')
def notify_tg(self, msg):
if not 'notify' in self.args:
return
extra_data = {'parse_mode': 'html'}
self.notify(msg, name = self.args['notify'], data=extra_data)
@@ -0,0 +1,22 @@
#################################
# MOS.RU DATA RETRIEVER #
#################################
uniservice_epd_sensor:
class: EpdBalanceSensor
module: uniservice
login: !secret uniservice_login
pwd: !secret uniservice_pwd
accounts: !secret uniservice_accounts
entity_ids:
fonvizina_epd_balance: sensor.fonvizina_epd_balance
fonvizina_trash_balance: sensor.fonvizina_trash_balance
fonvizina_kladovka_balance: sensor.fonvizina_kladovka_balance
heat_total: sensor.heat_total
counter_cold_outer: sensor.counter_cold_outer
counter_hot_outer: sensor.counter_hot_outer
notify: telegram_monster
global_dependencies:
- globals

Some files were not shown because too many files have changed in this diff Show More