updating configuration
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
|||||||
|
*.env
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.log
|
||||||
|
*.1
|
||||||
|
*.fault
|
||||||
|
__pycache__/
|
||||||
|
hass/settings/.storage
|
||||||
|
hass/settings/*.log
|
||||||
|
secrets/
|
||||||
|
certbot/settings/data/
|
||||||
|
hass/settings/custom_components/
|
||||||
|
zigbee2mqtt/settings/log/
|
||||||
|
zigbee2mqtt/settings/configuration.yaml
|
||||||
|
zigbee2mqtt/settings/coordinator_backup.json
|
||||||
|
zigbee2mqtt/settings/state.json
|
||||||
|
nginx/settings/site-confs/z2m_htpasswd
|
||||||
|
postgres/settings/data/
|
||||||
|
node-red/settings/data/.npm/
|
||||||
|
node-red/settings/data/lib/
|
||||||
|
node-red/settings/data/node-modules/
|
||||||
|
node-red/settings/data/flows_cred.json
|
||||||
|
node-red/settings/data/*.backup
|
||||||
|
node-red/settings/data/.config.users.json
|
||||||
|
node-red/settings/data/package-lock.json
|
||||||
|
node-red/settings/data/package.json
|
||||||
|
node-red/settings/data/.config.nodes.json
|
||||||
|
node-red/settings/data/.config.runtime.json
|
||||||
|
homeconnect2mqtt/settings/config.json
|
||||||
|
hass/settings/backups/
|
||||||
|
matter-bridge/settings/data/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "secrets"]
|
||||||
|
path = secrets
|
||||||
|
url = git@github.com:monster1025/secrets.git
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#SHELL := /bin/bash
|
||||||
|
|
||||||
|
ARGS = `arg="$(filter-out $@,$(MAKECMDGOALS))" && echo $${arg:-${1}}`
|
||||||
|
|
||||||
|
update_secrets_sample:
|
||||||
|
@echo "Masking passwords..."
|
||||||
|
#hass
|
||||||
|
@cat ./secrets/hass-alt/settings/secrets.yaml | sed "s/\:.*/\: xxxxxxxxx/g" > hass/settings/secrets.yaml.sample #mask passwords
|
||||||
|
@sed -i "s/latitude:.*/latitude: 1.0/g" hass/settings/secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/longitude:.*/longitude: 1.0/g" hass/settings/secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/aqara1_mac:.*/aqara1_mac: xxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #len 12
|
||||||
|
@sed -i "s/aqara1_key:.*/aqara1_key: xxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #len 16
|
||||||
|
@sed -i "s/mirobot_key:.*/mirobot_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #len 32
|
||||||
|
@sed -i "s/powerstrip_key:.*/powerstrip_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #len 32
|
||||||
|
@sed -i "s/telegram_chat:.*/telegram_chat: 0/g" hass/settings/secrets.yaml.sample #must be a number
|
||||||
|
@sed -i "s/timezone:.*/timezone: Europe\/London/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/telegram_group_chat:.*/telegram_group_chat: 0/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/telegram_monster_chat:.*/telegram_monster_chat: 0/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/purifier_key:.*/purifier_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/philips_downlight_token:.*/philips_downlight_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/wifi_socket_key:.*/wifi_socket_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/modbus_host:.*/modbus_host: modbus.host/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/modbus_port:.*/modbus_port: 9977/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
@sed -i "s/xiaomi_remote_token:.*/xiaomi_remote_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/g" hass/settings/secrets.yaml.sample #must be valid
|
||||||
|
#@jq '(.wifi_ssid, .wifi_pwd, .mqtt_user, .mqtt_pwd) |= "XXXX"' -c sls/settings/config.json > sls/settings/config.json.sample
|
||||||
|
|
||||||
|
@echo "Masking env files..."
|
||||||
|
@find . -name *.env | xargs -I{} cp {} {}.sample
|
||||||
|
@find . -name *.env.sample | xargs -I{} sed -i "s/\=.*/\=xxxxxxxxx/g" {}
|
||||||
|
|
||||||
|
# @cat zigbee2mqtt/settings/configuration.yaml | sed "s/\:.*/\: xxxxxxxxx/g" > zigbee2mqtt/settings/configuration.yaml.sample #mask passwords
|
||||||
|
|
||||||
|
commit: update_secrets_sample
|
||||||
|
git add .
|
||||||
|
git diff-index --quiet HEAD || git commit -a -m "$(call ARGS,\"updating configuration\")"
|
||||||
|
git push
|
||||||
|
|
||||||
|
%:
|
||||||
|
@:
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Home Assistant FNV - Документация
|
||||||
|
|
||||||
|
Репозиторий содержит конфигурацию умного дома на базе Home Assistant с интеграцией различных сервисов и устройств.
|
||||||
|
|
||||||
|
## Структура документации
|
||||||
|
|
||||||
|
- [Docker контейнеры и настройки](docs/docker.md) - Описание всех Docker сервисов и их конфигурации
|
||||||
|
- [Home Assistant конфигурация](docs/home-assistant.md) - Настройки Home Assistant, устройства и интеграции
|
||||||
|
- [AppDaemon приложения](docs/appdaemon.md) - Скрипты, дашборды и функционал AppDaemon
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
1. Склонируйте репозиторий
|
||||||
|
2. Настройте переменные окружения в соответствующих `.env` файлах
|
||||||
|
3. Запустите `docker-compose up -d`
|
||||||
|
|
||||||
|
## Основные компоненты
|
||||||
|
|
||||||
|
- **Home Assistant** - Центральная система умного дома
|
||||||
|
- **AppDaemon** - Автоматизация и скрипты
|
||||||
|
- **ESPHome** - Управление ESP устройствами
|
||||||
|
- **Zigbee2MQTT** - Zigbee устройства
|
||||||
|
- **Node-RED** - Визуальное программирование
|
||||||
|
- **Mosquitto** - MQTT брокер
|
||||||
|
- **PostgreSQL** - База данных
|
||||||
|
- **Nginx** - Веб-сервер с SSL
|
||||||
|
- **Matter Bridge** - Интеграция с Matter устройствами
|
||||||
@@ -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" ]
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include *.md
|
||||||
|
include LICENSE
|
||||||
|
recursive-include emp_mos_api/*.py
|
||||||
|
recursive-include emp_mos_api/examples/*.py
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 += "Пожалуйста пополните баланс ({}): (< {}) \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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user