Compare commits

...

162 Commits

Author SHA1 Message Date
Andrey
1fd321d4c1 fix errors 2021-03-04 15:01:00 +03:00
Andrey
6732e1b7a2 add more logs on i2c update process 2021-03-03 13:55:35 +03:00
Andrey
f784c1c261 edit readme 2021-03-03 13:15:05 +03:00
Andrey
ab572c4db5 edit readme 2021-03-03 12:34:08 +03:00
Andrey
e55184f565 edit readme 2021-03-03 12:20:44 +03:00
Andrey
3f74f9739c edit readme 2021-03-03 12:20:09 +03:00
Andrey
49fcf880d9 add i2c sensors 2021-03-03 12:01:02 +03:00
Andrey
289f52ef73 fix errors 2021-02-28 22:27:57 +03:00
Andrey
ce589c97b9 fix errors 2021-02-28 22:22:17 +03:00
Andrey
22a6f8f444 fix errors 2021-02-28 22:06:36 +03:00
Andrey
9a53de1d5d fix errors 2021-02-28 22:02:22 +03:00
Andrey
bd8b07dd90 fix errors 2021-02-28 21:52:34 +03:00
Andrey
d9b6ba3a50 fix errors 2021-02-28 21:52:06 +03:00
Andrey
1042592a31 fix errors 2021-02-28 21:33:26 +03:00
Andrey
137eb8b6ba fix errors 2021-02-28 21:15:48 +03:00
Andrey
a2f412b89e fix errors 2021-02-28 20:16:04 +03:00
Andrey
8fa14cdbc5 fix errors 2021-02-28 20:15:47 +03:00
Andrey
fc17b82021 support response for extenders 2021-02-28 15:01:39 +03:00
Andrey
1aeaabfb3c fix errors 2021-02-28 14:50:47 +03:00
Andrey
a0bd8acac0 fix errors 2021-02-28 14:49:38 +03:00
andvikt
c48a3632d2 Update http.py 2021-02-28 13:04:00 +03:00
Andrey
e06ba65ead fix errors 2021-02-28 09:49:16 +03:00
Andrey
22720a27bd fix errors 2021-02-28 09:46:06 +03:00
Andrey
d0769b5b02 fix errors 2021-02-27 17:39:54 +03:00
Andrey
9ae093dd91 fix errors 2021-02-27 14:44:20 +03:00
Andrey
f88109c3a6 fix int(port) 2021-02-26 16:18:05 +03:00
Andrey
8742bb975f поддержка PCA9685 и MCP230 2021-02-25 12:11:05 +03:00
Andrey
97da209cf0 поддержка PCA9685 и MCP230 2021-02-25 11:52:11 +03:00
Andrey
2c0894356c добавлена возможность восстановления состояний выходов после перезагрузки меги 2021-02-24 10:56:11 +03:00
andvikt
cdc38f2200 Merge pull request #16 from Foroxon/master
UA translation update
2021-02-22 16:15:16 +03:00
Vladyslav Heneraliuk
bab1eab64a updated UA translation 2021-02-22 10:28:17 +02:00
Vladyslav Heneraliuk
c5d8ece77f Repo Sync (#2)
Repo Sync
2021-02-22 10:18:26 +02:00
Andrey
e4c5a4a712 find mega by mdid 2021-02-22 09:53:58 +03:00
Andrey
c60c088795 add allow hosts to web-ui 2021-02-20 18:10:36 +03:00
Andrey
eb3813843c edit readme 2021-02-19 12:46:10 +03:00
Andrey
804a6ad333 add proxy support 2021-02-19 11:10:34 +03:00
Andrey
a3398d0f2a add proxy support 2021-02-19 10:47:08 +03:00
Andrey
1e8777ca6d add proxy support 2021-02-19 10:39:15 +03:00
Andrey
ad2501ca8c add proxy support 2021-02-19 10:32:28 +03:00
Andrey
5681f7315c add proxy support 2021-02-19 10:06:56 +03:00
Andrey
da87c24db9 add proxy support 2021-02-19 09:50:31 +03:00
Andrey
a0dcfa4b9c fix force_d 2021-02-18 20:09:45 +03:00
Andrey
80e43888b9 fix force_d 2021-02-18 15:45:13 +03:00
Andrey
ba41cbb100 fix 1wire bus 2021-02-18 15:41:12 +03:00
Andrey
0b5b9744ba fix 1wire bus 2021-02-18 15:40:59 +03:00
Andrey
9746311f38 fix 1wire bus 2021-02-18 15:40:31 +03:00
Andrey
38a525f2f5 fix 2021-02-18 15:16:43 +03:00
Andrey
6503d6bddd fix 1wire 2021-02-18 15:02:21 +03:00
Andrey
4af40c29a7 fix d 2021-02-18 14:10:04 +03:00
Andrey
75a41c9667 fix i2c 2021-02-18 12:36:43 +03:00
Andrey
4f8f38fde6 fix http response and ds2413 bugs 2021-02-18 12:07:55 +03:00
Andrey
7e02797be8 fix http response 2021-02-18 11:58:33 +03:00
Andrey
035bdebf64 fix ds2413 2021-02-18 11:42:23 +03:00
Andrey
8d92bbdfb3 ad more logging 2021-02-18 11:23:44 +03:00
Andrey
0c43e61c59 make fake http response 2021-02-18 11:11:30 +03:00
Andrey
0a71be693e fix bugs 2021-02-18 11:00:41 +03:00
Andrey
8146148d0c fix bugs 2021-02-18 10:46:17 +03:00
Andrey
e0eaafd0fa fix bugs 2021-02-18 10:40:09 +03:00
Andrey
51f3eb3b19 fix bugs 2021-02-18 10:23:27 +03:00
Andrey
1716651497 fix bugs 2021-02-18 10:12:01 +03:00
Andrey
a87e8139a7 fix bugs 2021-02-18 09:53:30 +03:00
Andrey
358d29f8fd fix bugs 2021-02-18 09:27:40 +03:00
Andrey
fcce9dcfc1 fix bugs 2021-02-17 22:01:33 +03:00
Andrey
4fe2469a01 fix bugs 2021-02-17 19:10:58 +03:00
Andrey
9fab4fce62 fix config scan for ds2413 2021-02-17 18:08:53 +03:00
Andrey
3c28cf4598 Merge remote-tracking branch 'origin/master' 2021-02-17 18:04:53 +03:00
Andrey
af2360b0a0 edit 2021-02-17 18:04:09 +03:00
andvikt
8f7604c9f6 Update bug-report.md 2021-02-17 17:44:56 +03:00
andvikt
f70d72bf39 Update bug-report.md 2021-02-17 17:44:37 +03:00
Andrey
896e02a457 edit readme 2021-02-17 17:40:26 +03:00
Andrey
bb95c9d312 edit readme 2021-02-17 17:23:23 +03:00
Andrey
7ba1562f12 fix assumed state 2021-02-17 17:11:00 +03:00
Andrey
ec505ac2ef edit readme 2021-02-17 16:50:00 +03:00
Andrey
9544f562ba do not update all if "v" parametr 2021-02-15 21:54:21 +03:00
Andrey
c2422cac9c fix manifest 2021-02-15 21:51:31 +03:00
Andrey
0adba7fc0f add http v parsing 2021-02-15 21:50:49 +03:00
Andrey
39642700ca fix force_d 2021-02-15 19:05:00 +03:00
Andrey
07589e8e3a edit readme 2021-02-11 17:35:58 +03:00
Andrey
5a6903c67e fix config bug 2021-02-11 16:03:10 +03:00
Andrey
6758fd8d8e add server response in mqtt mode 2021-02-11 12:19:17 +03:00
Andrey
a9896c82fe add server response in mqtt mode 2021-02-11 12:00:01 +03:00
Andrey
5ed0b74eff add server response in mqtt mode 2021-02-11 11:57:17 +03:00
Andrey
4fccb23c39 remove port from command sendings 2021-02-10 14:05:16 +03:00
Andrey
03e4d3ff7e Merge remote-tracking branch 'origin/master' 2021-02-09 22:15:27 +03:00
Andrey
7907e0cd85 edit readme 2021-02-09 22:04:39 +03:00
andvikt
5211ee5330 Merge pull request #8 from r7sa/master
Улучшение поддержки I2C устройств

Спасибо большое!
2021-02-09 21:35:05 +03:00
Andrey
4e0f1dddcb add readme about srv loop 2021-02-09 21:30:26 +03:00
Sergey
742a0a9a09 - добавлены два новых класса устройства (давление, освещённость)
- улучшена поддержка I2C устройств, возвращающих только одно значение
2021-02-07 12:29:40 +03:00
Andrey
59443989a0 add poll outs 2021-02-06 09:43:10 +03:00
Andrey
5b86ceefe4 Merge remote-tracking branch 'origin/master' 2021-02-05 21:06:40 +03:00
Andrey
8cf000beae fix get_port 2021-02-05 21:06:27 +03:00
andvikt
99317da9f6 Update bug-report.md 2021-02-05 20:47:58 +03:00
Andrey
97911d0241 fix get_port 2021-02-05 20:21:14 +03:00
Andrey
d9925a2de0 i2c sensors 2021-02-05 20:08:28 +03:00
Andrey
f197a09072 invert inputs 2021-01-31 17:38:44 +03:00
Andrey
e8d92cfa36 invert inputs 2021-01-31 17:36:56 +03:00
Andrey
5f94186a14 edit readme 2021-01-31 12:10:17 +03:00
Andrey
f09610355b add version to manifest 2021-01-31 11:46:43 +03:00
Andrey
70e182fec3 Merge remote-tracking branch 'origin/master' 2021-01-31 11:42:11 +03:00
Andrey
ef3152a086 add adc port and template rendering values 2021-01-31 11:41:27 +03:00
andvikt
9b9443864c Update readme.md 2021-01-29 17:57:33 +03:00
andvikt
b7669ac407 Update readme.md 2021-01-29 17:55:45 +03:00
andvikt
2b308a71a1 Update readme.md 2021-01-29 17:55:12 +03:00
Andrey
8f67652c0e Merge remote-tracking branch 'origin/master' 2021-01-29 09:53:49 +03:00
Andrey
f83cdaa583 event monitoring propper restarting 2021-01-29 09:53:10 +03:00
andvikt
8dfa5926ad Merge pull request #4 from Foroxon/master
Ukrainian translation updating
2021-01-29 09:09:40 +03:00
Andrey
3978ce2203 fix 1-wire updating while busy 2021-01-29 09:08:34 +03:00
Andrey
72cf516353 fix mqtt sensor 2021-01-28 12:17:38 +03:00
Andrey
3b6459468b fix mqtt sensor 2021-01-28 12:04:43 +03:00
Andrey
c36c1369aa fix binary sensors 2021-01-28 10:29:16 +03:00
Andrey
6d3391bc45 add new events, fix binsensor 2021-01-27 21:40:55 +03:00
Vladyslav Heneraliuk
2e025eb0c2 updated Ukrainian translation 2021-01-26 12:16:42 +02:00
Vladyslav Heneraliuk
20d5b8ff40 Merge pull request #1 from andvikt/master
Sync
2021-01-26 12:10:46 +02:00
Andrey
a7d7738a5c fix mqtt 2021-01-26 08:34:27 +03:00
Andrey
c0b1247b9e smaller headers 2021-01-25 21:35:06 +03:00
Andrey
1548e8c364 fix multiple megas 2021-01-25 20:17:15 +03:00
Andrey
39c4ab0e3b fix device name 2021-01-25 18:45:23 +03:00
Andrey
a002e48e04 fix old mega out type 0 2021-01-25 18:13:25 +03:00
Andrey
dc6bdfc8f4 fix yaml exclusion 2021-01-25 17:56:47 +03:00
Andrey
e51b50797c fix yaml exclusion 2021-01-25 17:46:26 +03:00
Andrey
c4205c7ddc fix sw-link 2021-01-25 17:23:33 +03:00
Andrey
6164966d0b fix scanning 2021-01-25 17:21:55 +03:00
Andrey
ff6225a959 fix scanning 2021-01-25 17:18:42 +03:00
Andrey
57f355d479 fix scanning 2021-01-25 16:37:48 +03:00
Andrey
7d6273539e fix scanning 2021-01-25 15:57:10 +03:00
Andrey
5da2973351 fix scanning 2021-01-25 15:52:24 +03:00
Andrey
5eadd295f1 Merge branch 'master' of https://github.com/andvikt/mega_hacs 2021-01-25 15:48:29 +03:00
Andrey
5bf432a27f change readme 2021-01-25 15:47:55 +03:00
Andrey
d934e87ae5 force use http while scanning 2021-01-25 15:46:40 +03:00
andvikt
0b54db9c44 Update issue templates 2021-01-25 13:05:31 +03:00
Andrey
2d15b60929 add allowed hosts config 2021-01-25 12:54:15 +03:00
Andrey
b0b4fdd6cf fix dependencies 2021-01-25 12:29:31 +03:00
Andrey
8bbb4ab271 add http support 2021-01-25 12:25:11 +03:00
Andrey
876a1f0cc8 get port one more fix 2021-01-24 11:17:09 +03:00
Andrey
18d0aee391 small fix 2021-01-23 21:08:33 +03:00
Andrey
4e2d659c44 fix mid 2021-01-22 21:32:55 +03:00
Andrey
e0b4fec0ca add name cusomisation for multiple sensors 2021-01-22 12:28:04 +03:00
Andrey
b5e4b2c802 add name cusomisation for multiple sensors 2021-01-22 12:27:07 +03:00
Andrey
ed6a86a721 add name cusomisation for multiple sensors 2021-01-22 12:24:38 +03:00
Andrey
811f2067bb fix save_all service 2021-01-22 12:20:16 +03:00
Andrey
3e1499f78b fix get_port 2021-01-22 12:16:32 +03:00
Andrey
2a4a85a20e small bugfix 2021-01-22 11:12:18 +03:00
Andrey
2a0f60f1b2 edit readme 2021-01-22 10:53:15 +03:00
Andrey
a8eb50ee44 edit readme 2021-01-22 10:52:39 +03:00
Andrey
6b1635f60f edit readme 2021-01-22 10:51:32 +03:00
Andrey
62bdcfeb1b edit readme 2021-01-22 10:50:34 +03:00
Andrey
fa1c3330ba edit readme 2021-01-22 10:48:00 +03:00
Andrey
9755a9c654 edit readme 2021-01-22 10:27:54 +03:00
Andrey
bb4ce882f5 рефакторинг, конфиг yaml 2021-01-22 10:27:02 +03:00
andvikt
c3b9474d56 Merge pull request #3 from Foroxon/master
Added Ukrainian translation
2021-01-20 23:45:39 +03:00
andvikt
42f44d3020 Fix offline bugs 2021-01-20 23:33:53 +03:00
andvikt
3108927f15 fix offline bug 2021-01-20 23:25:57 +03:00
Vladyslav Heneraliuk
686d86aec5 renamed UA translation file 2021-01-18 11:30:24 +02:00
Vladyslav Heneraliuk
8d1b828362 added Ukrainian translation 2021-01-18 11:15:39 +02:00
Andrey
8539b8a6ee fix eng lang in options 2021-01-18 09:32:25 +03:00
Andrey
405fbe4777 fix eng lang in options 2021-01-18 09:28:38 +03:00
Andrey
ed4928011b add refresh devices 2021-01-15 16:05:10 +03:00
Andrey
ed9011a6e1 edit readme 2021-01-15 09:23:32 +03:00
Andrey
c7e8bcb83e edit readme 2021-01-15 09:20:52 +03:00
Andrey
21fd00083c edit readme 2021-01-15 09:13:12 +03:00
Andrey
c4f4510941 add device registry 2021-01-15 09:10:42 +03:00
Andrey
254015be4c edit readme 2021-01-15 08:45:27 +03:00
24 changed files with 2120 additions and 628 deletions

121
.experiment.py Normal file
View File

@@ -0,0 +1,121 @@
from urllib.parse import urlparse, parse_qsl
from bs4 import BeautifulSoup
page = '''
<html><head></head><body><a href="/sec/?pt=33">Back</a><br>0x15 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=t67xx">T67XX</a><br>0x40 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=htu21d">HTU21D</a>/PCA9685/HM3301<br>0x4a - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=max44009">MAX44009</a><br>
</body></html>
'''
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
def parse_scan_page(page: str):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
if 'i2c_dev' in params:
dev = params['i2c_dev']
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
class Skip:
pass
class Request:
pass
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}
print(parse_scan_page(page))

