Compare commits

...

58 Commits

Author SHA1 Message Date
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
20 changed files with 621 additions and 465 deletions

View File

@@ -1,61 +1,43 @@
import asyncio import re
from urllib.parse import urlparse, parse_qsl
from asyncio import Event, FIRST_COMPLETED
import signal
import typing
from logging import getLogger, DEBUG
stop = Event() PATT_FW = re.compile(r'fw:\s(.+)\)')
loop = asyncio.get_event_loop() data = """
lg = getLogger(__name__) <html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
lg.setLevel(DEBUG) <head></head>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
MegaD-2561 by
def make_handler(get_ans: typing.Callable[[dict], str]): <a href="http://ab-log.ru">ab-log.ru</a>
(fw: 4.48b7)
async def handler( <br>
reader: asyncio.StreamReader, <a href="/sec/?cf=1">Config</a>
writer: asyncio.StreamWriter, <br>
): -- MODS --
data = await reader.read(200) <br>
print(data) <a href="/sec/?cf=3">XP1</a>
message = data.decode() <br>
addr = writer.get_extra_info('peername') <a href="/sec/?cf=4">XP2</a>
lg.debug('process msg "%s" from %s', message, addr) <br>
try: -- XT2 --
(_, p, *_) = message.split(' ') <br>
p = dict(parse_qsl(urlparse(p).query)) <a href="/sec/?pt=30">P30 - OUT</a>
lg.debug('query %s', p) <br>
ans = get_ans(p) <a href="/sec/?pt=31">P31 - OUT</a>
ans = f'''HTTP/1.1 200 OK\nContent-Length: {len(ans)}\n\n{ans}'''.encode() # \nContent-Length: 6 <br>
ans = b'HTTP/1.1 200 OK\r\n\r\n7:2' <a href="/sec/?pt=32">P32 - IN</a>
print(ans) <br>
except Exception as exc: <a href="/sec/?pt=33">P33 - I2C/SCL</a>
print(exc) <br>
lg.exception('process msg "%s" from %s', message, addr) <a href="/sec/?pt=34">P34 - DS</a>
ans = '''HTTP/1.1 500\n\n'''.encode() <br>
writer.write(ans) <a href="/sec/?pt=35">P35 - NC</a>
await writer.drain() <br>
# writer.transport.close() -- XP5/6 --
writer.close() <br>
await writer.wait_closed() <a href="/sec/?pt=36">P36 - ADC</a>
return handler <br>
<a href="/sec/?pt=37">P37 - NC</a>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
async def serve(): <html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
server = await asyncio.start_server( """
make_handler(lambda x: '7:2'), print(PATT_FW.search(data).groups()[0])
host='0.0.0.0',
port=1111,
)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await asyncio.wait((server.serve_forever(), stop.wait()), return_when=FIRST_COMPLETED)
if __name__ == '__main__':
loop.add_signal_handler(
signal.SIGINT, stop.set
)
loop.run_until_complete(serve())

View File

@@ -24,10 +24,4 @@ megad firmware version:
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/Отладка)
```yaml
logger:
default: info
logs:
custom_components.mega: debug
```

View File

