Compare commits

...

131 Commits

Author SHA1 Message Date
Andrey
5edf000ce8 tune restore 2021-03-05 12:38:14 +03:00
Andrey
b821d182b2 edit readme 2021-03-05 12:25:42 +03:00
Andrey
149d30e921 add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:45:41 +03:00
Andrey
84f677656a add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:02:49 +03:00
Andrey
a0900052dc fix bugs 2021-03-05 00:02:43 +03:00
Andrey
b8d355f412 fix bugs 2021-03-04 22:54:21 +03:00
Andrey
d3f76a88df fix bugs 2021-03-04 22:52:15 +03:00
Andrey
fa2bb2674e fix updater 2021-03-04 21:52:25 +03:00
Andrey
a1ae4e294b fix pop 2021-03-04 21:28:01 +03:00
Andrey
81d85ba1ed remove crap entities 2021-03-04 20:45:42 +03:00
Andrey
7135bb273e add sync_time 2021-03-04 20:25:59 +03:00
Andrey
7d8554a7aa add force_i2c, add new port namings 2021-03-04 19:45:37 +03:00
Andrey
40dcadc109 fix ptsensor 2021-03-04 16:52:07 +03:00
Andrey
35f99877ca add delay on ptsensor 2021-03-04 16:49:02 +03:00
Andrey
6c50b81bff remove scl 2021-03-04 16:22:33 +03:00
Andrey
c810693ba5 remove scl 2021-03-04 16:21:59 +03:00
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
24 changed files with 1355 additions and 472 deletions

3
.experiment.py Normal file
View File

@@ -0,0 +1,3 @@
import struct
print(struct.unpack('!f', bytes.fromhex('435c028f'))[0])

View File

@@ -8,19 +8,20 @@ assignees: ''
--- ---
**Описание** **Описание**
A clear and concise description of what the bug is. Описание проблемы
**Версии систем** **Версии систем**
Enviroment: raspberry/linux/windows/macos/docker Enviroment: raspberry/linux/windows/macos/docker
HA version: HA version:
mega_hacs version: mega_hacs version:
megad firmware version: megad firmware version:
используется mqtt: true/false
**Ожидаемое поведение** **Ожидаемое поведение**
A clear and concise description of what you expected to happen. Описание правильного поведения
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**LOG** **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

@@ -7,45 +7,75 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN, CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST 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.template import Template
from homeassistant.helpers import config_validation as cv 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, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \ 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_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, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT
from .hub import MegaD from .hub import MegaD
from .config_flow import ConfigFlow from .config_flow import ConfigFlow
from .http import MegaView from .http import MegaView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CUSTOMIZE_PORT = {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_NAME): vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
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,
vol.Optional(CONF_FORCE_I2C_SCAN): bool,
vol.Optional(CONF_HEX_TO_FLOAT): bool,
}
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: { DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str], vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Required(str, description='id меги из веб-интерфейса'): { vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(int, description='номер порта'): { vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(
vol.Optional(CONF_INVERT, default=False): bool, CONF_DEF_RESPONSE,
vol.Optional(CONF_NAME): vol.Any(str, { description='Ответ по умолчанию',
vol.Required(str): str default=None
}), ): vol.Any(cv.template, None),
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'), vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'): CUSTOMIZE_PORT,
vol.Any(str, { CUSTOMIZE_DS2413,
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,
}
} }
} }
}, },
@@ -63,6 +93,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
"""YAML-конфигурация содержит только кастомизации портов""" """YAML-конфигурация содержит только кастомизации портов"""
hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})} hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})}
hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(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, [])) view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
hass.http.register_view(view) hass.http.register_view(view)
hass.services.async_register( hass.services.async_register(
@@ -73,12 +104,12 @@ 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,
}) })
@@ -103,7 +134,7 @@ async def get_hub(hass, entry):
break break
if _mqtt is None: if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first') raise Exception('mqtt not configured, please configure mqtt first')
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop()) hub = MegaD(hass, config=entry, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id() hub.mqtt_id = await hub.get_mqtt_id()
return hub return hub
@@ -111,8 +142,9 @@ async def get_hub(hass, entry):
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry): async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry) hub = await get_hub(hass, entry)
hass.data[DOMAIN][id] = hass.data[DOMAIN]['__def'] = hub hub.fw = await hub.get_fw()
hass.data[DOMAIN][entry.data.get(CONF_HOST)] = hub 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()
@@ -142,31 +174,34 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
:param entry: :param entry:
:return: :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.get(CONF_PORT_TO_SCAN, 0) # hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options await hass.config_entries.async_reload(entry.entry_id)
for platform in PLATFORMS:
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: HomeAssistant, entry: ConfigEntry) -> 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)
hub: MegaD = hass.data[DOMAIN][id] hub: MegaD = hass.data[DOMAIN].get(id)
if hub is None: if hub is None:
return return
_LOGGER.debug(f'remove {id}') _LOGGER.debug(f'remove {id}')
_hubs.pop(entry.entry_id) _hubs.pop(id, None)
hass.data[DOMAIN].pop(id, None)
hass.data[DOMAIN][CONF_ALL].pop(id, None)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
task: asyncio.Task = _POLL_TASKS.pop(id, None) task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None: if task is not None:
task.cancel() task.cancel()
if hub is None: if hub is None:
return return
await hub.stop() await hub.stop()
return True
async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_migrate_entry(hass, config_entry: ConfigEntry):
@@ -205,27 +240,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): if not isinstance(hub, MegaD):
continue 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

