Compare commits

...

28 Commits

Author SHA1 Message Date
Andrey
ad1210d5cc fix some timeouts 2021-03-29 21:43:25 +03:00
Andrey Viktorov
687e80f729 fix empty config while first setup 2021-03-29 19:54:48 +03:00
Andrey Viktorov
7d777c9e82 fix sensors 2021-03-24 18:14:15 +03:00
Andrey Viktorov
c9f0e85f6a edit readme 2021-03-24 17:04:37 +03:00
andvikt
e75f8b91ef Update sensor.py 2021-03-24 12:40:39 +03:00
andvikt
bf15d4f3f9 Update hub.py 2021-03-24 10:57:42 +03:00
Andrey Viktorov
124ef36564 - fix order for ws28xx 2021-03-24 08:09:09 +03:00
Andrey Viktorov
9e73191a91 - add rgbw support
- add ws28 support
- add transitions to pwm
- add units of measurement for all i2c sensors
- remove mqtt support
2021-03-23 15:30:34 +03:00
Andrey Viktorov
1270ea2ee2 - add rgbw support
- add ws28 support
- add transitions to pwm
- add units of measurement for all i2c sensors
- remove mqtt support
2021-03-23 15:10:50 +03:00
andvikt
36433a7fdd Update readme.md 2021-03-11 15:46:44 +03:00
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
21 changed files with 865 additions and 413 deletions

View File

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

View File

@@ -6,23 +6,49 @@ from functools import partial
import voluptuous as vol
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt
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, \
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_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \
RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE
from .hub import MegaD
from .config_flow import ConfigFlow
from .http import MegaView
_LOGGER = logging.getLogger(__name__)
_port_n = vol.Any(int, str)
LED_LIGHT = \
{
str: vol.Any(
{
vol.Required(CONF_PORTS): vol.Any(
vol.ExactSequence([_port_n, _port_n, _port_n]),
vol.ExactSequence([_port_n, _port_n, _port_n, _port_n]),
msg='ports must be [R, G, B] or [R, G, B, W] of integers 0..255'
),
vol.Optional(CONF_NAME): str,
vol.Optional(CONF_WHITE_SEP, default=True): bool,
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
},
{
vol.Required(CONF_PORT): int,
vol.Required(CONF_WS28XX): True,
vol.Optional(CONF_CHIP, default=100): int,
vol.Optional(CONF_ORDER, default='rgb'): vol.Any(*RGB_COMBINATIONS, msg=f'order must be one of {RGB_COMBINATIONS}'),
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
vol.Optional(CONF_NAME): str,
},
)
}
CUSTOMIZE_PORT = {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Optional(CONF_INVERT, default=False): bool,
@@ -46,6 +72,16 @@ CUSTOMIZE_PORT = {
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,
vol.Optional(CONF_SMOOTH): cv.time_period_seconds,
# vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений
vol.Optional(str): {
vol.Optional(CONF_NAME): str,
vol.Optional(CONF_DEVICE_CLASS): str,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
}
CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
@@ -70,10 +106,11 @@ CONFIG_SCHEMA = vol.Schema(
description='Ответ по умолчанию',
default=None
): vol.Any(cv.template, None),
vol.Optional(CONF_LED): LED_LIGHT,
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
CUSTOMIZE_PORT,
CUSTOMIZE_DS2413,
)
),
}
}
},
@@ -121,18 +158,7 @@ async def get_hub(hass, entry):
data = dict(entry.data)
data.update(entry.options or {})
data.update(id=id)
use_mqtt = data.get(CONF_MQTT_INPUTS, True)
_mqtt = hass.data.get(mqtt.DOMAIN) if use_mqtt else None
if _mqtt is None and use_mqtt:
for x in range(5):
await asyncio.sleep(5)
_mqtt = hass.data.get(mqtt.DOMAIN)
if _mqtt is not None:
break
if _mqtt is None:
raise Exception('mqtt not configured, please configure mqtt first')
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
hub = MegaD(hass, config=entry, **data, lg=_LOGGER, loop=asyncio.get_event_loop())
hub.mqtt_id = await hub.get_mqtt_id()
return hub
@@ -172,18 +198,14 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
:param entry:
:return:
"""
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options
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)
# hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
# hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
# hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
await hass.config_entries.async_reload(entry.entry_id)
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."""
id = entry.data.get('id', entry.entry_id)
hub: MegaD = hass.data[DOMAIN].get(id)
@@ -193,12 +215,17 @@ async def async_remove_entry(hass, entry) -> None:
_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)
if task is not None:
task.cancel()
if hub is None:
return
await hub.stop()
return True
async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry):

View File

@@ -57,6 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
continue
hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices)
@@ -95,20 +97,4 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def _update(self, payload: dict):
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