@@ -7,24 +7,23 @@ 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
) )
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_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE
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.Schema({ CUSTOMIZE_PORT = {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Optional(CONF_INVERT, default=False): bool, vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_NAME): vol.Any(str, { vol.Optional(CONF_NAME): vol.Any(str, {
@@ -41,20 +40,33 @@ CUSTOMIZE_PORT = vol.Schema({
'сообщение из меги '): cv.template, 'сообщение из меги '): cv.template,
vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано
vol.Optional(CONF_GET_VALUE, default=True): bool, vol.Optional(CONF_GET_VALUE, default=True): bool,
vol.Optional(CONF_CONV_TEMPLATE): cv.template vol.Optional(CONF_CONV_TEMPLATE): cv.template,
}) vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
CUSTOMIZE_DS2413 = vol.Schema({ }
CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT 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.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Required(str, description='id меги из веб-интерфейса'): { vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool, vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional(int, description='номер порта'): vol.Any( vol.Optional(
CONF_DEF_RESPONSE,
description='Ответ по умолчанию',
default=None
): vol.Any(cv.template, None),
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
CUSTOMIZE_PORT, CUSTOMIZE_PORT,
CUSTOMIZE_DS2413, CUSTOMIZE_DS2413,
) )
@@ -124,8 +136,8 @@ 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 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")
@@ -170,7 +182,7 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
async def async_remove_entry(hass, entry) -> None: async def async_remove_entry(hass, entry) -> None:
"""Handle removal of an entry.""" """Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
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}')

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT, CONF_RESPONSE_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__)
@@ -51,7 +51,7 @@ 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
@@ -67,6 +67,10 @@ 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):
@@ -78,14 +82,15 @@ class MegaBinarySensor(BinarySensorEntity, 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, {})
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:
if val in ['ON', 'OFF']: if val in ['ON', 'OFF', '1', '0']:
return val == 'ON' if not self.invert else val == 'OFF' return val in ['ON', '1'] if not self.invert else val in ['OFF', '0']
else: elif isinstance(val, int):
return val != 1 if not self.invert else val == 1 return val != 1 if not self.invert else val == 1
def _update(self, payload: dict): def _update(self, payload: dict):

View File

@@ -10,7 +10,8 @@ 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, CONF_POLL_OUTS # 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 # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -24,9 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_SCAN_INTERVAL, default=0): int, vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool, vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool, 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,
}, },
) )
@@ -57,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega.""" """Handle a config flow for mega."""
VERSION = 5 VERSION = 16
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):
@@ -107,12 +113,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options."""
hub = await get_hub(self.hass, self.config_entry.data)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD) 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) hub = await get_hub(self.hass, cfg)
if reload: if reload:
await hub.start() await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37)) new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
@@ -138,6 +144,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
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_INVERT, default=''): str, # vol.Optional(CONF_INVERT, default=''): str,
}), }),
) )

View File

@@ -0,0 +1,58 @@
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)
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)
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

@@ -23,11 +23,17 @@ 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_CONV_TEMPLATE = 'conv_template'
CONF_POLL_OUTS = 'poll_outs' CONF_POLL_OUTS = 'poll_outs'
CONF_FORCE_D = 'force_d' CONF_FORCE_D = 'force_d'
CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart'
CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",
@@ -46,3 +52,5 @@ PRESS = 'press'
LUX = 'lux' LUX = 'lux'
SINGLE_CLICK = 'single' SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double' DOUBLE_CLICK = 'double'
PATT_FW = re.compile(r'fw:\s(.+)\)')

View File

@@ -48,6 +48,9 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
index=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
@@ -60,9 +63,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._name = name or f"{mega.id}_{port}" + \ self._name = name or f"{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None self._customize: dict = None
self.http_cmd = http_cmd
self.index = index self.index = index
self.addr = addr self.addr = addr
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property @property
def customize(self): def customize(self):
@@ -92,7 +96,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
"name": f'{self._mega_id} port {self.port}', "name": f'{self._mega_id} port {self.port}',
"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),
} }
@@ -125,7 +129,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
_task_set_ev_on = asyncio.create_task(_set_events_on()) _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()
@@ -209,6 +214,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__(
@@ -217,10 +223,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 # @property
def assumed_state(self) -> bool: # def assumed_state(self) -> bool:
return True if self.index is not None or self.mega.mqtt is None else False # return True if self.index is not None or self.mega.mqtt is None else False
@property @property
def invert(self): def invert(self):
@@ -230,10 +237,22 @@ class MegaOutPort(MegaPushEntity):
def brightness(self): def brightness(self):
if not self.dimmer: if not self.dimmer:
return return
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.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
@@ -243,26 +262,38 @@ class MegaOutPort(MegaPushEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}) val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 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 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") val = val.get("value")
if self.index and self.addr: if not isinstance(val, str) and self.index is not None and self.addr is not None:
_val = val.get(self.addr) if not isinstance(val, dict):
if not isinstance(val, str): self.mega.lg.warning(f'{self.entity_id}: {val} is not a dict')
self.mega.lg.warning(f'{self} has wrong state: {val}') 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 return
_val = _val.split('/') _val = _val.split('/')
if len(_val) >= 2: if len(_val) >= 2:
val = val[self.index] self.mega.lg.debug('%s parsed values: %s[%s]="%s"', self.entity_id, _val, self.index, _val)
val = _val[self.index]
else: else:
self.mega.lg.warning(f'{self} has wrong state: {val}') self.mega.lg.warning(f'{self.entity_id}: {_val} has wrong length')
return return
elif self.index and self.addr is None: elif self.index is not None and self.addr is None:
self.mega.lg.warning(f'{self} does not has addr') self.mega.lg.warning(f'{self.entity_id} does not has addr')
return 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:
@@ -277,25 +308,30 @@ class MegaOutPort(MegaPushEntity):
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
cmd = {"cmd": f"{self.cmd_port}:{cmd}"} _cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr: if self.addr:
cmd['addr'] = self.addr _cmd['addr'] = self.addr
await self.mega.request(**cmd) await self.mega.request(**_cmd)
if self.index is not None: if self.index is not None:
# обновление текущего стейта для ds2413 # обновление текущего стейта для ds2413
self.hass.async_create_task(self.mega.get_port( await self.mega.get_port(
port=self.port, port=self.port,
force_http=True, force_http=True,
conv=False, conv=False,
http_cmd='list', 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: else:
self.mega.values[self.port] = {'value': cmd} self.mega.values[self.port] = {'value': cmd}
await self.get_state() await self.get_state()
@@ -303,26 +339,30 @@ class MegaOutPort(MegaPushEntity):
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}"} _cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if self.addr: if self.addr:
cmd['addr'] = self.addr _cmd['addr'] = self.addr
await self.mega.request(**cmd) await self.mega.request(**_cmd)
if self.index is not None: if self.index is not None:
# обновление текущего стейта для ds2413 # обновление текущего стейта для ds2413
self.hass.async_create_task(self.mega.get_port( await self.mega.get_port(
port=self.port, port=self.port,
force_http=True, force_http=True,
conv=False, conv=False,
http_cmd='list', 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: else:
self.mega.values[self.port] = {'value': cmd} 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, TypeError): except (ValueError, TypeError):

View File

@@ -16,6 +16,12 @@ 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):
url = '/mega' url = '/mega'
@@ -24,7 +30,9 @@ 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: {
@@ -34,23 +42,43 @@ class MegaView(HomeAssistantView):
} for mid in cfg if isinstance(cfg[mid], dict) } 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:
_LOGGER.debug('request from %s %s', request.remote, request.headers)
auth = False
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: 'h.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']:
hub = self.hubs.get('__def')
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,6 +87,9 @@ 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' and hub.restore_on_restart:
asyncio.create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
update_all = True update_all = True
@@ -66,23 +97,70 @@ class MegaView(HomeAssistantView):
update_all = False update_all = False
data['value'] = data.pop('v') data['value'] = data.pop('v')
data['mega_id'] = hub.id data['mega_id'] = hub.id
ret = 'd' ret = 'd' if hub.force_d else ''
if port is not None: if port is not None:
hub.values[port] = data if is_ext(data):
for cb in self.callbacks[hub.id][port]: ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
cb(data) if port in hub.extenders:
template: Template = self.templates.get(hub.id, {}).get(port) pt_orig = port
else:
pt_orig = hub.ext_in.get(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)
if ret == 'd' and act:
await hub.request(cmd=act)
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: 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={'Server': 's', 'Date': 'n'}) Response(body='' if hub.fake_response else ret, content_type='text/plain')
if hub.fake_response and 'value' not in data and 'pt' in data:
if 'd' in ret:
await hub.request(pt=port, cmd=ret)
else:
await hub.request(cmd=ret)
return ret return ret
async def later_restore(self, hub):
"""
Восстановление всех выходов с небольшой задержкой. Задержка нужна чтобы ответ прошел успешно
:param hub:
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
async def later_update(self, hub): async def later_update(self, hub):
_LOGGER.debug('force update')
await asyncio.sleep(1) await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh() await hub.updater.async_refresh()

View File

@@ -16,14 +16,15 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
from .const import ( from .const import (
TEMP, HUM, PRESS, TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN, LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW
) )
from .entities import set_events_off, BaseMegaEntity from .entities import set_events_off, BaseMegaEntity, MegaOutPort
from .exceptions import CannotConnect, NoPort from .exceptions import CannotConnect, NoPort
from .tools import make_ints from .tools import make_ints, int_ignore
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\.]+)')
@@ -72,9 +73,16 @@ class MegaD:
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,
poll_outs=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,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
@@ -82,11 +90,20 @@ class MegaD:
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.extenders = extenders or []
self.ext_in = ext_in or {}
self.ext_act = ext_acts or {}
self.poll_outs = poll_outs 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
@@ -101,6 +118,7 @@ class MegaD:
self.cnd = asyncio.Condition() self.cnd = asyncio.Condition()
self.online = True self.online = True
self.entities: typing.List[BaseMegaEntity] = [] 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)
@@ -116,16 +134,29 @@ class MegaD:
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()
@@ -170,6 +201,10 @@ class MegaD:
def force_d(self): def force_d(self):
return self.customize.get(CONF_FORCE_D, False) 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)
@@ -191,20 +226,31 @@ 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):
""" """
Polling ports Polling ports
""" """
self.lg.debug('poll') self.lg.debug('poll')
for x in self.entities: for x in self.extenders:
# обновление ds2413 устройств ret = await self._update_extender(x)
if x.http_cmd == 'ds2413': if not isinstance(ret, dict):
await self.get_port( self.lg.warning(f'wrong updater result: {ret} from extender {x}')
port=x.port, continue
force_http=True, self.values.update(ret)
http_cmd='list',
conv=False
)
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)
@@ -214,6 +260,7 @@ class MegaD:
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):
@@ -227,12 +274,18 @@ 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, **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:
async with aiohttp.request("get", url=url) as req: async with aiohttp.request("get", url=url) as req:
@@ -247,13 +300,18 @@ 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: if 'busy' in ret:
return None return None
if ':' in ret: if ':' in ret:
ret = PATT_SPLIT.split(ret) if ';' in ret:
ret = ret.split(';')
elif '/' in ret and not cmd == 'list':
ret = ret.split('/')
else:
ret = [ret]
ret = {'value': dict([ ret = {'value': dict([
x.split(':') for x in ret if x.count(':') == 1 x.split(':') for x in ret if x.count(':') == 1
])} ])}
@@ -275,7 +333,7 @@ class MegaD:
if http_cmd == 'list' and conv: if http_cmd == 'list' and conv:
await self.request(pt=port, cmd='conv') await self.request(pt=port, cmd='conv')
await asyncio.sleep(1) await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ntry = 0 ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3: while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -308,6 +366,8 @@ class MegaD:
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: if check_skip and not port in self.ports:
continue continue
ret = self.parse_response(x) ret = self.parse_response(x)
@@ -338,7 +398,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
@@ -367,7 +427,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
) )
@@ -393,61 +453,79 @@ 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] # async with self.lck:
url = f'http://{self.host}/{self.sec}/?pt={port}' # if port in self._scanned:
self.lg.debug( # return self._scanned[port]
f'scan port %s: %s', port, url # url = f'http://{self.host}/{self.sec}/?pt={port}'
) # self.lg.debug(
async with aiohttp.request('get', url) as req: # f'scan port %s: %s', port, url
html = await req.text() # )
if req.status != 200: # async with aiohttp.request('get', url) as req:
return # html = await req.text()
tree = BeautifulSoup(html, features="lxml") # if req.status != 200:
pty = tree.find('select', attrs={'name': 'pty'}) # return
if pty is None: # tree = BeautifulSoup(html, features="lxml")
return # pty = tree.find('select', attrs={'name': 'pty'})
else: # if pty is None:
pty = pty.find(selected=True) # return
if pty: # else:
pty = pty['value'] # pty = pty.find(selected=True)
else: # if pty:
return # pty = pty['value']
if pty in ['0', '1']: # else:
m = tree.find('select', attrs={'name': 'm'}) # return
if m: # if pty in ['0', '1']:
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'm'})
self._scanned[port] = (pty, m) # if m:
return pty, m # m = m.find(selected=True)['value']
elif pty == '3': # self._scanned[port] = (pty, m)
m = tree.find('select', attrs={'name': 'd'}) # return pty, m
if m: # elif pty == '3':
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'd'})
self._scanned[port] = (pty, m) # if m:
return pty, m # m = m.find(selected=True)['value']
elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно # self._scanned[port] = (pty, m)
m = tree.find('select', attrs={'name': 'd'}) # return pty, m
if m: # elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
m = m.find(selected=True)['value'] # m = tree.find('select', attrs={'name': 'd'})
self._scanned[port] = (pty, m or '0') # if m:
return pty, m or '0' # m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m or '0')
# return pty, m or '0'
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 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 = {}
async for port, cfg in self.scan_ports(nports):
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 == "1" and m == "2": elif cfg == DS2413:
# ds2413 # ds2413
_data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False) _data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
data = _data.get('value', {}) data = _data.get('value', {})
@@ -455,20 +533,43 @@ class MegaD:
self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}') self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}')
continue continue
for addr, state in data.items(): for addr, state in data.items():
ret['light'][port].append([ ret['light'][port].extend([
{"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'}, {"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'},
{"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'}, {"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'},
]) ])
elif pty in ('3', '2', '4'): elif cfg == MCP230:
try: extenders.append(port)
http_cmd = 'get' if cfg.inta:
ext_int[int_ignore(cfg.inta)] = port
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page)
pt = f'{port}e{n}'
if ext_cfg.ety == '1':
ret['light'][pt].append({})
elif ext_cfg.ety == '0':
if ext_cfg.eact:
ext_acts[pt] = ext_cfg.eact
ret['binary_sensor'][pt].append({})
elif cfg == PCA9685:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty 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')
@@ -478,8 +579,8 @@ 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):
if pty == '4' and m in I2C_DEVICE_TYPES: if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES[m]: values} values = {I2C_DEVICE_TYPES.get(cfg.m): values}
else: else:
values = {None: values} values = {None: values}
for key in values: for key in values:
@@ -493,4 +594,12 @@ 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()

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,7 +62,7 @@ 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