@@ -16,10 +16,11 @@ from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP 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 .entities import MegaPushEntity
from .hub import MegaD from .hub import MegaD
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -50,12 +51,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('binary_sensor', {}).items(): for port, cfg in config_entry.data.get('binary_sensor', {}).items():
port = int(port) port = int_ignore(port)
c = customize.get(mid, {}).get(port, {}) c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False): if c.get(CONF_SKIP, False):
continue continue
hub.lg.debug(f'add binary_sensor on port %s', port) hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry) sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
if '<' in sensor.name:
continue
devices.append(sensor) devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
@@ -66,20 +69,48 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_on = None self._is_on = None
self._attrs = None self._attrs = None
self._click_task = None
async def _click(self):
await self.customize.get
@property @property
def state_attributes(self): def state_attributes(self):
return self._attrs 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:
val = self.mega.values.get(self.port, {}).get("value") \ val = self.mega.values.get(self.port, {})
or self.mega.values.get(self.port, {}).get('m') if isinstance(val, dict):
val = val.get("value", val.get('m'))
if val is None and self._state is not None: if val is None and self._state is not None:
return self._state == 'ON' return self._state == 'ON'
elif val is not None: elif val is not None:
return val == 'ON' or val == 1 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):
self.mega.values[self.port] = payload self.mega.values[self.port] = payload
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