27
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Описание**
Описание проблемы
**Версии систем**
Enviroment: raspberry/linux/windows/macos/docker
HA version:
mega_hacs version:
megad firmware version:
используется mqtt: true/false
**Ожидаемое поведение**
Описание правильного поведения
**Screenshots**
If applicable, add screenshots to help explain your problem.
**LOG**
Просьба прикладывать детальный лог, подробная инструкция как включать отладку по [ссылке](https://github.com/andvikt/mega_hacs/wiki/Отладка)

View File

@@ -1,36 +0,0 @@
import asyncio
from bs4 import BeautifulSoup
import aiohttp
host = '192.168.88.14/sec'
# page = '''
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
# <head><style>input,select{margin:1px}</style></head>
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
# <a href="/sec/?cf=3">Back</a>
# <br>
# P7/ON
# <br>
# <a href="/sec/?pt=7&amp;cmd=7:1">ON</a>
# <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a>
# <br>
# <form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form>
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&amp;cmd=7:1">ON</a> <a href="/sec/?pt=7&amp;cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
# '''
# tree = BeautifulSoup(page, features="lxml")
# pty = tree.find('select', attrs={'name': 'pty'}).find(selected=True)['value']
# m = tree.find('select', attrs={'name': 'm'})
# if m:
# m = m.find(selected=True)['value']
#
# print(pty, m)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(
scan_port(0)
)

View File