View File

@@ -15,5 +15,5 @@
"@andvikt" "@andvikt"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues", "issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v0.4.1b" "version": "v0.5.1b2"
} }

View File

@@ -13,7 +13,7 @@ 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,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@@ -22,6 +22,8 @@ from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE
from .hub import MegaD from .hub import MegaD
import re import re
from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)') TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)') HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -81,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
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 port, cfg in config_entry.data.get('sensor', {}).items():
port = int(port) port = int_ignore(port)
for data in cfg: for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data) hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = Mega1WSensor( sensor = Mega1WSensor(
@@ -163,7 +165,7 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret) ret = str(ret)
except: except:
ret = None ret = None
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE) 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: if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret}) ret = tmpl.async_render({'value': ret})

View File

@@ -15,6 +15,11 @@
"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%]" "poll_outs": "[%key:common::config_flow::data::poll_outs%]"
} }
} }

View File

@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
from . import hub as h 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__)
@@ -50,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
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

View File

@@ -1,5 +1,6 @@
_params = ['m', 'click', 'cnt', 'pt'] _params = ['m', 'click', 'cnt', 'pt']
def make_ints(d: dict): def make_ints(d: dict):
for x in _params: for x in _params:
try: try:
@@ -10,3 +11,10 @@ def make_ints(d: dict):
d['m'] = 0 d['m'] = 0
if 'click' not in d: if 'click' not in d:
d['click'] = 0 d['click'] = 0
def int_ignore(x):
try:
return int(x)
except (TypeError, ValueError):
return x