@@ -5,13 +5,12 @@ import logging
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
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_RELOAD, \
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
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME
from .hub import MegaD
from . import exceptions
@@ -22,10 +21,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_ID, default='mega'): str,
vol.Required(CONF_HOST, default="192.168.0.14"): str,
vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
vol.Optional(CONF_SCAN_INTERVAL, default=30): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=False): 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_UPDATE_ALL, default=True): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
@@ -33,15 +32,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
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,
},
)
async def get_hub(hass: HomeAssistant, data):
_mqtt = hass.data.get(mqtt.DOMAIN)
# _mqtt = hass.data.get(mqtt.DOMAIN)
# if not isinstance(_mqtt, mqtt.MQTT):
# raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt, loop=asyncio.get_event_loop())
hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) #mqtt=_mqtt,
hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate():
raise exceptions.InvalidAuth
@@ -63,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega."""
VERSION = 19
VERSION = 24
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
@@ -78,10 +78,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
hub = await validate_input(self.hass, user_input)
await hub.start()
hub.new_naming=True
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
hub.lg.debug(f'config loaded: %s', config)
config.update(user_input)
config['new_naming'] = True
return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config,
@@ -113,22 +115,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Manage the options."""
hub = await get_hub(self.hass, self.config_entry.data)
new_naming = self.config_entry.data.get('new_naming', False)
if user_input is not None:
reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data)
cfg.update(user_input)
hub = await get_hub(self.hass, cfg)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
cfg['new_naming'] = new_naming
self.config_entry.data = cfg
await get_hub(self.hass, cfg)
if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id)
cfg = 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(
title='',
data=cfg,
@@ -139,8 +139,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema=vol.Schema({
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): 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_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
@@ -149,6 +149,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
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,
}),
)

View File

@@ -44,6 +44,9 @@ def parse_config(page: str):
v = page.find('input', attrs={'name': x})
if v:
ret[x] = v['value']
smooth = page.find('input', attrs={'name': 'misc'})
if smooth is None or smooth.get('checked') is None:
ret['misc'] = None
return Config(**ret, src=page)

View File