@@ -4,40 +4,82 @@ import logging
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_ID
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
)
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass from homeassistant.helpers.service import bind_hass
from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE
from .hub import MegaD from .hub import MegaD
from .config_flow import ConfigFlow
from .http import MegaView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MQTT_ID = "mqtt_id"
CONF_PORT_TO_SCAN = 'port_to_scan'
MEGA = { CUSTOMIZE_PORT = {
vol.Required(CONF_HOST): str, vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_MQTT_ID, default=""): str, vol.Optional(CONF_NAME): vol.Any(str, {
vol.Optional(CONF_SCAN_INTERVAL, default=60): int, vol.Required(str): str
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, }),
} vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
MEGA_MAPPED = {str: MEGA} vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(CONF_DEVICE_CLASS):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(
CONF_RESPONSE_TEMPLATE,
description='шаблон ответа когда на этот порт приходит'
'сообщение из меги '): cv.template,
vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано
vol.Optional(CONF_GET_VALUE, default=True): bool,
vol.Optional(CONF_CONV_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
}
def extender(x):
if isinstance(x, str) and 'e' in x:
return x
else:
raise ValueError('must has "e" in port name')
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Any(MEGA, MEGA_MAPPED) DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional(
CONF_DEF_RESPONSE,
description='Ответ по умолчанию',
default=None
): vol.Any(cv.template, None),
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
CUSTOMIZE_PORT,
CUSTOMIZE_DS2413,
)
}
}
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
PLATFORMS = [
"light",
"binary_sensor",
"sensor",
]
ALIVE_STATE = 'alive' ALIVE_STATE = 'alive'
DEF_ID = 'def' DEF_ID = 'def'
_POLL_TASKS = {} _POLL_TASKS = {}
@@ -46,9 +88,12 @@ _subs = {}
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the mega component.""" """YAML-конфигурация содержит только кастомизации портов"""
conf = config.get(DOMAIN) hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})}
hass.data[DOMAIN] = {} hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(DOMAIN, {}))
hass.data[DOMAIN][CONF_ALL] = {}
view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
hass.http.register_view(view)
hass.services.async_register( hass.services.async_register(
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({ DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
vol.Optional('mega_id'): str vol.Optional('mega_id'): str
@@ -57,35 +102,47 @@ async def async_setup(hass: HomeAssistant, config: dict):
hass.services.async_register( hass.services.async_register(
DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({ DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({
vol.Optional('mega_id'): str, vol.Optional('mega_id'): str,
vol.Optional('port'): int, vol.Optional('port'): vol.Any(int, [int]),
}) })
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({ DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({
vol.Required('port'): int, vol.Optional('port'): int,
vol.Required('cmd'): str, vol.Required('cmd'): str,
vol.Optional('mega_id'): str, vol.Optional('mega_id'): str,
}) })
) )
if conf is None:
return True
if CONF_HOST in conf:
conf = {DEF_ID: conf}
for id, data in conf.items():
_LOGGER.warning('YAML configuration is deprecated, please use web-interface')
await _add_mega(hass, id, data)
for id, hub in hass.data[DOMAIN].items():
_POLL_TASKS[id] = asyncio.create_task(hub.poll())
return True return True
async def _add_mega(hass: HomeAssistant, id, data: dict): async def get_hub(hass, entry):
id = entry.data.get('id', entry.entry_id)
data = dict(entry.data)
data.update(entry.options or {})
data.update(id=id) data.update(id=id)
_mqtt = hass.data.get(mqtt.DOMAIN) use_mqtt = data.get(CONF_MQTT_INPUTS, True)
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first') _mqtt = hass.data.get(mqtt.DOMAIN) if use_mqtt else None
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER) if _mqtt is None and use_mqtt:
for x in range(5):
await asyncio.sleep(5)
_mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is not None:
break
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first')
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
return hub
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry)
hub.fw = await hub.get_fw()
hass.data[DOMAIN][id] = hub
hass.data[DOMAIN][CONF_ALL][id] = hub
if not await hub.authenticate(): if not await hub.authenticate():
raise Exception("not authentificated") raise Exception("not authentificated")
mid = await hub.get_mqtt_id() mid = await hub.get_mqtt_id()
@@ -94,42 +151,72 @@ async def _add_mega(hass: HomeAssistant, id, data: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id) hub: MegaD = await _add_mega(hass, entry)
data = dict(entry.data)
data.update(entry.options or {})
hub = await _add_mega(hass, id, data)
_hubs[entry.entry_id] = hub _hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(update) _subs[entry.entry_id] = entry.add_update_listener(updater)
await hub.start()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup( hass.config_entries.async_forward_entry_setup(
entry, platform entry, platform
) )
) )
_POLL_TASKS[id] = asyncio.create_task(hub.poll()) await hub.updater.async_refresh()
return True return True
async def update(hass: HomeAssistant, entry: ConfigEntry): async def updater(hass: HomeAssistant, entry: ConfigEntry):
"""
Обновляется конфигурация
:param hass:
:param entry:
:return:
"""
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]] hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL] hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options[CONF_PORT_TO_SCAN] hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
if entry.options[CONF_RELOAD]: entry.data = entry.options
await async_remove_entry(hass, entry) for platform in PLATFORMS:
await async_setup_entry(hass, entry) await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True return True
async def async_remove_entry(hass, entry) -> None: async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry.""" """Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hass.data[DOMAIN][id].unsubscribe_all() hub: MegaD = hass.data[DOMAIN].get(id)
task: asyncio.Task = _POLL_TASKS.pop(id) if hub is None:
task.cancel() return
_hubs.pop(entry.entry_id) _LOGGER.debug(f'remove {id}')
unsub = _subs.pop(entry.entry_id) _hubs.pop(id, None)
unsub() hass.data[DOMAIN].pop(id, None)
hass.data[DOMAIN][CONF_ALL].pop(id, None)
task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None:
task.cancel()
if hub is None:
return
await hub.stop()
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION)
hub = await get_hub(hass, config_entry)
new = dict(config_entry.data)
await hub.start()
cfg = await hub.get_config()
await hub.stop()
new.update(cfg)
_LOGGER.debug(f'new config: %s', new)
config_entry.data = new
config_entry.version = ConfigFlow.VERSION
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
async def _save_service(hass: HomeAssistant, call: ServiceCall): async def _save_service(hass: HomeAssistant, call: ServiceCall):
@@ -139,7 +226,8 @@ async def _save_service(hass: HomeAssistant, call: ServiceCall):
await hub.save() await hub.save()
else: else:
for hub in hass.data[DOMAIN].values(): for hub in hass.data[DOMAIN].values():
await hub.save() if isinstance(hub, MegaD):
await hub.save()
@bind_hass @bind_hass
@@ -149,25 +237,32 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
if port is None: if port is None:
await hub.get_all_ports() await hub.get_all_ports(check_skip=True)
else: elif isinstance(port, int):
await hub.get_port(port) await hub.get_port(port)
elif isinstance(port, list):
for x in port:
await hub.get_port(x)
else: else:
for hub in hass.data[DOMAIN].values(): for hub in hass.data[DOMAIN][CONF_ALL].values():
if not isinstance(hub, MegaD):
continue
if port is None: if port is None:
await hub.get_all_ports() await hub.get_all_ports(check_skip=True)
else: elif isinstance(port, int):
await hub.get_port(port) await hub.get_port(port)
elif isinstance(port, list):
for x in port:
await hub.get_port(x)
@bind_hass @bind_hass
async def _run_cmd(hass: HomeAssistant, call: ServiceCall): async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
port = call.data.get('port')
mega_id = call.data.get('mega_id') mega_id = call.data.get('mega_id')
cmd = call.data.get('cmd') cmd = call.data.get('cmd')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.send_command(port=port, cmd=cmd) await hub.request(cmd=cmd)
else: else:
for hub in hass.data[DOMAIN].values(): for hub in hass.data[DOMAIN].values():
await hub.send_command(port=port, cmd=cmd) await hub.request(cmd=cmd)

View File

@@ -1,6 +1,5 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import asyncio
import voluptuous as vol import voluptuous as vol
@@ -11,15 +10,17 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID CONF_ID,
CONF_ENTITY_ID,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT, CONF_RESPONSE_TEMPLATE
from .entities import MegaPushEntity
from .hub import MegaD from .hub import MegaD
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -40,20 +41,7 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for binary_sensors, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config:
if isinstance(x, int):
ent = MegaBinarySensor(
mega_id=mid, port=x
)
else:
ent = MegaBinarySensor(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME]
)
ents.append(ent)
add_entities(ents)
return True return True
@@ -61,30 +49,66 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
async def scan(): for port, cfg in config_entry.data.get('binary_sensor', {}).items():
async for port, pty, m in hub.scan_ports(): port = int_ignore(port)
if pty == "0": c = customize.get(mid, {}).get(port, {})
sensor = MegaBinarySensor(mega_id=mid, port=port) if c.get(CONF_SKIP, False):
devices.append(sensor) continue
hub.lg.debug(f'add binary_sensor on port %s', port)
async_add_devices(devices) sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
devices.append(sensor)
asyncio.create_task(scan()) async_add_devices(devices)
class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity): class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_on = None self._is_on = None
self._attrs = None
self._click_task = None
async def _click(self):
await self.customize.get
@property
def state_attributes(self):
return self._attrs
@property
def invert(self):
return self.customize.get(CONF_INVERT, False)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
if self._is_on is not None: val = self.mega.values.get(self.port, {})
return self._is_on if isinstance(val, dict):
return self._state == 'ON' val = val.get("value", val.get('m'))
if val is None and self._state is not None:
return self._state == 'ON'
elif val is not None:
if val in ['ON', 'OFF', '1', '0']:
return val in ['ON', '1'] if not self.invert else val in ['OFF', '0']
elif isinstance(val, int):
return val != 1 if not self.invert else val == 1
def _update(self, payload: dict): def _update(self, payload: dict):
val = payload.get("value") self.mega.values[self.port] = payload
self._is_on = val == 'ON' if not self.mega.mqtt_inputs:
return
template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None)
if template is not None:
template.hass = self.hass
ret = template.async_render(payload)
self.mega.lg.debug(f'response: %s', ret)
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd=ret)
)
elif self.mega.force_d:
self.mega.lg.debug(f'response d')
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd='d')
)

View File

@@ -1,5 +1,5 @@
"""Пока не сделано""" """Пока не сделано"""
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@@ -8,8 +8,10 @@ from homeassistant import config_entries, core
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD # pylint:disable=unused-import from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -17,15 +19,35 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema( STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ID, default='def'): str, vol.Required(CONF_ID, default='mega'): str,
vol.Required(CONF_HOST, default="192.168.0.14"): str, vol.Required(CONF_HOST, default="192.168.0.14"): str,
vol.Required(CONF_PASSWORD, default="sec"): str, vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=60): int, vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
vol.Optional(CONF_NPORTS, default=37): int,
vol.Optional(CONF_UPDATE_ALL, default=True): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
vol.Optional(CONF_FORCE_D, default=True): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool,
vol.Optional(CONF_PROTECTED, default=True): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
}, },
) )
async def get_hub(hass: HomeAssistant, data):
_mqtt = hass.data.get(mqtt.DOMAIN)
# if not isinstance(_mqtt, mqtt.MQTT):
# raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
@@ -33,12 +55,7 @@ async def validate_input(hass: core.HomeAssistant, data):
""" """
if data[CONF_ID] in hass.data.get(DOMAIN, []): if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId('duplicate_id') raise exceptions.DuplicateId('duplicate_id')
_mqtt = hass.data.get(mqtt.DOMAIN) hub = await get_hub(hass, data)
if not isinstance(_mqtt, mqtt.MQTT):
raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
if not await hub.authenticate():
raise exceptions.InvalidAuth
return hub return hub
@@ -46,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega.""" """Handle a config flow for mega."""
VERSION = 1 VERSION = 19
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
@@ -59,7 +76,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
try: try:
await validate_input(self.hass, user_input) hub = await validate_input(self.hass, user_input)
await hub.start()
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
)
except exceptions.CannotConnect: except exceptions.CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except exceptions.InvalidAuth: except exceptions.InvalidAuth:
@@ -69,11 +95,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors[CONF_ID] = str(exc) errors[CONF_ID] = str(exc)
else:
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=user_input,
)
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -92,18 +113,42 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options."""
hub = await get_hub(self.hass, self.config_entry.data)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data)
cfg.update(user_input)
hub = await get_hub(self.hass, cfg)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
cfg.update(new)
return self.async_create_entry( return self.async_create_entry(
title='', title='',
data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}}, data=cfg,
) )
e = self.config_entry.data e = self.config_entry.data
ret = self.async_show_form( ret = self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): int, vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_RELOAD, default=False): bool, vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)): bool,
vol.Optional(CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=e.get(CONF_RESTORE_ON_RESTART, False)): bool,
vol.Optional(CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)): bool,
vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
# vol.Optional(CONF_INVERT, default=''): str, # vol.Optional(CONF_INVERT, default=''): str,
}), }),
) )

View File

@@ -0,0 +1,59 @@
from dataclasses import dataclass, field
from bs4 import BeautifulSoup
inputs = [
'eact',
'inta',
'misc',
]
selectors = [
'pty',
'm',
'gr',
'd',
'ety',
]
@dataclass(frozen=True, eq=True)
class Config:
pty: str = None
m: str = None
gr: str = None
d: str = None
ety: str = None
inta: str = field(compare=False, hash=False, default=None)
misc: str = field(compare=False, hash=False, default=None)
eact: str = field(compare=False, hash=False, default=None)
src: BeautifulSoup = field(compare=False, hash=False, default=None)
def parse_config(page: str):
page = BeautifulSoup(page, features="lxml")
ret = {}
for x in selectors:
v = page.find('select', attrs={'name': x})
if v is None:
continue
else:
v = v.find(selected=True)
if v:
v = v['value']
ret[x] = v
for x in inputs:
v = page.find('input', attrs={'name': x})
if v:
ret[x] = v['value']
return Config(**ret, src=page)
DIGITAL_IN = Config(pty="0")
RELAY_OUT = Config(pty="1", m="0")
PWM_OUT = Config(pty="1", m="1")
DS2413 = Config(pty="1", m="2")
MCP230 = Config(pty="4", m="1", gr="3", d="20")
MCP230_OUT = Config(ety="1")
MCP230_IN = Config(ety="0")
PCA9685 = Config(pty="4", m="1", gr="3", d="21")
OWIRE_BUS = Config(pty="3", d="5")

View File

@@ -1,4 +1,5 @@
"""Constants for the mega integration.""" """Constants for the mega integration."""
import re
DOMAIN = "mega" DOMAIN = "mega"
CONF_MEGA_ID = "mega_id" CONF_MEGA_ID = "mega_id"
@@ -12,3 +13,44 @@ W1BUS = 'w1bus'
CONF_PORT_TO_SCAN = 'port_to_scan' CONF_PORT_TO_SCAN = 'port_to_scan'
CONF_RELOAD = 'reload' CONF_RELOAD = 'reload'
CONF_INVERT = 'invert' CONF_INVERT = 'invert'
CONF_PORTS = 'ports'
CONF_CUSTOM = '__custom'
CONF_HTTP = '__http'
CONF_ALL = '__all'
CONF_SKIP = 'skip'
CONF_MQTT_INPUTS = 'mqtt_inputs'
CONF_NPORTS = 'nports'
CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all'
CONF_FAKE_RESPONSE = 'fake_response'
CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts'
CONF_PROTECTED = 'protected'
CONF_CONV_TEMPLATE = 'conv_template'
CONF_POLL_OUTS = 'poll_outs'
CONF_FORCE_D = 'force_d'
CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart'
CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time'
PLATFORMS = [
"light",
"switch",
"binary_sensor",
"sensor",
]
EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor'
EVENT_BINARY = f'{DOMAIN}.binary'
PATT_SPLIT = re.compile('[;/]')
LONG = 'long'
RELEASE = 'release'
LONG_RELEASE = 'long_release'
PRESS = 'press'
LUX = 'lux'
SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double'
PATT_FW = re.compile(r'fw:\s(.+)\)')

View File

@@ -1,36 +1,104 @@
import logging
import asyncio import asyncio
import json from homeassistant.config_entries import ConfigEntry
import logging from homeassistant.const import CONF_NAME
from homeassistant.core import State from homeassistant.core import State
from .hub import MegaD from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from .const import DOMAIN from . import hub as h
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY
_events_on = False
_LOGGER = logging.getLogger(__name__)
class BaseMegaEntity(RestoreEntity): async def _set_events_on():
global _events_on, _task_set_ev_on
await asyncio.sleep(10)
_LOGGER.debug('events on')
_events_on = True
def set_events_off():
global _events_on, _task_set_ev_on
_events_on = False
_task_set_ev_on = None
_task_set_ev_on = None
class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
""" """
Base Mega's entity. It is responsible for storing reference to mega hub Base Mega's entity. It is responsible for storing reference to mega hub
Also provides some basic entity information: unique_id, name, availiability Also provides some basic entity information: unique_id, name, availiability
It also makes subscription to port states All base entities are polled in order to be online or offline
""" """
def __init__( def __init__(
self, self,
mega_id: str, mega: 'h.MegaD',
port: int, port: int,
config_entry: ConfigEntry = None,
id_suffix=None, id_suffix=None,
name=None, name=None,
unique_id=None unique_id=None,
http_cmd='get',
addr: str=None,
index=None,
): ):
super().__init__(mega.updater)
self.http_cmd = http_cmd
self._state: State = None self._state: State = None
self.port = port self.port = port
self._mega_id = mega_id self.config_entry = config_entry
self.mega = mega
mega.entities.append(self)
self._mega_id = mega.id
self._lg = None self._lg = None
self._unique_id = unique_id or f"mega_{mega_id}_{port}" + \ self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._name = name or f"{mega_id}_{port}" + \ self._name = name or f"{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None
self.index = index
self.addr = addr
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property
def customize(self):
if self.hass is None:
return {}
if self._customize is None:
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {}
c = c.get(self.port) or {}
if self.addr is not None and self.index is not None and isinstance(c, dict):
idx = self.addr.lower() + f'_a' if self.index == 0 else '_b'
c = c.get(idx, {})
self._customize = c
return self._customize
@property
def device_info(self):
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, f'{self._mega_id}', self.port),
},
"config_entries": [
self.config_entry,
],
"name": f'{self._mega_id} port {self.port}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
"sw_version": self.mega.fw,
"via_device": (DOMAIN, self._mega_id),
}
@property @property
def lg(self) -> logging.Logger: def lg(self) -> logging.Logger:
@@ -38,38 +106,264 @@ class BaseMegaEntity(RestoreEntity):
self._lg = self.mega.lg.getChild(self._name or self.unique_id) self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return self._lg return self._lg
@property
def mega(self) -> MegaD:
return self.hass.data[DOMAIN][self._mega_id]
@property @property
def available(self) -> bool: def available(self) -> bool:
return self.mega.online return self.mega.online
@property @property
def name(self): def name(self):
return self._name or f"{self.mega.id}_p{self.port}" c = self.customize.get(CONF_NAME)
if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}"
return c
@property @property
def unique_id(self): def unique_id(self):
return self._unique_id return self._unique_id
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
await self.mega.subscribe(self.port, callback=self.__update) global _task_set_ev_on
await super().async_added_to_hass()
self._state = await self.async_get_last_state() self._state = await self.async_get_last_state()
await asyncio.sleep(0.1) if self.mega.mqtt_inputs and _task_set_ev_on is None:
await self.mega.get_port(self.port) _task_set_ev_on = asyncio.create_task(_set_events_on())
def __update(self, msg): async def get_state(self):
try: self.lg.debug(f'state is %s', self.state)
value = json.loads(msg.payload) if not self.mega.mqtt_inputs:
except Exception as exc: self.async_write_ha_state()
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return
class MegaPushEntity(BaseMegaEntity):
"""
Updates on messages from mqtt
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mega.subscribe(self.port, callback=self.__update)
self.is_first_update = True
def __update(self, value: dict):
self._update(value) self._update(value)
self.hass.async_create_task(self.async_update_ha_state()) self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state) self.lg.debug(f'state after update %s', self.state)
if self.mega.mqtt_inputs and not _events_on:
_LOGGER.debug('skip event because events are off')
return
if not self.entity_id.startswith('binary_sensor'):
_LOGGER.debug('skip event because not a bnary sens')
return
ll: bool = self.mega.last_long.get(self.port, False)
if safe_int(value.get('click', 0)) == 1:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': SINGLE_CLICK
}
)
elif safe_int(value.get('click', 0)) == 2:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': DOUBLE_CLICK
}
)
elif safe_int(value.get('m', 0)) == 2:
self.mega.last_long[self.port] = True
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': LONG
}
)
elif safe_int(value.get('m', 0)) == 1:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': LONG_RELEASE if ll else RELEASE,
}
)
elif safe_int(value.get('m', None)) == 0:
self.hass.bus.async_fire(
event_type=EVENT_BINARY,
event_data={
'entity_id': self.entity_id,
'type': PRESS,
}
)
self.mega.last_long[self.port] = False
return return
def _update(self, payload: dict): def _update(self, payload: dict):
raise NotImplementedError pass
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
if self.mega.mqtt is not None:
asyncio.create_task(self.mega.get_port(self.port))
class MegaOutPort(MegaPushEntity):
def __init__(
self,
dimmer=False,
dimmer_scale=1,
*args, **kwargs
):
super().__init__(
*args, **kwargs
)
self._brightness = None
self._is_on = None
self.dimmer = dimmer
self.dimmer_scale = dimmer_scale
# @property
# def assumed_state(self) -> bool:
# return True if self.index is not None or self.mega.mqtt is None else False
@property
def invert(self):
return self.customize.get(CONF_INVERT, False)
@property
def brightness(self):
if not self.dimmer:
return
val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state.attributes.get("brightness")
elif isinstance(self.port, str) and 'e' in self.port:
if isinstance(val, str):
val = safe_int(val)
else:
val = 0
if val == 0:
return self._brightness
elif isinstance(val, (int, float)):
return int(val / self.dimmer_scale)
elif val is not None:
val = val.get("value")
if val is None:
return
try:
val = int(val)
return val
except Exception:
pass
@property
def is_on(self) -> bool:
val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state == 'ON'
elif isinstance(self.port, str) and 'e' in self.port and val:
if val is None:
return
if self.dimmer:
val = safe_int(val)
if val is not None:
return val > 0 if not self.invert else val == 0
else:
return val == 'ON' if not self.invert else val == 'OFF'
elif val is not None:
val = val.get("value")
if not isinstance(val, str) and self.index is not None and self.addr is not None:
if not isinstance(val, dict):
self.mega.lg.warning(f'{self.entity_id}: {val} is not a dict')
return
_val = val.get(self.addr, val.get(self.addr.lower(), val.get(self.addr.upper())))
if not isinstance(_val, str):
self.mega.lg.warning(f'{self.entity_id}: can not get {self.addr} from {val}, recieved {_val}')
return
_val = _val.split('/')
if len(_val) >= 2:
self.mega.lg.debug('%s parsed values: %s[%s]="%s"', self.entity_id, _val, self.index, _val)
val = _val[self.index]
else:
self.mega.lg.warning(f'{self.entity_id}: {_val} has wrong length')
return
elif self.index is not None and self.addr is None:
self.mega.lg.warning(f'{self.entity_id} does not has addr')
return
self.mega.lg.debug('%s.state = %s', self.entity_id, val)
if not self.invert:
return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0)
else:
return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0)
@property
def cmd_port(self):
if self.index is not None:
return f'{self.port}A' if self.index == 0 else f'{self.port}B'
else:
return self.port
async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness or 255
self._brightness = brightness
if self.dimmer and brightness == 0:
cmd = 255 * self.dimmer_scale
elif self.dimmer:
cmd = brightness * self.dimmer_scale
else:
cmd = 1 if not self.invert else 0
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
port=self.port,
force_http=True,
conv=False,
http_cmd='list',
)
elif isinstance(self.port, str) and 'e' in self.port:
if not self.dimmer:
self.mega.values[self.port] = 'ON' if not self.invert else 'OFF'
else:
self.mega.values[self.port] = cmd
else:
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0" if not self.invert else "1"
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
port=self.port,
force_http=True,
conv=False,
http_cmd='list',
)
elif isinstance(self.port, str) and 'e' in self.port:
self.mega.values[self.port] = 'OFF' if not self.invert else 'ON'
else:
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
def safe_int(v):
if v == 'ON':
return 1
elif v == 'OFF':
return 0
try:
return int(v)
except (ValueError, TypeError):
return None