View File

@@ -22,7 +22,12 @@
"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" "poll_outs": "Poll outs"
} }
} }
@@ -35,8 +40,13 @@
"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" "poll_outs": "Poll outs"
} }
} }

View File

@@ -19,9 +19,14 @@
"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": "Обновлять выходы (регулярно)" "poll_outs": "Обновлять выходы (регулярно)"
} }
} }
@@ -35,10 +40,15 @@
"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' по умолчанию",
"poll_outs": "Обновлять выходы (регулярно)" "nports": "Кол-во портов",
"update_all": "Обновить все выходы когда срабатывает вход",
"protected": "Блокировать неразрешенные соединения",
"allow_hosts": "Разрешенные ip (через ;)",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"poll_outs": "Обновлять выходы (регулярно)"
} }
} }
} }

View File

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

246
readme.md
View File

@@ -10,24 +10,39 @@
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната ) интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git) Обновление прошивки 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 (начиная с версии 0.4.1) - поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.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
# из папки с конфигом # из папки с конфигом
@@ -40,226 +55,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
10:
skip: false # если нужен skip, то он работает только целиком для всего порта
# в случае если порт настроен как ds2413
address_a: #address - это адрес ds2413
# здесь внутри работают все стандартные поля конфигурирования порта
invert: true
domain: switch
c6c439000000_b:
name: какое-то имя
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
# можно так же указать шаблон для конвертации значения, может быть полезно для ацп-входа
# текущее значение порта передается в шаблон в переменной "value"
conv_template: "{{(value|float)/100}}"
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`
**Внимание!** Не используйте srv loop на контроллере, это может приводить к ложным срабатываниям входов. Вместо srv loop
интеграция будет сама обновлять все состояния портов с заданным интервалом
В самой меге необходимо прописать настройки: В самой меге необходимо прописать настройки:
```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 должен быть разным.
#### Ответ на входящие события от контроллера При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает объекты`
запустить то что прописано в поле act в настройках порта).
Поддерживаются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не ## Зависимости
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id) Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
Примеры: Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
```yaml из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
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 %}
```
Начиная с версии v0.3.17 ответ можно слать так же и в режиме MQTT. Аналогично, темплейт должен возвращать готовую команду
такую же как требует команда cmd, так же можно использовать d, но d не отправляется по умолчанию, это сделано чтобы не
сломать текущую логику у пользователей предыдущих версий. Чтобы включить для всех входов в режиме mqtt отправку команды
d необходимо в конфиге прописать следующее:
```yaml
mega:
mega1:
force_d: true
```
**Внимание!** Нельзя использовать чекбокс напротив поля act если планируется использовать ответ сервера - у вас и
сработает act и команда от сервера, а вслучае ответа d сработает act два раза.
Так же следует понимать, что это не "ответ" в нормальном понимании - это вызов следом за полученным mqtt-сообщением Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
http команды такого вида `http://megaurl/?pt=port&cmd=rendered_template`, где `port` - это номер порта сработавшего входа,
а `cmd` - текст команды, который получен из темплейта. Те это имитация ответа. У этого подхода есть минус - задержка в
исполнении будет значительно выше чем при ответе в режиме http, но тем не менее эта задержка скорее всего не будет
сильно заметна.
## binary_sensor и события
Входы будут доступны как binary_sensor, а так же в виде событий `mega.sensor` и `mega.binary`.
Для корректной работы binary_sensor имеет смысл использовать режим P&R, для остальных режимов - лучше пользоваться
событиями.
События можно использовать в автоматизациях, например так:
```yaml
# Пример события с полями как есть прямо из меги
- alias: some double click
trigger:
- platform: event
event_type: mega.sensor
event_data:
pt: 1
click: 2
action:
- service: light.toggle
entity_id: light.some_light
```
События могут содержать следующие поля:
- `mega_id`: id как в конфиге HA
- `pt`: номер порта
- `cnt`: счетчик срабатываний
- `mdid`: if как в конфиге контроллера
- `click`: клик (подробнее в документации меги)
- `value`: текущее значение (только для mqtt)
- `port`: номер порта
Начиная с версии 0.3.7 появилось так же событие типа `mega.binary`:
```yaml
# Пример события с полями как есть прямо из меги
- alias: some long click
trigger:
- platform: event
event_type: mega.binary
event_data:
entity_id: binary_sensor.some_id
type: long
action:
- service: light.toggle
entity_id: light.some_light
```
Возможные варианты поля `type`:
- `long`: долгое нажатие
- `release`: размыкание (с гарантией** что не было долгого нажатия)
- `long_release`: размыкание после долгого нажатия
- `press`: замыкание
- `single`: одинарный клик (в режиме кликов)
- `double`: двойной клик
**гарантия есть только при использовании http-метода синхронизации, mqtt не гарантирует порядок доставки сообщений, хотя
маловероятно, что порядок будет нарушен, но все же сам протокол не дает таких гарантий.
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `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
```
#### Отладка ответов http-сервера
Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли
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`, либо скрипт, который вы настроили