@@ -1,5 +1,6 @@
"""Constants for the mega integration."""
import re
from itertools import permutations
DOMAIN = "mega"
CONF_MEGA_ID = "mega_id"
@@ -34,6 +35,16 @@ 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'
CONF_LED = 'led'
CONF_WS28XX = 'ws28xx'
CONF_ORDER = 'order'
CONF_SMOOTH = 'smooth'
CONF_WHITE_SEP = 'white_sep'
CONF_CHIP = 'chip'
CONF_RANGE = 'range'
PLATFORMS = [
"light",
"switch",
@@ -53,4 +64,18 @@ LUX = 'lux'
SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double'
PATT_FW = re.compile(r'fw:\s(.+)\)')
PATT_FW = re.compile(r'fw:\s(.+)\)')
REMOVE_CONFIG = [
'extenders',
'ext_in',
'ext_acts',
'i2c_sensors',
'binary_sensor',
'light',
'i2c',
'sensor',
'smooth',
]
RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')]
RGB = 'rgb'

View File

@@ -1,5 +1,9 @@
import logging
import asyncio
import time
import typing
from datetime import timedelta
from functools import partial
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
@@ -8,7 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.restore_state import RestoreEntity
from . import hub as h
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY, CONF_SMOOTH
_events_on = False
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +42,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def __init__(
self,
mega: 'h.MegaD',
port: int,
port: typing.Union[int, str, typing.List[int]],
config_entry: ConfigEntry = None,
id_suffix=None,
name=None,
@@ -46,11 +50,13 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
http_cmd='get',
addr: str=None,
index=None,
customize=None,
smooth=None,
**kwargs,
):
super().__init__(mega.updater)
self._smooth = smooth
self.http_cmd = http_cmd
self._state: State = None
self.port = port
self.config_entry = config_entry
@@ -58,18 +64,68 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
mega.entities.append(self)
self._mega_id = mega.id
self._lg = None
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "")
self._name = name or f"{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None
if not isinstance(port, list):
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "")
_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 "")
self._customize: dict = None
else:
assert id_suffix is not None
assert name is not None
assert isinstance(customize, dict)
self._unique_id = unique_id or f"mega_{mega.id}_{id_suffix}"
self._name = name
self._customize = customize
self.index = index
self.addr = addr
self.id_suffix = id_suffix
self._can_smooth_hard = None
if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port}
@property
def is_ws(self):
return False
def get_attribute(self, name, default=None):
attr = getattr(self, f'_{name}', None)
if attr is None and self._state is not None:
if name == 'is_on':
attr = self._state.state
else:
attr = self._state.attributes.get(f'{name}', default)
return attr if attr is not None else default
@property
def can_smooth_hardware(self):
if self._can_smooth_hard is None:
if self.is_ws:
self._can_smooth_hard = False
if not isinstance(self.port, list):
self._can_smooth_hard = self.port in self.mega.smooth
else:
for x in self.port:
if isinstance(x, str):
self._can_smooth_hard = False
break
else:
self._can_smooth_hard = self.port in self.mega.smooth
return self._can_smooth_hard
@property
def enabled(self):
if '<' in self.name:
return False
else:
return super().enabled
@property
def customize(self):
if self._customize is not None:
return self._customize
if self.hass is None:
return {}
if self._customize is None:
@@ -85,15 +141,23 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@property
def device_info(self):
if isinstance(self.port, list):
pt_idx = self.id_suffix
else:
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
if isinstance(_pt, str) and 'e' in _pt:
pt_idx, _ = _pt.split('e')
else:
pt_idx = _pt
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, f'{self._mega_id}', self.port),
(DOMAIN, f'{self._mega_id}', pt_idx),
},
"config_entries": [
self.config_entry,
],
"name": f'{self._mega_id} port {self.port}',
"name": f'{self._mega_id} port {pt_idx}' if not isinstance(self.port, list) else f'{self._mega_id} {pt_idx}',
"manufacturer": 'ab-log.ru',
# "model": self.light.productname,
"sw_version": self.mega.fw,
@@ -114,7 +178,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def name(self):
c = self.customize.get(CONF_NAME)
if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}"
if not isinstance(self.port, list):
_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}"
else:
c = self.id_suffix
return c
@property
@@ -125,13 +193,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
global _task_set_ev_on
await super().async_added_to_hass()
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):
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()
class MegaPushEntity(BaseMegaEntity):
@@ -147,11 +212,10 @@ class MegaPushEntity(BaseMegaEntity):
def __update(self, value: dict):
self._update(value)
if self.hass is None:
return
self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state)
if self.mega.mqtt_inputs and not _events_on:
_LOGGER.debug('skip event because events are off')
return
if not self.entity_id.startswith('binary_sensor'):
_LOGGER.debug('skip event because not a bnary sens')
return
@@ -203,11 +267,6 @@ class MegaPushEntity(BaseMegaEntity):
def _update(self, payload: dict):
pass
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
if self.mega.mqtt is not None:
asyncio.create_task(self.mega.get_port(self.port))
class MegaOutPort(MegaPushEntity):
@@ -224,11 +283,24 @@ class MegaOutPort(MegaPushEntity):
self._is_on = None
self.dimmer = dimmer
self.dimmer_scale = dimmer_scale
self.is_extender = isinstance(self.port, str) and 'e' in self.port
self.task: asyncio.Task = None
self._restore_brightness = None
self._last_called: float = 0
# @property
# def assumed_state(self) -> bool:
# return True if self.index is not None or self.mega.mqtt is None else False
@property
def max_dim(self):
if self.dimmer_scale == 1:
return 255
elif self.dimmer == 16:
return 4095
else:
return 255
@property
def invert(self):
return self.customize.get(CONF_INVERT, False)
@@ -306,19 +378,74 @@ class MegaOutPort(MegaPushEntity):
else:
return self.port
async def async_turn_on(self, brightness=None, **kwargs) -> None:
@property
def smooth(self) -> timedelta:
ret = self.customize.get(CONF_SMOOTH)
if ret is None and self._smooth:
ret = timedelta(seconds=self._smooth)
return ret
@property
def smooth_dim(self):
if not self.dimmer:
return False
return self.smooth or self.can_smooth_hardware
def update_from_smooth(self, value, update_state=False):
if isinstance(self.port, str):
self.mega.values[self.port] = value[0]
else:
self.mega.values[self.port] = {
'value': value[0]
}
if update_state:
self.async_write_ha_state()
def _set_dim_brightness(self, from_, to_, transition):
pct = abs(to_ - from_) / (255 if self.dimmer_scale == 1 else 4095)
update_state = transition is not None and transition > 3
tm = (self.smooth.total_seconds() * pct) if transition is None else transition
if self.task is not None:
self.task.cancel()
self.task = asyncio.create_task(self.mega.smooth_dim(
(self.cmd_port, from_, to_),
time=tm,
can_smooth_hardware=self.can_smooth_hardware,
max_values=[255 if self.dimmer_scale == 1 else 4095],
updater=partial(self.update_from_smooth, update_state=update_state),
))
async def async_turn_on(self, brightness=None, transition=None, **kwargs):
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
if not self.dimmer:
transition = None
if not self.is_on:
brightness = self._restore_brightness
brightness = brightness or self.brightness or 255
_prev = safe_int(self.brightness) or 0
self._brightness = brightness
if self.dimmer and brightness == 0:
cmd = 255 * self.dimmer_scale
cmd = self.max_dim
elif self.dimmer:
cmd = brightness * self.dimmer_scale
cmd = min((brightness * self.dimmer_scale, self.max_dim))
if self.smooth_dim or transition:
self._set_dim_brightness(from_=_prev, to_=cmd, transition=transition)
else:
cmd = 1 if not self.invert else 0
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
if transition is None:
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
else:
_cmd = {
"pt": f"{self.cmd_port}",
"pwm": cmd,
"cnt": round(transition / (abs(_prev - brightness) / 255)),
}
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
@@ -336,13 +463,26 @@ class MegaOutPort(MegaPushEntity):
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
async def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, transition=None, **kwargs) -> None:
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
self._restore_brightness = safe_int(self._brightness)
if not self.dimmer:
transition = None
cmd = "0" if not self.invert else "1"
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
_prev = safe_int(self.brightness) or 0
if self.addr:
_cmd['addr'] = self.addr
await self.mega.request(**_cmd, priority=-1)
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1)
else:
self._set_dim_brightness(
from_=_prev,
to_=0,
transition=transition,
)
if self.index is not None:
# обновление текущего стейта для ds2413
await self.mega.get_port(
@@ -357,6 +497,11 @@ class MegaOutPort(MegaPushEntity):
self.mega.values[self.port] = {'value': cmd}
await self.get_state()
async def async_will_remove_from_hass(self) -> None:
if self.task is not None:
self.task.cancel()
def safe_int(v):
if v == 'ON':

View File

@@ -5,10 +5,6 @@ class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class MqttNotConfigured(exceptions.HomeAssistantError):
"""Error to indicate mqtt is not configured"""
class DuplicateId(exceptions.HomeAssistantError):
"""Error to indicate duplicate id"""

View File

@@ -77,7 +77,11 @@ class MegaView(HomeAssistantView):
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')
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)
data = dict(request.query)
@@ -87,8 +91,8 @@ class MegaView(HomeAssistantView):
)
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data)
if data.get('st') == '1' and hub.restore_on_restart:
asyncio.create_task(self.later_restore(hub))
if data.get('st') == '1':
hass.async_create_task(self.later_restore(hub))
return Response(status=200)
port = data.get('pt')
data = data.copy()
@@ -158,10 +162,11 @@ class MegaView(HomeAssistantView):
:return:
"""
await asyncio.sleep(0.2)
await hub.restore_states()
if hub.restore_on_restart:
await hub.restore_states()
await hub.reload()
async def later_update(self, hub):
await asyncio.sleep(1)
_LOGGER.debug('force update')
await hub.updater.async_refresh()
await hub.updater.async_refresh()

View File

@@ -9,7 +9,7 @@ import re
import json
from bs4 import BeautifulSoup
from homeassistant.components import mqtt
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
@@ -20,9 +20,10 @@ from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN,
from .const import (
TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN,
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW
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 .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int
from .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock
@@ -66,10 +67,9 @@ class MegaD:
loop: asyncio.AbstractEventLoop,
host: str,
password: str,
mqtt: mqtt.MQTT,
lg: logging.Logger,
id: str,
mqtt_inputs: bool = True,
config: ConfigEntry = None,
mqtt_id: str = None,
scan_interval=60,
port_to_scan=0,
@@ -85,34 +85,38 @@ class MegaD:
ext_in=None,
ext_acts=None,
i2c_sensors=None,
new_naming=False,
update_time=False,
smooth: list=None,
**kwargs,
):
"""Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
self.http.allowed_hosts |= {host}
self.http.hubs[host] = self
if len(self.http.hubs) == 1:
self.http.hubs['__def'] = self
if mqtt_id:
self.http.hubs[mqtt_id] = self
else:
self.http = None
if config is not None:
lg.debug(f'load config: %s', config.data)
self.config = config
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None:
self.http.allowed_hosts |= {host}
self.http.hubs[host] = self
if len(self.http.hubs) == 1:
self.http.hubs['__def'] = self
if mqtt_id:
self.http.hubs[mqtt_id] = self
self.smooth = smooth or []
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.nports = nports
self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None
self.hass = hass
self.host = host
self.sec = password
self.mqtt = mqtt
self.id = id
self.lck = asyncio.Lock()
self.last_long = {}
@@ -153,23 +157,15 @@ class MegaD:
if force_d is not None:
self.customize[CONF_FORCE_D] = force_d
try:
if allow_hosts is not None:
if allow_hosts is not None and DOMAIN in hass.data:
allow_hosts = set(allow_hosts.split(';'))
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
hass.data[DOMAIN][CONF_HTTP].protected = protected
except Exception:
self.lg.exception('while setting allowed hosts')
async def start(self):
self.loop = asyncio.get_event_loop()
if self.mqtt is not None:
set_events_off()
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
pass
async def stop(self):
if self.subs is not None:
@@ -248,6 +244,8 @@ class MegaD:
Polling ports
"""
self.lg.debug('poll')
if self._update_time:
await self.update_time()
for x in self.i2c_sensors:
if not isinstance(x, dict):
continue
@@ -260,15 +258,9 @@ class MegaD:
self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue
self.values.update(ret)
if self.mqtt is None:
await self.get_all_ports()
await self.get_sensors(only_list=True)
elif self.poll_outs:
await self.get_all_ports(check_skip=True)
elif len(self.sensors) > 0:
await self.get_sensors()
else:
await self.get_port(self.port_to_scan)
await self.get_all_ports()
await self.get_sensors(only_list=True)
await self._get_ds2413()
return self.values
@@ -297,14 +289,20 @@ class MegaD:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck(priority):
async with aiohttp.request("get", url=url) as req:
if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return None
else:
ret = await req.text()
self.lg.debug('response %s', ret)
return ret
for _ntry in range(3):
try:
async with aiohttp.request("get", url=url) as req:
if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return None
else:
ret = await req.text()
self.lg.debug('response %s', ret)
return ret
except asyncio.TimeoutError:
self.lg.warning(f'timeout while requesting {url}')
await asyncio.sleep(1)
raise asyncio.TimeoutError('after 3 tries')
async def save(self):
await self.send_command(cmd='s')
@@ -338,55 +336,33 @@ class MegaD:
хранилище values
"""
self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http:
if http_cmd == 'list' and conv:
await self.request(pt=port, cmd='conv')
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ntry += 1
self.lg.debug('parsed: %s', ret)
self.values[port] = ret
return ret
if http_cmd == 'list' and conv:
await self.request(pt=port, cmd='conv')
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ntry += 1
self.lg.debug('parsed: %s', ret)
self.values[port] = ret
return ret
async with self._notif_lck:
async with self.notifiers[port]:
cnd = self.notifiers[port]
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=2,
retain=False,
)
try:
await asyncio.wait_for(cnd.wait(), timeout=10)
return self.values.get(port)
except asyncio.TimeoutError:
self.lg.error(f'timeout when getting port {port}')
@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:
ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports:
continue
if check_skip and not port in self.ports:
continue
ret = self.parse_response(x)
self.values[port] = ret
elif not check_skip:
for x in range(self.nports + 1):
await self.get_port(x)
else:
for x in self.ports:
await self.get_port(x)
ret = await self.request(cmd='all')
for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports:
continue
if check_skip and not port in self.ports:
continue
ret = self.parse_response(x)
self.values[port] = ret
async def reboot(self, save=True):
await self.save()
@@ -440,10 +416,7 @@ class MegaD:
self.lg.debug(
f'subscribe %s %s', port, callback
)
if self.mqtt_inputs:
self._callbacks[port].append(callback)
else:
self.http.callbacks[self.id][port].append(callback)
self.http.callbacks[self.id][port].append(callback)
async def authenticate(self) -> bool:
"""Test if we can authenticate with the host."""
@@ -464,45 +437,6 @@ class MegaD:
async def scan_port(self, port):
data = await self.request(pt=port)
return parse_config(data)
# async with self.lck:
# if port in self._scanned:
# 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
# elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
# m = tree.find('select', attrs={'name': 'd'})
# if m:
# m = m.find(selected=True)['value']
# self._scanned[port] = (pty, m or '0')
# return pty, m or '0'
async def scan_ports(self, nports=37):
for x in range(0, nports+1):
@@ -530,9 +464,17 @@ class MegaD:
:return:
"""
_params = tuple(params.items())
return {
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):
ret = defaultdict(lambda: defaultdict(list))
@@ -541,11 +483,17 @@ class MegaD:
ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = []
ret['smooth'] = smooth = []
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({})
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': cfg.m == '1'})
if cfg.misc is not None:
smooth.append(port)
ret['light'][port].append({'dimmer': cfg.m == '1', 'smooth': safe_int(cfg.misc)})
elif cfg == DS2413:
# ds2413
_data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
@@ -567,7 +515,7 @@ class MegaD:
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}'
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':
@@ -579,8 +527,10 @@ class MegaD:
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
elif cfg.pty == '4' and cfg.gr == '0':
pt = f'{port}e{n}'
name = pt if not self.new_naming else f'{port:02}e{n:02}'
ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16, 'name': f'{self.id}_{name}'})
if 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)
@@ -590,6 +540,9 @@ class MegaD:
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':
@@ -633,4 +586,102 @@ class MegaD:
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)
cfg = dict(self.config.data)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
self.lg.debug(f'new config: %s', cfg)
self.config.data = cfg
if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id)
return cfg
def _wrap_port_smooth(self, from_, to_, time):
self.lg.debug('dim from %s to %s for %s seconds', from_, to_, time)
if time <= 0:
return
beg = datetime.now()
diff = to_ - from_
while True:
_pct = (datetime.now() - beg).total_seconds() / time
if _pct > 1:
return
val = from_ + round(diff * _pct)
yield val
async def smooth_dim(
self,
*config: typing.Tuple[typing.Any, int, int],
time: float,
jitter: int = 50,
ws=False,
updater=None,
can_smooth_hardware=False,
max_values=None,
chip=None,
):
"""
Плавное диммирование силами сервера, сразу нескольких портов (одной командой)
:param config: [(port, from, to), (port, from, to)]
:param time: время на диммирование
:param jitter: дополнительное замедление между командами в милисекундах
:param ws: если True, используется режим ws21xx
:param updater: функция, в которую передается текущее состояние
:param can_smooth_hardware: если True, используется аппаратная реализация smooth
:param max_values: максимальные значения (необходимы для расчета тайминга аппаратного smooth)
:param chip: кол-во чипов для ws-лент
:return:
"""
if can_smooth_hardware:
for i, (pt, from_, to_) in enumerate(config):
pct = abs(from_ - to_) / max_values[i]
tm = max([round(time / pct), 1])
await self.request(pt=pt, pwm=to_, cnt=tm)
last_step = tuple([to_ for (_, _, to_) in config])
gen = [self._wrap_port_smooth(f, t, time) for (_, f, t) in config]
c = None
stop = False
while True:
if stop:
return
await asyncio.sleep(jitter / 1000)
try:
_next_val = tuple([next(x) for x in gen])
except StopIteration:
_next_val = last_step
stop = True
if _next_val == c:
continue
if updater is not None:
updater(_next_val)
if can_smooth_hardware:
if _next_val == last_step:
return
continue
if not ws:
cmd = dict(
cmd=';'.join([f'{pt}:{_next_val[i]}' for i, (pt, _, _) in enumerate(config)])
)
await self.request(**cmd)
else:
# для адресных лент
cmd = dict(
pt=config[0][0],
chip=chip,
ws=''.join([hex(x).split('x')[1].rjust(2, '0').upper() for x in _next_val])
)
await self.request(**cmd)
if _next_val == last_step:
return
c = _next_val

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
@@ -5,7 +6,15 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
PERCENTAGE,
LIGHT_LUX,
TEMP_CELSIUS,
CONCENTRATION_PARTS_PER_MILLION,
PRESSURE_BAR,
)
from collections import namedtuple
DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
def parse_scan_page(page: str):
@@ -27,12 +36,17 @@ def parse_scan_page(page: str):
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
elif isinstance(c, Request):
if c.delay:
params = params.copy()
params['delay'] = c.delay
req.append(params)
continue
elif isinstance(c, DeviceType):
c, m, suffix = c
else:
suffix = ''
continue
suffix = suffix or c
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
@@ -42,10 +56,12 @@ def parse_scan_page(page: str):
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
'unit_of_measurement': m,
})
req.append(params)
return req, ret
@@ -55,55 +71,57 @@ class Skip:
pass
@dataclass
class Request:
pass
delay: float = None
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None)
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
Skip,
Request(delay=1), # запрос на измерение
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
DeviceType(None, CONCENTRATION_PARTS_PER_MILLION, None) # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
]
}

View File

@@ -1,11 +1,20 @@
"""Platform for light integration."""
import asyncio
import logging
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol
import colorsys
import time
from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_SCHEMA,
SUPPORT_BRIGHTNESS,
LightEntity,
SUPPORT_TRANSITION,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -16,7 +25,7 @@ from homeassistant.const import (
CONF_DOMAIN,
)
from homeassistant.core import HomeAssistant
from .entities import MegaOutPort
from .entities import MegaOutPort, BaseMegaEntity, safe_int
from .hub import MegaD
from .const import (
@@ -24,12 +33,12 @@ from .const import (
CONF_SWITCH,
DOMAIN,
CONF_CUSTOM,
CONF_SKIP,
CONF_SKIP, CONF_LED, CONF_WS28XX, CONF_PORTS, CONF_WHITE_SEP, CONF_SMOOTH, CONF_ORDER, CONF_CHIP, RGB,
)
from .tools import int_ignore
from .tools import int_ignore, map_reorder_rgb
lg = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
# Validation of the user's configuration
_EXTENDED = {
@@ -60,16 +69,32 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
skip = []
if CONF_LED in customize:
for entity_id, conf in customize[CONF_LED].items():
ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)]
skip.extend(ports)
devices.append(MegaRGBW(
mega=hub,
port=ports,
name=entity_id,
customize=conf,
id_suffix=entity_id,
config_entry=config_entry
))
for port, cfg in config_entry.data.get('light', {}).items():
port = int_ignore(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
c = customize.get(port, {})
if c.get(CONF_SKIP, False) or port in skip or c.get(CONF_DOMAIN, 'light') != 'light':
continue
for data in cfg:
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)
if '<' in light.name:
continue
devices.append(light)
async_add_devices(devices)
@@ -77,5 +102,216 @@ class MegaLight(MegaOutPort, LightEntity):
@property
def supported_features(self):
return SUPPORT_BRIGHTNESS if self.dimmer else 0
return (
(SUPPORT_BRIGHTNESS if self.dimmer else 0) |
(SUPPORT_TRANSITION if self.dimmer else 0)
)
class MegaRGBW(LightEntity, BaseMegaEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_on = None
self._brightness = None
self._hs_color = None
self._white_value = None
self._task: asyncio.Task = None
self._restore = None
self.smooth: timedelta = self.customize[CONF_SMOOTH]
self._color_order = self.customize.get(CONF_ORDER, 'rgb')
self._last_called: float = 0
self._max_values = None
@property
def max_values(self) -> list:
if self._max_values is None:
if self.is_ws:
self._max_values = [255] * 3
else:
self._max_values = [
255 if isinstance(x, int) else 4095 for x in self.port
]
return self._max_values
@property
def chip(self) -> int:
return self.customize.get(CONF_CHIP, 100)
@property
def is_ws(self):
return self.customize.get(CONF_WS28XX)
@property
def white_value(self):
if self.supported_features & SUPPORT_WHITE_VALUE:
return float(self.get_attribute('white_value', 0))
@property
def brightness(self):
return float(self.get_attribute('brightness', 0))
@property
def hs_color(self):
return self.get_attribute('hs_color', [0, 0])
@property
def is_on(self):
return self.get_attribute('is_on', False)
@property
def supported_features(self):
return (
SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION |
SUPPORT_COLOR |
(SUPPORT_WHITE_VALUE if len(self.port) == 4 else 0)
)
def get_rgbw(self):
if not self.is_on:
return [0 for x in range(len(self.port))] if not self.is_ws else [0] * 3
rgb = colorsys.hsv_to_rgb(
self.hs_color[0]/360, self.hs_color[1]/100, self.brightness / 255
)
rgb = [x for x in rgb]
if self.white_value is not None:
white = self.white_value
if not self.customize.get(CONF_WHITE_SEP):
white = white * (self.brightness / 255)
rgb.append(white / 255)
rgb = [
round(x * self.max_values[i]) for i, x in enumerate(rgb)
]
if self.is_ws:
# восстанавливаем мэпинг
rgb = map_reorder_rgb(rgb, RGB, self._color_order)
return rgb
async def async_turn_on(self, **kwargs):
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
self.lg.debug(f'turn on %s with kwargs %s', self.entity_id, kwargs)
if self._restore is not None:
self._restore.update(kwargs)
kwargs = self._restore
self._restore = None
_before = self.get_rgbw()
self._is_on = True
if self._task is not None:
self._task.cancel()
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def async_turn_off(self, **kwargs):
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
self._restore = {
'hs_color': self.hs_color,
'brightness': self.brightness,
'white_value': self.white_value,
}
_before = self.get_rgbw()
self._is_on = False
if self._task is not None:
self._task.cancel()
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def set_color(self, _before, **kwargs):
transition = kwargs.get('transition')
update_state = transition is not None and transition > 3
for item, value in kwargs.items():
setattr(self, f'_{item}', value)
_after = self.get_rgbw()
if transition is None:
transition = self.smooth.total_seconds()
ratio = self.calc_speed_ratio(_before, _after)
transition = transition * ratio
self.async_write_ha_state()
ports = self.port if not self.is_ws else self.port*3
config = [(port, _before[i], _after[i]) for i, port in enumerate(ports)]
try:
await self.mega.smooth_dim(
*config,
time=transition,
ws=self.is_ws,
jitter=50,
updater=partial(self._update_from_rgb, update_state=update_state),
can_smooth_hardware=self.can_smooth_hardware,
max_values=self.max_values,
chip=self.chip,
)
except asyncio.CancelledError:
return
except:
self.lg.exception('while dimming')
async def async_will_remove_from_hass(self) -> None:
await super().async_will_remove_from_hass()
if self._task is not None:
self._task.cancel()
def _update_from_rgb(self, rgbw, update_state=False):
if len(self.port) == 4:
w = rgbw[-1]
rgb = rgbw[:3]
else:
w = None
rgb = rgbw
if self.is_ws:
rgb = map_reorder_rgb(
rgb, self._color_order, RGB
)
h, s, v = colorsys.rgb_to_hsv(*[x/self.max_values[i] for i, x in enumerate(rgb)])
h *= 360
s *= 100
v *= 255
self._hs_color = [h, s]
if self.is_on:
self._brightness = v
if w is not None:
if not self.customize.get(CONF_WHITE_SEP):
w = w/(self._brightness / 255)
else:
w = w
w = w / (self.max_values[-1] / 255)
self._white_value = w
# print(f'updated state {self.hs_color=} {self.brightness=}')
if update_state:
self.async_write_ha_state()
async def async_update(self):
"""
Эта штука нужна для синхронизации статуса вкл/выкл с реальностью. Если все цвета сброшены в ноль, значит мега
рестартнулась и не запомнила настройки, поэтому извещаем HA о выключении
Если вручную править цвет на стороне меги, тут изменения отражаться не будут
:return:
"""
if not self.enabled:
return
rgbw = []
for x in self.port:
data = self.coordinator.data
if not isinstance(data, dict):
return
data = data.get(x, None)
if isinstance(data, dict):
data = data.get('value')
data = safe_int(data)
if data is None:
return
rgbw.append(data)
if sum(rgbw) == 0:
self._is_on = False
self.async_write_ha_state()
def calc_speed_ratio(self, _before, _after):
ret = None
for i, x in enumerate(_before):
r = abs(x - _after[i]) / self.max_values[i]
if ret is None:
ret = r
else:
ret = max([r, ret])
return ret

View File

@@ -15,5 +15,5 @@
"@andvikt"
],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v0.6.1b1"
"version": "v1.0.1"
}

View File

@@ -1,6 +1,7 @@
"""Platform for light integration."""
import logging
import voluptuous as vol
import struct
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA,
@@ -19,7 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT
from .hub import MegaD
import re
@@ -94,6 +95,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
config_entry=config_entry,
**data,
)
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices)
@@ -101,16 +104,49 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
class MegaI2C(MegaPushEntity):
def __init__(self, *args, device_class: str, params: dict, **kwargs):
def __init__(
self,
*args,
device_class: str,
params: dict,
unit_of_measurement: str = None,
**kwargs
):
self._device_class = device_class
self._params = tuple(params.items())
self._unit_of_measurement = unit_of_measurement
super().__init__(*args, **kwargs)
@property
def customize(self):
return super().customize.get(self.id_suffix, {}) or {}
@property
def device_class(self):
return self._device_class
@property
def unit_of_measurement(self):
return self._unit_of_measurement
@property
def state(self):
return self.mega.values[self._params]
# 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))
try:
ret = float(ret)
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
except:
ret = ret
return str(ret)
@property
def device_class(self):
@@ -193,11 +229,20 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret)
except:
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
try:
ret = float(ret)
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
except:
pass
return str(ret)
@property
def name(self):
@@ -211,4 +256,4 @@ class Mega1WSensor(MegaPushEntity):
_constructors = {
'sensor': Mega1WSensor,
'i2c': MegaI2C,
}
}

View File

@@ -20,7 +20,9 @@
"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%]",
"update_time": "[%key:common::config_flow::data::update_time%]"
}
}
},

View File

@@ -58,6 +58,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data)
light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light)
async_add_devices(devices)

View File

@@ -57,7 +57,7 @@ class PriorityLock(asyncio.Lock):
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for _, w in self._waiters))):
all(w.cancelled() for _, _, w in self._waiters))):
self._locked = True
return True
@@ -115,3 +115,10 @@ class PriorityLock(asyncio.Lock):
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)
def map_reorder_rgb(rgb: list, from_: str, to_: str):
if from_ == to_:
return rgb
mapping = [from_.index(x) for x in to_]
return [rgb[x] for x in mapping]

View File

@@ -28,7 +28,8 @@
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs"
"poll_outs": "Poll outs",
"update_time": "Sync time"
}
}
}
@@ -47,7 +48,8 @@
"protected": "Protected",
"allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart",
"poll_outs": "Poll outs"
"poll_outs": "Poll outs",
"update_time": "Sync time"
}
}
}

View File

@@ -27,7 +27,8 @@
"protected": "Блокировать неразрешенные соединения",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"allow_hosts": "Разрешенные ip (через ;)",
"poll_outs": "Обновлять выходы (регулярно)"
"poll_outs": "Обновлять выходы (регулярно)",
"update_time": "Синхронизировать время"
}
}
}
@@ -48,7 +49,8 @@
"protected": "Блокировать неразрешенные соединения",
"allow_hosts": "Разрешенные ip (через ;)",
"restore_on_restart": "Восстанавливать выходы при перезагрузке",
"poll_outs": "Обновлять выходы (регулярно)"
"poll_outs": "Обновлять выходы (регулярно)",
"update_time": "Синхронизировать время"
}
}
}

View File

@@ -27,7 +27,8 @@
"protected": "Блокувати недозволені з'єднання",
"allow_hosts": "Дозволені ip (через ;)",
"restore_on_restart": "Відновлювати виходи при перезавантаженні",
"poll_outs": "Оновити виходи"
"poll_outs": "Оновити виходи",
"update_time": "Осинхронізувати час"
}
}
}

View File

@@ -20,16 +20,20 @@
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`)
- Поддержка rgb+w лент как с использованием диммеров, так и адресных лент на чипах ws28xx и подобных,
[подробнее про rgbw](https://github.com/andvikt/mega_hacs/wiki/rgbw)
- Плавное диммирование для любых диммируемых объектов (в том числе с аппаратной поддержкой и без),
[подробнее про smooth](https://github.com/andvikt/mega_hacs/wiki/smooth)
- Возможность работы с несколькими megad
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- Автоматическое добавление/изменение объектов после перезагрузки контроллера
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) в том числе несколько шиной (начиная с версии 0.4.1)
- поддержка расширителей MegaD-16I-XT, MegaD-16R-XT, MegaD-16PWM (начиная с версии 0.5.1)
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
[по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1)