View File

@@ -15,3 +15,7 @@ class DuplicateId(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth.""" """Error to indicate there is invalid auth."""
class NoPort(Exception):
pass

View File

@@ -0,0 +1,167 @@
import asyncio
import logging
import typing
from collections import defaultdict
from aiohttp.web_request import Request
from aiohttp.web_response import Response
from homeassistant.helpers.template import Template
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from .tools import make_ints
from . import hub as h
_LOGGER = logging.getLogger(__name__).getChild('http')
def is_ext(data: typing.Dict[str, typing.Any]):
for x in data:
if x.startswith('ext'):
return True
class MegaView(HomeAssistantView):
url = '/mega'
name = 'mega'
requires_auth = False
def __init__(self, cfg: dict):
self._try = 0
self.protected = True
self.allowed_hosts = {'::1', '127.0.0.1'}
self.notified_attempts = defaultdict(lambda : False)
self.callbacks = defaultdict(lambda: defaultdict(list))
self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
mid: {
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
for pt in cfg[mid]
if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg if isinstance(cfg[mid], dict)
}
_LOGGER.debug('templates: %s', self.templates)
self.hubs = {}
async def get(self, request: Request) -> Response:
_LOGGER.debug('request from %s %s', request.remote, request.headers)
hass: HomeAssistant = request.app['hass']
if self.protected:
auth = False
for x in self.allowed_hosts:
if request.remote.startswith(x):
auth = True
break
if not auth:
msg = f"Non-authorised request from {request.remote} to `/mega`. "\
f"If you want to accept requests from this host "\
f"please add it to allowed hosts in `mega` UI-configuration"
if not self.notified_attempts[request.remote]:
await hass.services.async_call(
'persistent_notification',
'create',
{
"notification_id": request.remote,
"title": "Non-authorised request",
"message": msg
}
)
_LOGGER.warning(msg)
return Response(status=401)
remote = request.headers.get('X-Real-IP', request.remote)
hub: 'h.MegaD' = self.hubs.get(remote)
if hub is None and 'mdid' in request.query:
hub = self.hubs.get(request.query['mdid'])
if hub is None:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
if hub is None and request.remote in ['::1', '127.0.0.1']:
hub = self.hubs.get('__def')
elif hub is None:
return Response(status=400)
data = dict(request.query)
hass.bus.async_fire(
EVENT_BINARY_SENSOR,
data,
)
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data)
if data.get('st') == '1' and hub.restore_on_restart:
asyncio.create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt')
data = data.copy()
update_all = True
if 'v' in data:
update_all = False
data['value'] = data.pop('v')
data['mega_id'] = hub.id
ret = 'd' if hub.force_d else ''
if port is not None:
if is_ext(data):
# ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
if port in hub.extenders:
pt_orig = port
else:
pt_orig = hub.ext_in.get(port, hub.ext_in.get(str(port)))
if pt_orig is None:
hub.lg.warning(f'can not find extender for int port {port}, '
f'have ext_int: {hub.ext_in}, ext: {hub.extenders}')
return Response(status=200)
for e, v in data.items():
_data = data.copy()
if e.startswith('ext'):
idx = e[3:]
pt = f'{pt_orig}e{idx}'
_data['pt_orig'] = pt_orig
_data['value'] = 'ON' if v == '1' else 'OFF'
_data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
hub.values[pt] = _data
for cb in self.callbacks[hub.id][pt]:
cb(_data)
act = hub.ext_act.get(pt)
hub.lg.debug(f'act on port {pt}: {act}, all acts are: {hub.ext_act}')
template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
if template is not None:
template.hass = hass
ret = template.async_render(_data)
hub.lg.debug(f'response={ret}, template={template}')
if ret == 'd' and act:
await hub.request(cmd=act.replace(':3', f':{v}'))
ret = 'd' if hub.force_d else ''
else:
hub.values[port] = data
for cb in self.callbacks[hub.id][port]:
cb(data)
template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
if template is not None:
template.hass = hass
ret = template.async_render(data)
if hub.update_all and update_all:
asyncio.create_task(self.later_update(hub))
_LOGGER.debug('response %s', ret)
Response(body='' if hub.fake_response else ret, content_type='text/plain')
if hub.fake_response and 'value' not in data and 'pt' in data:
if 'd' in ret:
await hub.request(pt=port, cmd=ret)
else:
await hub.request(cmd=ret)
return ret
async def later_restore(self, hub):
"""
Восстановление всех выходов с небольшой задержкой. Задержка нужна чтобы ответ прошел успешно
:param hub:
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
async def later_update(self, hub):
await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh()

View File

@@ -1,17 +1,60 @@
import asyncio import asyncio
import json
import logging import logging
from datetime import datetime from collections import defaultdict
from functools import wraps from datetime import datetime, timedelta
import aiohttp import aiohttp
import typing import typing
from bs4 import BeautifulSoup import re
import json
from bs4 import BeautifulSoup
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .exceptions import CannotConnect from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
from .const import (
TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW
)
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PRESS_PATT = re.compile(r'press:([01234567890\.]+)')
LUX_PATT = re.compile(r'lux:([01234567890\.]+)')
PATTERNS = {
TEMP: TEMP_PATT,
HUM: HUM_PATT,
PRESS: PRESS_PATT,
LUX: LUX_PATT
}
UNITS = {
TEMP: TEMP_CELSIUS,
HUM: PERCENTAGE,
PRESS: 'mmHg',
LUX: LIGHT_LUX
}
CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY,
PRESS: DEVICE_CLASS_PRESSURE,
LUX: DEVICE_CLASS_ILLUMINANCE
}
I2C_DEVICE_TYPES = {
"2": LUX, # BH1750
"3": LUX, # TSL2591
"7": LUX, # MAX44009
"70": LUX, # OPT3001
}
class MegaD: class MegaD:
@@ -20,97 +63,214 @@ class MegaD:
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
loop: asyncio.AbstractEventLoop,
host: str, host: str,
password: str, password: str,
mqtt: mqtt.MQTT, mqtt: mqtt.MQTT,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
mqtt_inputs: bool = True,
mqtt_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
inverted: typing.List[int] = None, nports=38,
update_all: bool=True,
poll_outs: bool=False,
fake_response: bool=True,
force_d: bool=None,
allow_hosts: str=None,
protected=True,
restore_on_restart=False,
extenders=None,
ext_in=None,
ext_acts=None,
i2c_sensors=None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
self.http.allowed_hosts |= {host}
self.http.hubs[host] = self
if len(self.http.hubs) == 1:
self.http.hubs['__def'] = self
if mqtt_id:
self.http.hubs[mqtt_id] = self
else:
self.http = None
self.extenders = extenders or []
self.ext_in = ext_in or {}
self.ext_act = ext_acts or {}
self.i2c_sensors = i2c_sensors or []
self.poll_outs = poll_outs
self.update_all = update_all if update_all is not None else True
self.nports = nports
self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
self.host = host self.host = host
self.sec = password self.sec = password
self.mqtt = mqtt self.mqtt = mqtt
self.id = id self.id = id
self.lck = asyncio.Lock() self.lck = asyncio.Lock()
self.is_alive = asyncio.Condition() self.last_long = {}
self._http_lck = PriorityLock()
self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition()
self.online = True self.online = True
self.entities: typing.List[Entity] = [] self.entities: typing.List[BaseMegaEntity] = []
self.ds2413_ports = set()
self.poll_interval = scan_interval self.poll_interval = scan_interval
self.subscriptions = [] self.subs = None
self.lg: logging.Logger = lg.getChild(self.id) self.lg: logging.Logger = lg.getChild(self.id)
self._scanned = {} self._scanned = {}
self.sensors = [] self.sensors = []
self.port_to_scan = port_to_scan self.port_to_scan = port_to_scan
self.inverted = inverted or []
self.last_update = datetime.now() self.last_update = datetime.now()
self._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list)
self._loop = loop
self._customize = None
self.values = {}
self.last_port = None
self.updater = DataUpdateCoordinator(
hass,
self.lg,
name="megad",
update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
)
self.updaters = []
self.fw = ''
self.notifiers = defaultdict(asyncio.Condition)
if not mqtt_id: if not mqtt_id:
_id = host.split(".")[-1] _id = host.split(".")[-1]
self.mqtt_id = f"megad/{_id}" self.mqtt_id = f"megad/{_id}"
else: else:
self.mqtt_id = mqtt_id self.mqtt_id = mqtt_id
self._loop: asyncio.AbstractEventLoop = None self.restore_on_restart = restore_on_restart
if force_d is not None:
self.customize[CONF_FORCE_D] = force_d
try:
if allow_hosts is not None:
allow_hosts = set(allow_hosts.split(';'))
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
hass.data[DOMAIN][CONF_HTTP].protected = protected
except Exception:
self.lg.exception('while setting allowed hosts')
async def start(self):
self.loop = asyncio.get_event_loop()
if self.mqtt is not None:
set_events_off()
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
async def stop(self):
if self.subs is not None:
self.subs()
for x in self._callbacks.values():
x.clear()
async def add_entity(self, ent): async def add_entity(self, ent):
async with self.lck: async with self.lck:
self.entities.append(ent) self.entities.append(ent)
async def get_sensors(self): async def get_sensors(self, only_list=False):
self.lg.debug(self.sensors) self.lg.debug(self.sensors)
_ports = {x.port for x in self.sensors} ports = []
for x in _ports: for x in self.sensors:
await self.get_port(x) if only_list and x.http_cmd != 'list':
await asyncio.sleep(0.1) continue
if x.port in ports:
continue
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
ports.append(x.port)
@property
def customize(self):
if self._customize is None:
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self.id) or {}
self._customize = c
return self._customize
@property
def force_d(self):
return self.customize.get(CONF_FORCE_D, False)
@property
def def_response(self):
return self.customize.get(CONF_DEF_RESPONSE, None)
@property
def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
def _warn_offline(self):
if self.online:
self.lg.warning('mega is offline')
self.hass.states.async_set(
f'mega.{self.id}',
'offline',
)
self.online = False
def _notify_online(self):
if not self.online:
self.hass.states.async_set(
f'mega.{self.id}',
'online',
)
self.online = True
async def _get_ds2413(self):
"""
обновление ds2413 устройств
:return:
"""
for x in self.ds2413_ports:
self.lg.debug(f'poll ds2413 for %s', x)
await self.get_port(
port=x,
force_http=True,
http_cmd='list',
conv=False
)
async def poll(self): async def poll(self):
""" """
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes Polling ports
offline
""" """
self._loop = asyncio.get_event_loop() self.lg.debug('poll')
for x in self.i2c_sensors:
while True: if not isinstance(x, dict):
if len(self.sensors) > 0: continue
await self.get_sensors() ret = await self._update_i2c(x)
else: if isinstance(ret, dict):
await self.get_port(self.port_to_scan) self.values.update(ret)
for x in self.extenders:
await asyncio.sleep(1) ret = await self._update_extender(x)
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval: if not isinstance(ret, dict):
await self.get_port(self.port_to_scan) self.lg.warning(f'wrong updater result: {ret} from extender {x}')
await asyncio.sleep(1) continue
if (datetime.now() - self.last_update).total_seconds() > self.poll_interval: self.values.update(ret)
self.lg.warning('mega is offline') if self.mqtt is None:
self.hass.states.async_set( await self.get_all_ports()
f'mega.{self.id}', await self.get_sensors(only_list=True)
'offline', elif self.poll_outs:
) await self.get_all_ports(check_skip=True)
self.online = False elif len(self.sensors) > 0:
else: await self.get_sensors()
self.hass.states.async_set( else:
f'mega.{self.id}', await self.get_port(self.port_to_scan)
'online', await self._get_ds2413()
) return self.values
self.online = True
for x in self.entities:
try:
await x.async_update_ha_state()
except RuntimeError:
pass
await asyncio.sleep(self.poll_interval - 1)
async def _async_notify(self):
async with self.is_alive:
self.is_alive.notify_all()
def _notify(self, *args):
asyncio.run_coroutine_threadsafe(self._async_notify(), self._loop)
async def get_mqtt_id(self): async def get_mqtt_id(self):
async with aiohttp.request( async with aiohttp.request(
@@ -123,103 +283,167 @@ class MegaD:
_id = _id['value'] _id = _id['value']
return _id or 'megad/' + self.host.split('.')[-1] return _id or 'megad/' + self.host.split('.')[-1]
async def get_fw(self):
data = await self.request()
return PATT_FW.search(data).groups()[0]
async def send_command(self, port=None, cmd=None): async def send_command(self, port=None, cmd=None):
if port: return await self.request(pt=port, cmd=cmd)
url = f"http://{self.host}/{self.sec}/?pt={port}&cmd={cmd}"
else: async def request(self, priority=0, **kwargs):
url = f"http://{self.host}/{self.sec}/?cmd={cmd}" cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
self.lg.debug('run command: %s', url) url = f"http://{self.host}/{self.sec}"
async with self.lck: if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck(priority):
async with aiohttp.request("get", url=url) as req: async with aiohttp.request("get", url=url) as req:
if req.status != 200: if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text()) self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return False return None
else: else:
return True ret = await req.text()
self.lg.debug('response %s', ret)
return ret
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
async def get_port(self, port): def parse_response(self, ret, cmd='get'):
if ret is None:
raise NoPort()
if 'busy' in ret:
return None
if ':' in ret:
if ';' in ret:
ret = ret.split(';')
elif '/' in ret and not cmd == 'list':
ret = ret.split('/')
else:
ret = [ret]
ret = {'value': dict([
x.split(':') for x in ret if x.count(':') == 1
])}
elif 'ON' in ret:
ret = {'value': 'ON'}
elif 'OFF' in ret:
ret = {'value': 'OFF'}
else:
ret = {'value': ret}
return ret
async def get_port(self, port, force_http=False, http_cmd='get', conv=True):
""" """
Опрашивает порт с помощью mqtt. Ждет ответ, возвращает ответ. Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values
:param port:
:return:
""" """
ftr = asyncio.get_event_loop().create_future() self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http:
if http_cmd == 'list' and conv:
await self.request(pt=port, cmd='conv')
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ntry += 1
self.lg.debug('parsed: %s', ret)
self.values[port] = ret
return ret
def cb(msg): async with self._notif_lck:
async with self.notifiers[port]:
try: cnd = self.notifiers[port]
if '"value":NA' in msg.payload.decode():
if not ftr.done():
ftr.set_result(None)
return
ret = json.loads(msg.payload).get('value')
if not ftr.done():
ftr.set_result(ret)
except Exception as exc:
ret = None
self.lg.exception(f'while parsing response from port {port}: {msg.payload}')
ftr.set_result(None)
self.lg.debug(
f'port: %s response: %s', port, ret
)
async with self.lck:
unsub = await self.mqtt.async_subscribe(
topic=f'{self.mqtt_id}/{port}',
msg_callback=cb,
qos=1,
)
try:
await self.mqtt.async_publish( await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd', topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}', payload=f'get:{port}',
qos=1, qos=2,
retain=False, retain=False,
) )
return await asyncio.wait_for(ftr, timeout=2) try:
except asyncio.TimeoutError: await asyncio.wait_for(cnd.wait(), timeout=10)
self.lg.warning(f'timeout on port {port}') return self.values.get(port)
finally: except asyncio.TimeoutError:
unsub() self.lg.error(f'timeout when getting port {port}')
async def get_all_ports(self): @property
for x in range(37): def ports(self):
asyncio.create_task(self.get_port(x)) return {e.port for e in self.entities}
async def get_all_ports(self, only_out=False, check_skip=False):
if not self.mqtt_inputs:
ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports:
continue
if check_skip and not port in self.ports:
continue
ret = self.parse_response(x)
self.values[port] = ret
elif not check_skip:
for x in range(self.nports + 1):
await self.get_port(x)
else:
for x in self.ports:
await self.get_port(x)
async def reboot(self, save=True): async def reboot(self, save=True):
await self.save() await self.save()
# await self.send_command(cmd=)
async def subscribe(self, port, callback): async def _notify(self, port, value):
async with self.notifiers[port]:
cnd = self.notifiers[port]
cnd.notify_all()
@wraps(callback) def _process_msg(self, msg):
def wrapper(msg): try:
self.lg.debug( d = msg.topic.split('/')
'process incomming message: %s', msg port = d[-1]
) except ValueError:
self.last_update = datetime.now() self.lg.warning('can not process %s', msg)
return callback(msg) return
if port == 'cmd':
return
try:
port = int_ignore(port)
except:
self.lg.warning('can not process %s', msg)
return
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, wrapper 'process incomming message: %s', msg
) )
subs = await self.mqtt.async_subscribe( value = None
topic=f"{self.mqtt_id}/{port}", try:
msg_callback=wrapper, value = json.loads(msg.payload)
qos=0, if isinstance(value, dict):
) make_ints(value)
self.subscriptions.append(subs) self.values[port] = value
for cb in self._callbacks[port]:
cb(value)
if isinstance(value, dict):
value = value.copy()
value['mega_id'] = self.id
self.hass.bus.async_fire(
EVENT_BINARY_SENSOR,
value,
)
except Exception as exc:
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return
finally:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
def unsubscribe_all(self): def subscribe(self, port, callback):
self.lg.info('unsubscribe') port = int_ignore(port)
for x in self.subscriptions: self.lg.debug(
self.lg.debug('unsubscribe %s', x) f'subscribe %s %s', port, callback
x() )
if self.mqtt_inputs:
self._callbacks[port].append(callback)
else:
self.http.callbacks[self.id][port].append(callback)
async def authenticate(self) -> bool: async def authenticate(self) -> bool:
"""Test if we can authenticate with the host.""" """Test if we can authenticate with the host."""
@@ -238,39 +462,177 @@ class MegaD:
return await req.text() return await req.text()
async def scan_port(self, port): async def scan_port(self, port):
if port in self._scanned: data = await self.request(pt=port)
return self._scanned[port] return parse_config(data)
url = f'http://{self.host}/{self.sec}/?pt={port}' # async with self.lck:
self.lg.debug( # if port in self._scanned:
f'scan port %s: %s', port, url # return self._scanned[port]
) # url = f'http://{self.host}/{self.sec}/?pt={port}'
async with aiohttp.request('get', url) as req: # self.lg.debug(
html = await req.text() # f'scan port %s: %s', port, url
tree = BeautifulSoup(html, features="lxml") # )
pty = tree.find('select', attrs={'name': 'pty'}) # async with aiohttp.request('get', url) as req:
if pty is None: # html = await req.text()
return # if req.status != 200:
else: # return
pty = pty.find(selected=True) # tree = BeautifulSoup(html, features="lxml")
if pty: # pty = tree.find('select', attrs={'name': 'pty'})
pty = pty['value'] # if pty is None:
else: # return
return # else:
if pty in ['0', '1']: # pty = pty.find(selected=True)
m = tree.find('select', attrs={'name': 'm'}) # if pty:
if m: # pty = pty['value']
m = m.find(selected=True)['value'] # else:
self._scanned[port] = (pty, m) # return
return pty, m # if pty in ['0', '1']:
elif pty == '3': # m = tree.find('select', attrs={'name': 'm'})
m = tree.find('select', attrs={'name': 'd'}) # if m:
if m: # m = m.find(selected=True)['value']
m = m.find(selected=True)['value'] # self._scanned[port] = (pty, m)
self._scanned[port] = (pty, m) # return pty, m
return pty, m # elif pty == '3':
# m = tree.find('select', attrs={'name': 'd'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m)
# return pty, m
# elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
# m = tree.find('select', attrs={'name': 'd'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m or '0')
# return pty, m or '0'
async def scan_ports(self,): async def scan_ports(self, nports=37):
for x in range(38): for x in range(0, nports+1):
ret = await self.scan_port(x) ret = await self.scan_port(x)
if ret: if ret:
yield [x, *ret] yield x, ret
self.nports = nports+1
async def _update_extender(self, port):
"""
Обновление mcp230, так же подходит для PCA9685
:param port:
:return:
"""
values = await self.request(pt=port, cmd='get')
ret = {}
for i, x in enumerate(values.split(';')):
ret[f'{port}e{i}'] = x
return ret
async def _update_i2c(self, params):
"""
Обновление портов i2c
:param params: параметры url
:return:
"""
_params = tuple(params.items())
ret = {
_params: await self.request(**params)
}
self.lg.debug('i2c response: %s', ret)
return ret
async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list))
ret['mqtt_id'] = await self.get_mqtt_id()
ret['extenders'] = extenders = []
ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = []
async for port, cfg in self.scan_ports(nports):
if cfg.pty == "0":
ret['binary_sensor'][port].append({})
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': cfg.m == '1'})
elif cfg == DS2413:
# ds2413
_data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
data = _data.get('value', {})
if not isinstance(data, dict):
self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}')
continue
for addr, state in data.items():
ret['light'][port].extend([
{"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'},
{"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'},
])
elif cfg == MCP230:
extenders.append(port)
if cfg.inta:
ext_int[int_ignore(cfg.inta)] = port
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page)
pt = f'{port}e{n}'
if ext_cfg.ety == '1':
ret['light'][pt].append({})
elif ext_cfg.ety == '0':
if ext_cfg.eact:
ext_acts[pt] = ext_cfg.eact
ret['binary_sensor'][pt].append({})
elif cfg == PCA9685:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and cfg.gr == '0':
# i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan')
self.lg.debug(f'find scan link: %s', scan)
if scan is not None:
page = await self.request(pt=port, cmd='scan')
req, parsed = parse_scan_page(page)
self.lg.debug(f'scan results: %s', (req, parsed))
ret['i2c'][port].extend(parsed)
i2c_sensors.extend(req)
elif cfg.pty in ('3', '2', '4'):
http_cmd = 'get'
if cfg.d == '5' and cfg.pty == '3':
# 1-wire bus
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
else:
values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
self.lg.debug(f'values: %s', values)
if values is None:
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
continue
if isinstance(values, dict) and 'value' in values:
values = values['value']
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES.get(cfg.m): values}
else:
values = {None: values}
for key in values:
self.lg.debug(f'add sensor {key}')
ret['sensor'][port].append(dict(
key=key,
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key,
http_cmd=http_cmd,
))
return ret
async def restore_states(self):
for x in self.entities:
if isinstance(x, MegaOutPort):
if x.is_on:
await x.async_turn_on(brightness=x.brightness)
else:
await x.async_turn_off()