@@ -10,7 +10,9 @@ 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, HomeAssistant from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \ from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
CONF_NPORTS, CONF_UPDATE_ALL # pylint:disable=unused-import CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME, \
REMOVE_CONFIG # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -18,14 +20,21 @@ _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=0): int, vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool, # 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_NPORTS, default=37): int,
# vol.Optional(CONF_UPDATE_ALL, default=True): bool, 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,
vol.Optional(CONF_UPDATE_TIME, default=True): bool,
}, },
) )
@@ -56,7 +65,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 = 4 VERSION = 23
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):
@@ -75,6 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await hub.stop() await hub.stop()
hub.lg.debug(f'config loaded: %s', config) hub.lg.debug(f'config loaded: %s', config)
config.update(user_input) config.update(user_input)
config['new_naming'] = True
return self.async_create_entry( return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]), title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config, data=config,
@@ -106,22 +116,20 @@ 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."""
new_naming = self.config_entry.data.get('new_naming', False)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD) reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data) cfg = dict(self.config_entry.data)
cfg.update(user_input) cfg.update(user_input)
hub = await get_hub(self.hass, self.config_entry.data) cfg['new_naming'] = new_naming
if reload: self.config_entry.data = cfg
await hub.start() await get_hub(self.hass, cfg)
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop() if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id)
await hub.reload(reload_entry=False)
_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=cfg, data=cfg,
@@ -131,11 +139,18 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int, vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 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_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool, 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_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_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_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)): bool,
# 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

@@ -16,14 +16,27 @@ CONF_INVERT = 'invert'
CONF_PORTS = 'ports' CONF_PORTS = 'ports'
CONF_CUSTOM = '__custom' CONF_CUSTOM = '__custom'
CONF_HTTP = '__http' CONF_HTTP = '__http'
CONF_ALL = '__all'
CONF_SKIP = 'skip' CONF_SKIP = 'skip'
CONF_MQTT_INPUTS = 'mqtt_inputs' CONF_MQTT_INPUTS = 'mqtt_inputs'
CONF_NPORTS = 'nports' CONF_NPORTS = 'nports'
CONF_RESPONSE_TEMPLATE = 'response_template' CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all' CONF_UPDATE_ALL = 'update_all'
CONF_FAKE_RESPONSE = 'fake_response'
CONF_GET_VALUE = 'get_value' CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts' 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'
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
CONF_UPDATE_TIME = 'update_time'
CONF_HEX_TO_FLOAT = 'hex_to_float'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",
@@ -31,4 +44,27 @@ PLATFORMS = [
"sensor", "sensor",
] ]
EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor' EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor'
PATT_SPLIT = re.compile('[;/]') 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(.+)\)')
REMOVE_CONFIG = [
'extenders',
'ext_in',
'ext_acts',
'i2c_sensors',
'binary_sensor',
'light',
'i2c',
'sensor',
]

View File

@@ -1,12 +1,32 @@
import logging import logging
import asyncio import asyncio
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import State from homeassistant.core import State
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from .hub import MegaD from . import hub as h
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT 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__)
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): class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@@ -17,14 +37,20 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
""" """
def __init__( def __init__(
self, self,
mega: MegaD, mega: 'h.MegaD',
port: int, port: int,
config_entry: ConfigEntry = None, 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) super().__init__(mega.updater)
self.http_cmd = http_cmd
self._state: State = None self._state: State = None
self.port = port self.port = port
self.config_entry = config_entry self.config_entry = config_entry
@@ -34,9 +60,21 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
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}" + \ _pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port
self._name = name or f"{mega.id}_{_pt}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None self._customize: dict = None
self.index = index
self.addr = addr
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property
def enabled(self):
if '<' in self.name:
return False
else:
return super().enabled
@property @property
def customize(self): def customize(self):
@@ -46,11 +84,16 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {} c = c.get(self._mega_id) or {}
c = c.get(self.port) 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 self._customize = c
return self._customize return self._customize
@property @property
def device_info(self): def device_info(self):
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
return { return {
"identifiers": { "identifiers": {
# Serial numbers are unique identifiers within a specific domain # Serial numbers are unique identifiers within a specific domain
@@ -59,10 +102,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
"config_entries": [ "config_entries": [
self.config_entry, self.config_entry,
], ],
"name": f'{self._mega_id} port {self.port}', "name": f'{self._mega_id} port {_pt}',
"manufacturer": 'ab-log.ru', "manufacturer": 'ab-log.ru',
# "model": self.light.productname, # "model": self.light.productname,
# "sw_version": self.light.swversion, "sw_version": self.mega.fw,
"via_device": (DOMAIN, self._mega_id), "via_device": (DOMAIN, self._mega_id),
} }
@@ -80,7 +123,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def name(self): def name(self):
c = self.customize.get(CONF_NAME) c = self.customize.get(CONF_NAME)
if not isinstance(c, str): if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}" _pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
c = self._name or f"{self.mega.id}_p{_pt}"
return c return c
@property @property
@@ -88,11 +132,15 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
return self._unique_id return self._unique_id
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
global _task_set_ev_on
await super().async_added_to_hass() await super().async_added_to_hass()
self._state = await self.async_get_last_state() self._state = await self.async_get_last_state()
if self.mega.mqtt_inputs and _task_set_ev_on is None:
_task_set_ev_on = asyncio.create_task(_set_events_on())
async def get_state(self): async def get_state(self):
if self.mega.mqtt is None: self.lg.debug(f'state is %s', self.state)
if not self.mega.mqtt_inputs:
self.async_write_ha_state() self.async_write_ha_state()
@@ -109,9 +157,59 @@ class MegaPushEntity(BaseMegaEntity):
def __update(self, value: dict): def __update(self, value: dict):
self._update(value) self._update(value)
if self.hass is None:
return
self.async_write_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)
self.is_first_update = False 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):
@@ -128,6 +226,7 @@ class MegaOutPort(MegaPushEntity):
def __init__( def __init__(
self, self,
dimmer=False, dimmer=False,
dimmer_scale=1,
*args, **kwargs *args, **kwargs
): ):
super().__init__( super().__init__(
@@ -136,6 +235,11 @@ class MegaOutPort(MegaPushEntity):
self._brightness = None self._brightness = None
self._is_on = None self._is_on = None
self.dimmer = dimmer 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 @property
def invert(self): def invert(self):
@@ -143,10 +247,24 @@ class MegaOutPort(MegaPushEntity):
@property @property
def brightness(self): def brightness(self):
val = self.mega.values.get(self.port, {}).get("value") if not self.dimmer:
if val is None and self._state is not None: 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") 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: elif val is not None:
val = val.get("value")
if val is None:
return
try: try:
val = int(val) val = int(val)
return val return val
@@ -155,41 +273,109 @@ class MegaOutPort(MegaPushEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}).get("value") val = self.mega.values.get(self.port, {})
if val is None and self._state is not None: if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state == 'ON' 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: 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: if not self.invert:
return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0) return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0)
else: else:
return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0) 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: async def async_turn_on(self, brightness=None, **kwargs) -> None:
brightness = brightness or self.brightness or 255 brightness = brightness or self.brightness or 255
self._brightness = brightness
if self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
cmd = 255 cmd = 255 * self.dimmer_scale
elif self.dimmer: elif self.dimmer:
cmd = brightness cmd = brightness * self.dimmer_scale
else: else:
cmd = 1 if not self.invert else 0 cmd = 1 if not self.invert else 0
await self.mega.send_command(self.port, f"{self.port}:{cmd}") _cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
self.mega.values[self.port] = {'value': 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() await self.get_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
cmd = "0" if not self.invert else "1" cmd = "0" if not self.invert else "1"
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
await self.mega.send_command(self.port, f"{self.port}:{cmd}") if self.addr:
self.mega.values[self.port] = {'value': cmd} _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() await self.get_state()
def safe_int(v): def safe_int(v):
if v in ['ON', 'OFF']: if v == 'ON':
return None return 1
elif v == 'OFF':
return 0
try: try:
return int(v) return int(v)
except ValueError: except (ValueError, TypeError):
return None 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

@@ -8,16 +8,21 @@ from aiohttp.web_request import Request
from aiohttp.web_response import Response from aiohttp.web_response import Response
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, CONF_HTTP, DOMAIN, CONF_CUSTOM, CONF_RESPONSE_TEMPLATE
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback, HomeAssistant from homeassistant.core import HomeAssistant
from . import hub 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') _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): class MegaView(HomeAssistantView):
"""Handle Yandex Smart Home unauthorized requests."""
url = '/mega' url = '/mega'
name = 'mega' name = 'mega'
@@ -25,32 +30,59 @@ class MegaView(HomeAssistantView):
def __init__(self, cfg: dict): def __init__(self, cfg: dict):
self._try = 0 self._try = 0
self.allowed_hosts = {'::1'} self.protected = True
self.allowed_hosts = {'::1', '127.0.0.1'}
self.notified_attempts = defaultdict(lambda : False)
self.callbacks = defaultdict(lambda: defaultdict(list)) self.callbacks = defaultdict(lambda: defaultdict(list))
self.templates: typing.Dict[str, typing.Dict[str, Template]] = { self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
mid: { mid: {
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE] pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
for pt in cfg[mid] for pt in cfg[mid]
if CONF_RESPONSE_TEMPLATE in cfg[mid][pt] if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg } for mid in cfg if isinstance(cfg[mid], dict)
} }
_LOGGER.debug('templates: %s', self.templates) _LOGGER.debug('templates: %s', self.templates)
self.hubs = {}
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
auth = False _LOGGER.debug('request from %s %s', request.remote, request.headers)
for x in self.allowed_hosts:
if request.remote.startswith(x):
auth = True
break
if not auth:
_LOGGER.warning(f'unauthorised attempt to connect from {request.remote}')
return Response(status=401)
hass: HomeAssistant = request.app['hass'] hass: HomeAssistant = request.app['hass']
hub: 'hub.MegaD' = hass.data.get(DOMAIN).get(request.remote) # TODO: проверить какой remote if self.protected:
if hub is None and request.remote == '::1': auth = False
hub = hass.data.get(DOMAIN).get('__def') for x in self.allowed_hosts:
if hub is None: 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']:
try:
hub = list(self.hubs.values())[0]
except IndexError:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
return Response(status=400)
elif hub is None:
return Response(status=400) return Response(status=400)
data = dict(request.query) data = dict(request.query)
hass.bus.async_fire( hass.bus.async_fire(
@@ -59,36 +91,82 @@ class MegaView(HomeAssistantView):
) )
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote) _LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data) make_ints(data)
if data.get('st') == '1':
hass.async_create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
ret = 'd' 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 port is not None:
for cb in self.callbacks[hub.id][port]: if is_ext(data):
cb(data) # ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
template: Template = self.templates.get(hub.id, {}).get(port) if port in hub.extenders:
if hub.update_all: 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)) asyncio.create_task(self.later_update(hub))
if template is not None:
template.hass = hass
ret = template.async_render(data)
_LOGGER.debug('response %s', ret) _LOGGER.debug('response %s', ret)
ret = Response(body=ret or 'd', content_type='text/plain', headers={}) Response(body='' if hub.fake_response else ret, content_type='text/plain')
ret.headers.clear()
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 return ret
async def later_restore(self, hub):
"""
Восстановление всех выходов с небольшой задержкой. Задержка нужна чтобы ответ прошел успешно
:param hub:
:return:
"""
await asyncio.sleep(0.2)
if hub.restore_on_restart:
await hub.restore_states()
await hub.reload()
async def later_update(self, hub): async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1) await asyncio.sleep(1)
await hub.updater.async_refresh() _LOGGER.debug('force update')
await hub.updater.async_refresh()
def make_ints(d: dict):
for x in d:
try:
d[x] = float(d[x])
except ValueError:
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0

View File

@@ -10,31 +10,53 @@ import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY from homeassistant.config_entries import ConfigEntry
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM, PATT_SPLIT, DOMAIN, CONF_HTTP from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
from .exceptions import CannotConnect, MqttNotConfigured from .const import (
from .http import MegaView TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN,
REMOVE_CONFIG
)
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\.]+)') TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)') HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PRESS_PATT = re.compile(r'press:([01234567890\.]+)')
LUX_PATT = re.compile(r'lux:([01234567890\.]+)')
PATTERNS = { PATTERNS = {
TEMP: TEMP_PATT, TEMP: TEMP_PATT,
HUM: HUM_PATT, HUM: HUM_PATT,
PRESS: PRESS_PATT,
LUX: LUX_PATT
} }
UNITS = { UNITS = {
TEMP: '°C', TEMP: TEMP_CELSIUS,
HUM: '%' HUM: PERCENTAGE,
PRESS: 'mmHg',
LUX: LIGHT_LUX
} }
CLASSES = { CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE, TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY 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 NoPort(Exception):
pass
class MegaD: class MegaD:
@@ -49,24 +71,50 @@ class MegaD:
mqtt: mqtt.MQTT, mqtt: mqtt.MQTT,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
config: ConfigEntry = None,
mqtt_inputs: bool = True, 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,
nports=38, nports=38,
inverted: typing.List[int] = None, update_all: bool=True,
update_all=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,
new_naming=False,
update_time=False,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
self.config = config
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False: if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP) self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None: if not self.http is None:
self.http.allowed_hosts |= {host} 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: else:
self.http = None self.http = None
self.new_naming = new_naming
self.extenders = extenders or []
self.ext_in = ext_in or {}
self.ext_act = ext_acts or {}
self.i2c_sensors = i2c_sensors or []
self._update_time = update_time
self.poll_outs = poll_outs
self.update_all = update_all if update_all is not None else True self.update_all = update_all if update_all is not None else True
self.nports = nports self.nports = nports
self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
@@ -75,11 +123,13 @@ class MegaD:
self.mqtt = mqtt self.mqtt = mqtt
self.id = id self.id = id
self.lck = asyncio.Lock() self.lck = asyncio.Lock()
self._http_lck = asyncio.Lock() self.last_long = {}
self._http_lck = PriorityLock()
self._notif_lck = asyncio.Lock() self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition() 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.subs = None self.subs = None
self.lg: logging.Logger = lg.getChild(self.id) self.lg: logging.Logger = lg.getChild(self.id)
@@ -89,25 +139,40 @@ class MegaD:
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._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list)
self._loop = loop self._loop = loop
self._customize = None
self.values = {} self.values = {}
self.last_port = None self.last_port = None
self.updater = DataUpdateCoordinator( self.updater = DataUpdateCoordinator(
hass, hass,
self.lg, self.lg,
name="sensors", name="megad",
update_method=self.poll, update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None, update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
) )
self.updaters = []
self.fw = ''
self.notifiers = defaultdict(asyncio.Condition) 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.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): async def start(self):
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
if self.mqtt is not None: if self.mqtt is not None:
set_events_off()
self.subs = await self.mqtt.async_subscribe( self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+", topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg, msg_callback=self._process_msg,
@@ -135,6 +200,22 @@ class MegaD:
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd) await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
ports.append(x.port) 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 @property
def is_online(self): def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10) return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
@@ -156,20 +237,49 @@ class MegaD:
) )
self.online = True 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.lg.debug('poll') self.lg.debug('poll')
if self._update_time:
await self.update_time()
for x in self.i2c_sensors:
if not isinstance(x, dict):
continue
ret = await self._update_i2c(x)
if isinstance(ret, dict):
self.values.update(ret)
for x in self.extenders:
ret = await self._update_extender(x)
if not isinstance(ret, dict):
self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue
self.values.update(ret)
if self.mqtt is None: if self.mqtt is None:
await self.get_all_ports() await self.get_all_ports()
await self.get_sensors(only_list=True) await self.get_sensors(only_list=True)
return elif self.poll_outs:
if len(self.sensors) > 0: await self.get_all_ports(check_skip=True)
elif len(self.sensors) > 0:
await self.get_sensors() await self.get_sensors()
else: # else:
await self.get_port(self.port_to_scan) # await self.get_port(self.port_to_scan)
await self._get_ds2413()
return self.values return self.values
async def get_mqtt_id(self): async def get_mqtt_id(self):
@@ -183,14 +293,20 @@ 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):
return await self.request(pt=port, cmd=cmd) return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs): async def request(self, priority=0, **kwargs):
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None]) cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}/?{cmd}" url = f"http://{self.host}/{self.sec}"
if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url) self.lg.debug('request: %s', url)
async with self._http_lck: 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())
@@ -203,14 +319,21 @@ class MegaD:
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
def parse_response(self, ret): def parse_response(self, ret, cmd='get'):
if ret is None: if ret is None:
raise NoPort() raise NoPort()
if 'busy' in ret:
return None
if ':' in ret: if ':' in ret:
ret = PATT_SPLIT.split(ret) if ';' in ret:
ret = dict([ 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 x.split(':') for x in ret if x.count(':') == 1
]) ])}
elif 'ON' in ret: elif 'ON' in ret:
ret = {'value': 'ON'} ret = {'value': 'ON'}
elif 'OFF' in ret: elif 'OFF' in ret:
@@ -219,20 +342,23 @@ class MegaD:
ret = {'value': ret} ret = {'value': ret}
return ret return ret
async def get_port(self, port, force_http=False, http_cmd='get'): async def get_port(self, port, force_http=False, http_cmd='get', conv=True):
""" """
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values хранилище values
""" """
self.lg.debug(f'get port %s', port) self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http: if self.mqtt is None or force_http:
ret = await self.request(pt=port, cmd=http_cmd) if http_cmd == 'list' and conv:
ret = self.parse_response(ret) await self.request(pt=port, cmd='conv')
self.lg.debug('parsed: %s', ret)
if http_cmd == 'list' and isinstance(ret, dict) and 'value' in ret:
await asyncio.sleep(1) await asyncio.sleep(1)
ret = await self.request(pt=port, http_cmd=http_cmd) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ret = self.parse_response(ret) 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 self.values[port] = ret
return ret return ret
@@ -251,15 +377,26 @@ class MegaD:
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.lg.error(f'timeout when getting port {port}') self.lg.error(f'timeout when getting port {port}')
async def get_all_ports(self): @property
def ports(self):
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: if not self.mqtt_inputs:
ret = await self.request(cmd='all') ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')): 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) ret = self.parse_response(x)
self.values[port] = ret self.values[port] = ret
else: elif not check_skip:
for x in range(self.nports + 1): for x in range(self.nports + 1):
await self.get_port(x) 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()
@@ -280,7 +417,7 @@ class MegaD:
if port == 'cmd': if port == 'cmd':
return return
try: try:
port = int(port) port = int_ignore(port)
except: except:
self.lg.warning('can not process %s', msg) self.lg.warning('can not process %s', msg)
return return
@@ -290,9 +427,18 @@ class MegaD:
value = None value = None
try: try:
value = json.loads(msg.payload) value = json.loads(msg.payload)
if isinstance(value, dict):
make_ints(value)
self.values[port] = value self.values[port] = value
for cb in self._callbacks[port]: for cb in self._callbacks[port]:
cb(value) 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: except Exception as exc:
self.lg.warning(f'could not parse json ({msg.payload}): {exc}') self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return return
@@ -300,7 +446,7 @@ class MegaD:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop) asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
def subscribe(self, port, callback): def subscribe(self, port, callback):
port = int(port) port = int_ignore(port)
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, callback f'subscribe %s %s', port, callback
) )
@@ -326,64 +472,121 @@ class MegaD:
return await req.text() return await req.text()
async def scan_port(self, port): async def scan_port(self, port):
async with self.lck: data = await self.request(pt=port)
if port in self._scanned: return parse_config(data)
return self._scanned[port]
url = f'http://{self.host}/{self.sec}/?pt={port}'
self.lg.debug(
f'scan port %s: %s', port, url
)
async with aiohttp.request('get', url) as req:
html = await req.text()
if req.status != 200:
return
tree = BeautifulSoup(html, features="lxml")
pty = tree.find('select', attrs={'name': 'pty'})
if pty is None:
return
else:
pty = pty.find(selected=True)
if pty:
pty = pty['value']
else:
return
if pty in ['0', '1']:
m = tree.find('select', attrs={'name': 'm'})
if m:
m = m.find(selected=True)['value']
self._scanned[port] = (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
async def scan_ports(self, nports=37): async def scan_ports(self, nports=37):
for x in range(0, nports+1): 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 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())
delay = None
if 'delay' in params:
delay = params.pop('delay')
ret = {
_params: await self.request(**params)
}
self.lg.debug('i2c response: %s', ret)
if delay:
self.lg.debug('delay %s', delay)
await asyncio.sleep(delay)
return ret
async def get_config(self, nports=37): async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list)) ret = defaultdict(lambda: defaultdict(list))
async for port, pty, m in self.scan_ports(nports): ret['mqtt_id'] = await self.get_mqtt_id()
if pty == "0": 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):
_cust = self.customize.get(port)
if not isinstance(_cust, dict):
_cust = {}
if cfg.pty == "0":
ret['binary_sensor'][port].append({}) ret['binary_sensor'][port].append({})
elif pty == "1" and (m in ['0', '1', '3'] or m is None): elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': m == '1'}) ret['light'][port].append({'dimmer': cfg.m == '1'})
elif pty == '3': elif cfg == DS2413:
try: # ds2413
http_cmd = 'get' _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 not self.new_naming else f'{port:02}e{n:02}'
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)):
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)):
# 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 == '4' and cfg.m == '2':
# scl исключаем из сканирования
continue
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) values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')): 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') values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list' http_cmd = 'list'
except asyncio.TimeoutError:
self.lg.warning(f'timout on port {port}')
continue
self.lg.debug(f'values: %s', values) self.lg.debug(f'values: %s', values)
if values is None: if values is None:
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it') self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
@@ -393,7 +596,10 @@ class MegaD:
if isinstance(values, str) and TEMP_PATT.search(values): if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values} values = {TEMP: values}
elif not isinstance(values, dict): elif not isinstance(values, dict):
values = {None: values} 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: for key in values:
self.lg.debug(f'add sensor {key}') self.lg.debug(f'add sensor {key}')
ret['sensor'][port].append(dict( ret['sensor'][port].append(dict(
@@ -405,4 +611,27 @@ class MegaD:
)) ))
return ret 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()
async def update_time(self):
await self.request(
cf=7,
stime=datetime.now().strftime('%H:%M:%S')
)
async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports)
self.lg.debug(f'new config: %s', new)
cfg = dict(self.config.data)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
self.config.data = cfg
if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id)

View File

@@ -0,0 +1,118 @@
from dataclasses import dataclass, field
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, Request):
if c.delay:
params = params.copy()
params['delay'] = c.delay
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
@dataclass
class Request:
delay: float = None
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': [
Skip,
Request(delay=1), # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
]
}

View File

@@ -26,6 +26,7 @@ from .const import (
CONF_CUSTOM, CONF_CUSTOM,
CONF_SKIP, CONF_SKIP,
) )
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
@@ -61,13 +62,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items(): for port, cfg in config_entry.data.get('light', {}).items():
port = int(port) port = int_ignore(port)
c = customize.get(mid, {}).get(port, {}) c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light': if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data) hub.lg.debug(f'add light on port %s with data %s', port, data)
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data) light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)

View File

@@ -14,5 +14,6 @@
"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.3b1"
} }

View File

@@ -1,6 +1,7 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import voluptuous as vol import voluptuous as vol
import struct
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA, PLATFORM_SCHEMA as SENSOR_SCHEMA,
@@ -13,14 +14,18 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
from .entities import MegaPushEntity from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT
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\.]+)')
@@ -79,21 +84,54 @@ 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 port, cfg in config_entry.data.get('sensor', {}).items(): for tp in ['sensor', 'i2c']:
port = int(port) for port, cfg in config_entry.data.get(tp, {}).items():
for data in cfg: port = int_ignore(port)
hub.lg.debug(f'add sensor on port %s with data %s', port, data) for data in cfg:
sensor = Mega1WSensor( hub.lg.debug(f'add sensor on port %s with data %s', port, data)
mega=hub, sensor = _constructors[tp](
port=port, mega=hub,
config_entry=config_entry, port=port,
**data, config_entry=config_entry,
) **data,
devices.append(sensor) )
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
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)
ret = self.mega.values.get(self._params)
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex('41973333'))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
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 device_class(self):
return self._device_class
class Mega1WSensor(MegaPushEntity): class Mega1WSensor(MegaPushEntity):
def __init__( def __init__(
@@ -101,7 +139,6 @@ class Mega1WSensor(MegaPushEntity):
unit_of_measurement, unit_of_measurement,
device_class, device_class,
key=None, key=None,
http_cmd='get',
*args, *args,
**kwargs **kwargs
): ):
@@ -117,7 +154,6 @@ class Mega1WSensor(MegaPushEntity):
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) self.mega.sensors.append(self)
self.http_cmd = http_cmd
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@@ -140,7 +176,15 @@ class Mega1WSensor(MegaPushEntity):
@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:
return self._device_class
elif isinstance(_u, str):
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):
@@ -149,7 +193,9 @@ class Mega1WSensor(MegaPushEntity):
try: try:
ret = self.mega.values.get(self.port, {}) ret = self.mega.values.get(self.port, {})
if isinstance(ret, dict): if isinstance(ret, dict):
ret = ret.get(self.key) ret = ret.get('value', {})
if isinstance(ret, dict):
ret = ret.get(self.key)
except: except:
self.lg.error(self.mega.values.get(self.port, {}).get('value', {})) self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
return return
@@ -162,6 +208,15 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret) ret = str(ret)
except: except:
ret = None ret = None
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
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 return ret
@property @property
@@ -170,4 +225,10 @@ class Mega1WSensor(MegaPushEntity):
c = self.customize.get(CONF_NAME, {}) c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict): if isinstance(c, dict):
c = c.get(self.key) c = c.get(self.key)
return c or n 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

@@ -14,7 +14,15 @@
"invert": "[%key:common::config_flow::data::invert%]", "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]", "nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]" "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%]",
"update_time": "[%key:common::config_flow::data::update_time%]"
} }
} }
}, },
@@ -38,7 +46,8 @@
"invert": "[%key:common::config_flow::data::invert%]", "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]", "nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]" "update_all": "[%key:common::config_flow::data::update_all%]",
"poll_outs": "[%key:common::config_flow::data::poll_outs%]"
} }
} }
} }

View File

@@ -15,9 +15,10 @@ from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import MegaD from . import hub as h
from .entities import MegaOutPort from .entities import MegaOutPort
from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .tools import int_ignore
_LOGGER = lg = logging.getLogger(__name__) _LOGGER = lg = logging.getLogger(__name__)
@@ -45,18 +46,20 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: 'h.MegaD' = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items(): for port, cfg in config_entry.data.get('light', {}).items():
port = int(port) port = int_ignore(port)
c = customize.get(mid, {}).get(port, {}) c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'switch': if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'switch':
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data) 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) light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)

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

@@ -22,7 +22,14 @@
"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", "nports": "Number of ports",
"update_all": "Update all outs when input", "update_all": "Update all outs when input",
"mqtt_inputs": "Use MQTT" "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",
"update_time": "Sync time"
} }
} }
} }
@@ -34,8 +41,15 @@
"scan_interval": "Scan interval (sec), 0 - don't update", "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)",
"reload": "Reload objects", "reload": "Reload objects",
"mqtt_inputs": "Use MQTT", "mqtt_inputs": "Use MQTT (Deprecated)",
"update_all": "Update all outs when input" "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",
"update_time": "Sync time"
} }
} }
} }

View File

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

View File

@@ -17,8 +17,18 @@
"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 (Deprecated)",
"nports": "Кількість портів",
"update_all": "Оновити всі виходи коли спрацьовує вхід",
"fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи",
"update_time": "Осинхронізувати час"
} }
} }
} }
@@ -27,10 +37,19 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"scan_interval": "Період оновлення (сек.)", "scan_interval": "Період оновлення (сек.)",
"port_to_scan": "Порт для сканування при відсутності датчиків", "port_to_scan": "Порт для сканування при відсутності датчиків",
"reload": "Оновити об'єкти", "reload": "Оновити об'єкти",
"invert": "Список портів з інвертованою логікою (через ,)" "invert": "Список портів з інвертованою логікою (через ,)",
"mqtt_inputs": "Використовувати MQTT (Deprecated)",
"nports": "Кількість портів",
"fake_response": "Імітація http-відповіді",
"force_d": "Неявно відповідати 'd'",
"update_all": "Оновити всі виходи коли спрацьовує вхід",
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи"
} }
} }
} }

205
readme.md
View File

@@ -1,24 +1,51 @@
# 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 - Настройка в веб-интерфейсе + [yaml](https://github.com/andvikt/mega_hacs/wiki/Кастомизация)
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`) `sensor`)
- Возможность работы с несколькими megad - Возможность работы с несколькими megad
- Обратная связь по mqtt или http (на выбор) - Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
- События на двойные/долгие нажатия будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Автоматическое добавление/изменение объектов после перезагрузки контроллера
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей. выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка 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
# из папки с конфигом # из папки с конфигом
@@ -31,173 +58,23 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса. Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml
# configuration.yaml
mega:
hello: # ID меги, как в UI
7: # номер порта
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
14:
name: какой-то датчик
unit_of_measurement: "°C" # если датчик один, то просто строчкой
```
## Зависимости
Для совместимости c mqtt необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/)
в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt
## HTTP in
Начиная с версии `0.3.1` интеграция стала поддерживать обратную связь без mqtt, используя http-сервер. Для этого в настройках
интеграции необходимо снять галку с `использовать mqtt`
В самой меге необходимо прописать настройки: В самой меге необходимо прописать настройки:
```yaml ```yaml
srv: "192.168.1.4:8123" # ip:port вашего HA srv: "192.168.1.4:8123" # ip:port вашего HA
script: "mega" # это api интеграции, к которому будет обращаться контроллер script: "mega" # это api интеграции, к которому будет обращаться контроллер
``` ```
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor`. При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
События можно обрабатывать так: объекты`
```yaml
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
action:
- service: light.toggle
entity_id: light.some_light
```
Для binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться событиями.
## Ответ на входящие события от контроллера ## Зависимости
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
запустить то что прописано в поле act в настройках порта). настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, value) из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
Примеры:
```yaml
mega:
mega1: # id меги, который вы сами придумываете в конфиге в UI
4: # номер порта, с которого ожидаются события
response_template: 5:2 # простейший пример без шаблона. Каждый раз когда будет приходить сообщение на этот порт,
# будем менять состояние на противоположное
5:
# пример с использованием шаблона, порт 1 будет выключен если он сейчас включен и включен с последней сохраненной
# яркостью если он сейчас выключен
response_template: >-
{% if is_state('light.some_port_1', 'on') %}
1:0
{% else %}
1:{{state_attr('light.some_port_1', 'brightness')}}
{% endif %}
6:
# в шаблон так же передаются все параметры, которые передает контроллер (pt, cnt, m, click)
# эти параметры можно использовать в условиях или непосредственно в шаблоне в виде {{pt}}
response_template: >-
{% if m==2 %}1:0{% else %}d{% endif %}
```
## Отладка ответов
Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли
HA:
```shell
curl -v -X GET 'http://localhost:8123/mega?pt=5&m=1'
```
Если доступа нет, нужно в файл конфигурации добавить ip компьюетра, с которого вы хотите делать запросы, например:
```yaml
mega:
allow_hosts:
- 192.168.1.1
```
И тогда можно с локальной машины делать запросы на ваш сервер HA:
```shell
curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1'
```
В ответ будет приходить либо `d`, либо скрипт, который вы настроили
## События Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
`binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать
только с режимом входа P&R
При каждом срабатывании `binary_sensor` так же сообщает о событии типа `mega.sensor`.
События можно использовать в автоматизациях, например так:
```yaml
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
cnt: 2
action:
- service: light.toggle
entity_id: light.some_light
```
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки.
## Сервисы
Все сервисы доступны в меню разработчика с описанием и примерами использования
```yaml
mega.save:
description: Сохраняет текущее состояние портов (?cmd=s)
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"
```
## Отладка
Интеграция находится в активной разработке, при возникновении проблем [заводите issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
Просьба прикладывать детальный лог, который можно включить в конфиге так:
```yaml
logger:
default: info
logs:
custom_components.mega: debug
```