View File

@@ -0,0 +1,109 @@
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
def parse_scan_page(page: str):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
dev = params.get('i2c_dev')
if dev is None:
continue
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
class Skip:
pass
class Request:
pass
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}

View File

@@ -1,6 +1,5 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
@@ -11,17 +10,23 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID CONF_ID,
CONF_DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from .entities import MegaOutPort
from .hub import MegaD from .hub import MegaD
from .const import CONF_DIMMER, CONF_SWITCH from .const import (
CONF_DIMMER,
CONF_SWITCH,
DOMAIN,
CONF_CUSTOM,
CONF_SKIP,
)
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -47,29 +52,7 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for lights, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config["dimmer"]:
if isinstance(x, int):
ent = MegaLight(
mega_id=mid, port=x, dimmer=True)
else:
ent = MegaLight(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=True
)
ents.append(ent)
for x in _config["switch"]:
if isinstance(x, int):
ent = MegaLight(
mega_id=mid, port=x, dimmer=False
)
else:
ent = MegaLight(
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=False
)
ents.append(ent)
add_entities(ents)
return True return True
@@ -77,79 +60,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
async def scan_ports(): for port, cfg in config_entry.data.get('light', {}).items():
async for port, pty, m in hub.scan_ports(): port = int_ignore(port)
if pty == "1" and m in ['0', '1']: c = customize.get(mid, {}).get(port, {})
light = MegaLight(mega_id=mid, port=port, dimmer=m == '1') if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
devices.append(light) continue
async_add_devices(devices) for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data)
asyncio.create_task(scan_ports()) light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
devices.append(light)
async_add_devices(devices)
class MegaLight(LightEntity, BaseMegaEntity): class MegaLight(MegaOutPort, LightEntity):
def __init__(
self,
dimmer=False,
*args, **kwargs
):
super().__init__(
*args, **kwargs
)
self._brightness = None
self._is_on = None
self.dimmer = dimmer
@property
def brightness(self):
if self._brightness is not None:
return self._brightness
if self._state:
return self._state.attributes.get("brightness")
@property @property
def supported_features(self): def supported_features(self):
return SUPPORT_BRIGHTNESS if self.dimmer else 0 return SUPPORT_BRIGHTNESS if self.dimmer else 0
@property
def is_on(self) -> bool:
if self._is_on is not None:
return self._is_on
return self._state == 'ON'
async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness or 255
if self.dimmer and brightness == 0:
cmd = 255
elif self.dimmer:
cmd = brightness
else:
cmd = 1
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = True
self._brightness = brightness
await self.async_update_ha_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0"
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = False
await self.async_update_ha_state()
def _update(self, payload: dict):
val = payload.get("value")
try:
val = int(val)
except Exception:
pass
if isinstance(val, int):
self._is_on = val
if val > 0:
self._brightness = val
else:
self._is_on = val == 'ON'

View File

@@ -10,11 +10,10 @@
"ssdp": [], "ssdp": [],
"zeroconf": [], "zeroconf": [],
"homekit": {}, "homekit": {},
"dependencies": [ "after_dependencies": ["mqtt"],
"mqtt"
],
"codeowners": [ "codeowners": [
"@andvikt" "@andvikt"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues" "issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v0.6.1b1"
} }

View File

@@ -1,5 +1,4 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@@ -11,18 +10,21 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import BaseMegaEntity from homeassistant.helpers.template import Template
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE
from .hub import MegaD from .hub import MegaD
import re import re
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)') TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)') HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -59,17 +61,11 @@ PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM) lg.warning('mega integration does not support yaml for sensors, please use UI configuration')
ents = []
for mid, _config in config.items():
for x in _config:
ent = _make_entity(mid, **x)
ents.append(ent)
add_entities(ents)
return True return True
def _make_entity(mid: str, port: int, conf: dict): def _make_entity(config_entry, mid: str, port: int, conf: dict):
key = conf[CONF_KEY] key = conf[CONF_KEY]
return Mega1WSensor( return Mega1WSensor(
key=key, key=key,
@@ -78,7 +74,8 @@ def _make_entity(mid: str, port: int, conf: dict):
patt=PATTERNS.get(key), patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]), device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key id_suffix=key,
config_entry=config_entry
) )
@@ -86,42 +83,48 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = _constructors[tp](
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
async def scan(): async_add_devices(devices)
async for port, pty, m in hub.scan_ports():
if pty == "3":
values = await hub.get_port(port)
lg.debug(f'values: %s', values)
if values is None:
continue
if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values}
elif not isinstance(values, dict):
values = {None: values}
for key in values:
hub.lg.debug(f'add sensor {W1}:{key}')
sensor = _make_entity(
mid=mid,
port=port,
conf={
CONF_TYPE: W1,
CONF_KEY: key,
})
devices.append(sensor)
hub.sensors.append(sensor)
async_add_devices(devices)
asyncio.create_task(scan())
class Mega1WSensor(BaseMegaEntity): class MegaI2C(MegaPushEntity):
def __init__(self, *args, device_class: str, params: dict, **kwargs):
self._device_class = device_class
self._params = tuple(params.items())
super().__init__(*args, **kwargs)
def device_class(self):
return self._device_class
@property
def state(self):
# self.lg.debug(f'get % all states: %', self._params, self.mega.values)
return self.mega.values.get(self._params)
@property
def device_class(self):
return self._device_class
class Mega1WSensor(MegaPushEntity):
def __init__( def __init__(
self, self,
unit_of_measurement, unit_of_measurement,
device_class, device_class,
patt=None,
key=None, key=None,
*args, *args,
**kwargs **kwargs
@@ -135,13 +138,21 @@ class Mega1WSensor(BaseMegaEntity):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._value = None self._value = None
self.key = key self.key = key
self.patt = patt
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
return self._unit_of_measurement _u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None)
if _u is None:
return self._unit_of_measurement
elif isinstance(_u, str):
return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._unit_of_measurement
@property @property
def unique_id(self): def unique_id(self):
@@ -152,29 +163,54 @@ class Mega1WSensor(BaseMegaEntity):
@property @property
def device_class(self): def device_class(self):
return self._device_class _u = self.customize.get(CONF_DEVICE_CLASS, None)
if _u is None:
@property return self._device_class
def should_poll(self): elif isinstance(_u, str):
return False return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._device_class
@property @property
def state(self): def state(self):
if self._value is None and self._state is not None: ret = None
return self._state.state if self.key:
return self._value try:
ret = self.mega.values.get(self.port, {})
def _update(self, payload: dict): if isinstance(ret, dict):
val = payload.get('value', '') ret = ret.get('value', {})
if isinstance(val, str) and self.patt is not None: if isinstance(ret, dict):
val = self.patt.findall(val) ret = ret.get(self.key)
if val: except:
self._value = val[0] self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
else: return
self.lg.warning(f'could not parse: {payload}')
elif isinstance(val, dict) and self.key is not None:
self._value = val.get(self.key)
elif isinstance(val, (float, int)):
self._value = val
else: else:
self.lg.warning(f'could not parse: {payload}') ret = self.mega.values.get(self.port, {}).get('value')
if ret is None and self._state is not None:
ret = self._state.state
try:
ret = float(ret)
ret = str(ret)
except:
ret = None
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
return ret
@property
def name(self):
n = super().name
c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n
_constructors = {
'sensor': Mega1WSensor,
'i2c': MegaI2C,
}

View File

@@ -15,7 +15,7 @@ get_port:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega" example: "mega"
port: port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу) description: Номер порта или список портов (если не заполнять, будут запрошены все порты сразу)
example: 1 example: 1
run_cmd: run_cmd:
@@ -25,9 +25,10 @@ run_cmd:
mega_id: mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega" example: "mega"
port:
description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
example: 1
cmd: cmd:
description: Любая поддерживаемая мегой команда description: Любая поддерживаемая мегой команда
example: "1:0" example: "1:0"
port:
description: (Deprecated, больше не нужен) Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
example: 1

View File

@@ -11,7 +11,16 @@
"mqtt_id": "[%key:common::config_flow::data::mqtt_id%]", "mqtt_id": "[%key:common::config_flow::data::mqtt_id%]",
"scan_interval": "[%key:common::config_flow::data::mqtt_id%]", "scan_interval": "[%key:common::config_flow::data::mqtt_id%]",
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]", "port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
"invert": "[%key:common::config_flow::data::invert%]" "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]",
"fake_response": "[%key:common::config_flow::data::fake_response%]",
"force_d": "[%key:common::config_flow::data::force_d%]",
"protected": "[%key:common::config_flow::data::protected%]",
"allow_hosts": "[%key:common::config_flow::data::allow_hosts%]",
"restore_on_restart": "[%key:common::config_flow::data::restore_on_restart%]",
"poll_outs": "[%key:common::config_flow::data::poll_outs%]"
} }
} }
}, },
@@ -32,7 +41,11 @@
"scan_interval": "[%key:common::config_flow::data::scan_interval%]", "scan_interval": "[%key:common::config_flow::data::scan_interval%]",
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]", "port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
"reload": "[%key:common::config_flow::data::reload%]", "reload": "[%key:common::config_flow::data::reload%]",
"invert": "[%key:common::config_flow::data::invert%]" "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]",
"poll_outs": "[%key:common::config_flow::data::poll_outs%]"
} }
} }
} }

View File

@@ -7,15 +7,20 @@ from homeassistant.components.switch import (
PLATFORM_SCHEMA as LIGHT_SCHEMA, PLATFORM_SCHEMA as LIGHT_SCHEMA,
SwitchEntity, SwitchEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_PORT, CONF_PORT,
CONF_ID,
CONF_DOMAIN,
) )
from .entities import BaseMegaEntity from homeassistant.core import HomeAssistant
from .const import CONF_DIMMER, CONF_SWITCH from . import hub as h
from .entities import MegaOutPort
from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .tools import int_ignore
_LOGGER = logging.getLogger(__name__) _LOGGER = lg = logging.getLogger(__name__)
# Validation of the user's configuration # Validation of the user's configuration
@@ -33,50 +38,29 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
config.pop(CONF_PLATFORM)
ents = []
for mid, _config in config.items():
mega = hass.data["mega"][mid]
for x in _config:
if isinstance(x, int):
ent = MegaSwitch(hass, mega=mega, port=x)
else:
ent = MegaSwitch(
hass, mega=mega, port=x[CONF_PORT], name=x[CONF_NAME]
)
ents.append(ent)
add_entities(ents) async def async_setup_platform(hass, config, add_entities, discovery_info=None):
lg.warning('mega integration does not support yaml for switches, please use UI configuration')
return True return True
class MegaSwitch(SwitchEntity, BaseMegaEntity): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
mid = config_entry.data[CONF_ID]
hub: 'h.MegaD' = hass.data['mega'][mid]
devices = []
def __init__(self, *args, **kwargs): customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
super().__init__(*args, **kwargs) for port, cfg in config_entry.data.get('light', {}).items():
self._is_on = None port = int_ignore(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'switch':
continue
for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data)
light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
devices.append(light)
async_add_devices(devices)
@property
def is_on(self) -> bool:
if self._is_on is not None:
return self._is_on
return self._state == 'ON'
async def async_turn_on(self, **kwargs) -> None: class MegaSwitch(MegaOutPort, SwitchEntity):
cmd = 1 pass
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = True
await self.async_update_ha_state()
async def async_turn_off(self, **kwargs) -> None:
cmd = "0"
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
self._is_on = False
await self.async_update_ha_state()
def _update(self, payload: dict):
val = payload.get("value")
self._is_on = val == 'ON'

View File

@@ -0,0 +1,117 @@
import asyncio
import itertools
from heapq import heappush
from contextlib import asynccontextmanager
_params = ['m', 'click', 'cnt', 'pt']
def make_ints(d: dict):
for x in _params:
try:
d[x] = int(d.get(x, 0))
except (ValueError, TypeError):
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0
def int_ignore(x):
try:
return int(x)
except (TypeError, ValueError):
return x
class PriorityLock(asyncio.Lock):
"""
You can acquire lock with some kind of priority in mind, so that locks with higher priority will be released first.
priority can be set with lck.acquire(1)
or by using context manager:
>>> lck = PriorityLock()
... async with lck(1):
... # do something
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cnt = itertools.count()
def __call__(self, priority=0):
return self._with_priority(priority)
@asynccontextmanager
async def _with_priority(self, p):
await self.acquire(p)
try:
yield
finally:
self.release()
async def acquire(self, priority=0) -> bool:
"""Acquire a lock.
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for _, _, w in self._waiters))):
self._locked = True
return True
if self._waiters is None:
self._waiters = []
fut = self._loop.create_future()
cnt = next(self._cnt)
heappush(self._waiters, (priority, cnt, fut))
# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
try:
await fut
finally:
self._waiters.remove((priority, cnt, fut))
except asyncio.exceptions.CancelledError:
if not self._locked:
self._wake_up_first()
raise
self._locked = True
return True
def release(self):
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
if self._locked:
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
def _wake_up_first(self):
"""Wake up the first waiter if it isn't done."""
if not self._waiters:
return
try:
_, _, fut = self._waiters[0]
except IndexError:
return
# .done() necessarily means that a waiter will wake up later on and
# either take the lock, or, if it was cancelled and lock wasn't
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)

View File

@@ -7,7 +7,8 @@
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error", "unknown": "Unexpected error",
"duplicate_id": "Duplicate ID" "duplicate_id": "Duplicate ID",
"mqtt_inputs": "Use MQTT"
}, },
"step": { "step": {
"user": { "user": {
@@ -17,8 +18,36 @@
"username": "Username", "username": "Username",
"id": "ID", "id": "ID",
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Scan interval (sec) (used for aliveness and sensors)", "scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)" "port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"nports": "Number of ports",
"update_all": "Update all outs when input",
"mqtt_inputs": "Use MQTT (Deprecated)",
"fake_response": "Fake response",
"force_d": "Force 'd' response",
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"reload": "Reload objects",
"mqtt_inputs": "Use MQTT (Deprecated)",
"update_all": "Update all outs when input",
"fake_response": "Fake response",
"force_d": "Force 'd' response",
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs"
} }
} }
} }

View File

@@ -17,8 +17,17 @@
"username": "Пользователь", "username": "Пользователь",
"id": "ID", "id": "ID",
"mqtt_id": "MQTT id", "mqtt_id": "MQTT id",
"scan_interval": "Периодичность обновлений (сек.)", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять",
"port_to_scan": "Порт, который сканируется когда нет датчиков" "port_to_scan": "Порт, который сканируется когда нет датчиков",
"mqtt_inputs": "Использовать MQTT (Не рекомендуется)",
"nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход",
"fake_response": "Имитация http-ответа",
"force_d": "Ответ 'd' по умолчанию",
"protected": "Блокировать неразрешенные соединения",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"allow_hosts": "Разрешенные ip (через ;)",
"poll_outs": "Обновлять выходы (регулярно)"
} }
} }
} }
@@ -27,10 +36,19 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"scan_interval": "Периодичность обновлений (сек.)", "scan_interval": "Периодичность обновлений (сек.), 0 - не обновлять",
"port_to_scan": "Порт, который сканируется когда нет датчиков", "port_to_scan": "Порт, который сканируется когда нет датчиков",
"reload": "Обновить объекты", "reload": "Обновить объекты",
"invert": "Список портов (через ,) с инвертированной логикой" "invert": "Список портов (через ,) с инвертированной логикой",
"mqtt_inputs": "Использовать MQTT (Не рекомендуется)",
"fake_response": "Имитация http-ответа",
"force_d": "Ответ 'd' по умолчанию",
"nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход",
"protected": "Блокировать неразрешенные соединения",
"allow_hosts": "Разрешенные ip (через ;)",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"poll_outs": "Обновлять выходы (регулярно)"
} }
} }
} }

View File

@@ -0,0 +1,57 @@
{
"config": {
"abort": {
"already_configured": "Вже налаштовано"
},
"error": {
"cannot_connect": "Неможливо підключитись",
"invalid_auth": "Неправильний пароль",
"unknown": "Невідома помилка",
"duplicate_id": "Дублікат ID"
},
"step": {
"user": {
"data": {
"host": "Хост",
"password": "Пароль",
"username": "Користувач",
"id": "ID",
"mqtt_id": "MQTT id",
"scan_interval": "Період оновлення (сек.), 0 - не оновлювати",
"port_to_scan": "Порт для сканування при відсутності датчиків",
"mqtt_inputs": "Використовувати MQTT (Deprecated)",
"nports": "Кількість портів",
"update_all": "Оновити всі виходи коли спрацьовує вхід",
"fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Період оновлення (сек.)",
"port_to_scan": "Порт для сканування при відсутності датчиків",
"reload": "Оновити об'єкти",
"invert": "Список портів з інвертованою логікою (через ,)",
"mqtt_inputs": "Використовувати MQTT (Deprecated)",
"nports": "Кількість портів",
"fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"update_all": "Оновити всі виходи коли спрацьовує вхід",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи"
}
}
}
},
"title": "mega"
}

113
readme.md
View File

@@ -1,25 +1,49 @@
# MegaD HomeAssistant integration # MegaD HomeAssistant integration
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
[![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/410013955329136)
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561) Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
Предложения по доработкам просьба писать в [discussions](https://github.com/andvikt/mega_hacs/discussions), о проблемах
создавать [issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
## Основные особенности: ## Основные особенности:
- Настройка в веб-интерфейсе + [yaml](https://github.com/andvikt/mega_hacs/wiki/Кастомизация)
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для температурных датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`) `sensor`)
- Возможность работы с несколькими megad - Возможность работы с несколькими megad
- Обратная связь по mqtt - Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad - Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
- Поддержка температурных датчиков в режиме шины будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
## Зависимости - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
**Важно!!** Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
Для максимальной совместимости необходимо обновить ваш контроллер до последней версии, тк были важные обновления в части - поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
mqtt - поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
[по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1)
## Установка ## Установка
Рекомендованнй способ с поддержкой обновлений - через [HACS](https://hacs.xyz/docs/installation/installation). Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
После установки перейти в меню HACS - Integrations - Explore, в поиске ищем MegaD
HACS - Integrations - Explore, в поиске ищем MegaD.
Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
Обновления выполняются так же в меню HACS.
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
Альтернативный способ установки: Альтернативный способ установки:
```shell ```shell
@@ -27,54 +51,29 @@ mqtt
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash - wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
``` ```
Не забываем перезагрузить HA Не забываем перезагрузить HA
## Устройства
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер
## Настройка из веб-интерфейса ## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega `Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
## Сервисы Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
Все сервисы доступны в меню разработчика с описанием и примерами использования
В самой меге необходимо прописать настройки:
```yaml ```yaml
mega.save: srv: "192.168.1.4:8123" # ip:port вашего HA
description: Сохраняет текущее состояние портов (?cmd=s) script: "mega" # это api интеграции, к которому будет обращаться контроллер
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
example: "mega"
mega.get_port:
description: Запросить текущий статус порта (или всех)
fields:
mega_id:
description: ID меги, можно оставить пустым, тогда будут порты всех зарегистрированных мег
example: "mega"
port:
description: Номер порта (если не заполнять, будут запрошены все порты сразу)
example: 1
mega.run_cmd:
description: Выполнить любую произвольную команду
fields:
mega_id:
description: ID меги
example: "mega"
port:
description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
example: 1
cmd:
description: Любая поддерживаемая мегой команда
example: "1:0"
``` ```
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
## Отладка При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose) объекты`
Просьба прикладывать детальный лог, который можно включить в конфиге так: ## Зависимости
```yaml Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
logger: настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
default: info [специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
logs:
custom_components.mega: debug Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
``` из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)