Compare commits

...

38 Commits

Author SHA1 Message Date
Andrey
0fab2384b0 fix patt 2021-04-21 10:08:28 +03:00
Andrey
70a2e5bbb0 fix patt 2021-04-20 17:00:31 +03:00
Andrey
83c843d722 fix validation error 2021-04-20 09:23:36 +03:00
Andrey
af684a6c00 fix validation error 2021-04-20 09:19:37 +03:00
Andrey
d94efbe1a7 add ADS1115
add DEVICE_CLASS_CO2
2021-04-20 09:11:00 +03:00
Andrey
eaa46a99ae fix fw pattern
add dps368 support
fix docs url
2021-04-20 07:37:39 +03:00
Andrey
e65598fe63 fix int mega id 2021-04-20 07:35:59 +03:00
Andrey
e12ab45c04 fix int mega id 2021-04-20 07:35:06 +03:00
Andrey
7fb72be646 add skip sensors 2021-04-19 22:03:30 +03:00
Andrey
f9f97a91b6 add timeouts 2021-04-19 21:46:12 +03:00
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 922 additions and 425 deletions

View File

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

View File

@@ -6,23 +6,49 @@ from functools import partial
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass from homeassistant.helpers.service import bind_hass
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \ from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE CONF_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 .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__)
_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 = { 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,
@@ -46,6 +72,16 @@ CUSTOMIZE_PORT = {
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, 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 = { CUSTOMIZE_DS2413 = {
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
@@ -63,17 +99,18 @@ CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str], vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Required(str, description='id меги из веб-интерфейса'): { vol.Optional(vol.Any(str, int), description='id меги из веб-интерфейса'): {
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool, vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional( vol.Optional(
CONF_DEF_RESPONSE, CONF_DEF_RESPONSE,
description='Ответ по умолчанию', description='Ответ по умолчанию',
default=None default=None
): vol.Any(cv.template, None), ): vol.Any(cv.template, None),
vol.Optional(CONF_LED): LED_LIGHT,
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any( vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
CUSTOMIZE_PORT, CUSTOMIZE_PORT,
CUSTOMIZE_DS2413, CUSTOMIZE_DS2413,
) ),
} }
} }
}, },
@@ -121,18 +158,7 @@ async def get_hub(hass, entry):
data = dict(entry.data) data = dict(entry.data)
data.update(entry.options or {}) data.update(entry.options or {})
data.update(id=id) data.update(id=id)
use_mqtt = data.get(CONF_MQTT_INPUTS, True) hub = MegaD(hass, config=entry, **data, lg=_LOGGER, loop=asyncio.get_event_loop())
_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.mqtt_id = await hub.get_mqtt_id() hub.mqtt_id = await hub.get_mqtt_id()
return hub return hub
@@ -172,18 +198,14 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
:param entry: :param entry:
:return: :return:
""" """
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]] # hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL] # hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0) # hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options await hass.config_entries.async_reload(entry.entry_id)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True return True
async def async_remove_entry(hass, entry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle removal of an entry.""" """Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hub: MegaD = hass.data[DOMAIN].get(id) hub: MegaD = hass.data[DOMAIN].get(id)
@@ -193,12 +215,17 @@ async def async_remove_entry(hass, entry) -> None:
_hubs.pop(id, None) _hubs.pop(id, None)
hass.data[DOMAIN].pop(id, None) hass.data[DOMAIN].pop(id, None)
hass.data[DOMAIN][CONF_ALL].pop(id, None) hass.data[DOMAIN][CONF_ALL].pop(id, None)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
task: asyncio.Task = _POLL_TASKS.pop(id, None) task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None: if task is not None:
task.cancel() task.cancel()
if hub is None: if hub is None:
return return
await hub.stop() await hub.stop()
return True
async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_migrate_entry(hass, config_entry: ConfigEntry):

View File

@@ -57,6 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
continue continue
hub.lg.debug(f'add binary_sensor on port %s', port) hub.lg.debug(f'add binary_sensor on port %s', port)
sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry) sensor = MegaBinarySensor(mega=hub, port=port, config_entry=config_entry)
if '<' in sensor.name:
continue
devices.append(sensor) devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
@@ -95,20 +97,4 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def _update(self, payload: dict): def _update(self, payload: dict):
self.mega.values[self.port] = payload self.mega.values[self.port] = payload
if not self.mega.mqtt_inputs:
return
template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None)
if template is not None:
template.hass = self.hass
ret = template.async_render(payload)
self.mega.lg.debug(f'response: %s', ret)
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd=ret)
)
elif self.mega.force_d:
self.mega.lg.debug(f'response d')
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd='d')
)

View File

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

View File

@@ -44,6 +44,9 @@ def parse_config(page: str):
v = page.find('input', attrs={'name': x}) v = page.find('input', attrs={'name': x})
if v: if v:
ret[x] = v['value'] 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) return Config(**ret, src=page)

View File

@@ -1,5 +1,6 @@
"""Constants for the mega integration.""" """Constants for the mega integration."""
import re import re
from itertools import permutations
DOMAIN = "mega" DOMAIN = "mega"
CONF_MEGA_ID = "mega_id" CONF_MEGA_ID = "mega_id"
@@ -34,6 +35,16 @@ CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart' CONF_RESTORE_ON_RESTART = 'restore_on_restart'
CONF_CLICK_TIME = 'click_time' CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_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 = [ PLATFORMS = [
"light", "light",
"switch", "switch",
@@ -53,4 +64,18 @@ LUX = 'lux'
SINGLE_CLICK = 'single' SINGLE_CLICK = 'single'
DOUBLE_CLICK = 'double' 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 logging
import asyncio import asyncio
import time
import typing
from datetime import timedelta
from functools import partial
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME 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 homeassistant.helpers.restore_state import RestoreEntity
from . import hub as h from . import hub as h
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \ 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 _events_on = False
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -38,7 +42,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def __init__( def __init__(
self, self,
mega: 'h.MegaD', mega: 'h.MegaD',
port: int, port: typing.Union[int, str, typing.List[int]],
config_entry: ConfigEntry = None, config_entry: ConfigEntry = None,
id_suffix=None, id_suffix=None,
name=None, name=None,
@@ -46,11 +50,13 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
http_cmd='get', http_cmd='get',
addr: str=None, addr: str=None,
index=None, index=None,
customize=None,
smooth=None,
**kwargs,
): ):
super().__init__(mega.updater) super().__init__(mega.updater)
self._smooth = smooth
self.http_cmd = http_cmd 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
@@ -58,18 +64,68 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
mega.entities.append(self) mega.entities.append(self)
self._mega_id = mega.id self._mega_id = mega.id
self._lg = None self._lg = None
if not isinstance(port, list):
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \ self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._name = name or f"{mega.id}_{port}" + \ _pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port
self._name = name or f"{mega.id}_{_pt}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None self._customize: dict = None
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.index = index
self.addr = addr self.addr = addr
self.id_suffix = id_suffix
self._can_smooth_hard = None
if self.http_cmd == 'ds2413': if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port} 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 @property
def customize(self): def customize(self):
if self._customize is not None:
return self._customize
if self.hass is None: if self.hass is None:
return {} return {}
if self._customize is None: if self._customize is None:
@@ -85,15 +141,23 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@property @property
def device_info(self): 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 { return {
"identifiers": { "identifiers": {
# Serial numbers are unique identifiers within a specific domain # 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": [ "config_entries": [
self.config_entry, 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', "manufacturer": 'ab-log.ru',
# "model": self.light.productname, # "model": self.light.productname,
"sw_version": self.mega.fw, "sw_version": self.mega.fw,
@@ -114,7 +178,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def name(self): def name(self):
c = self.customize.get(CONF_NAME) c = self.customize.get(CONF_NAME)
if not isinstance(c, str): if not isinstance(c, str):
c = self._name or f"{self.mega.id}_p{self.port}" 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 return c
@property @property
@@ -125,12 +193,9 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
global _task_set_ev_on global _task_set_ev_on
await super().async_added_to_hass() await super().async_added_to_hass()
self._state = await self.async_get_last_state() self._state = await self.async_get_last_state()
if self.mega.mqtt_inputs and _task_set_ev_on is None:
_task_set_ev_on = asyncio.create_task(_set_events_on())
async def get_state(self): async def get_state(self):
self.lg.debug(f'state is %s', self.state) 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()
@@ -147,11 +212,10 @@ class MegaPushEntity(BaseMegaEntity):
def __update(self, value: dict): def __update(self, value: dict):
self._update(value) self._update(value)
if self.hass is None:
return
self.async_write_ha_state() self.async_write_ha_state()
self.lg.debug(f'state after update %s', self.state) self.lg.debug(f'state after update %s', self.state)
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'): if not self.entity_id.startswith('binary_sensor'):
_LOGGER.debug('skip event because not a bnary sens') _LOGGER.debug('skip event because not a bnary sens')
return return
@@ -203,11 +267,6 @@ class MegaPushEntity(BaseMegaEntity):
def _update(self, payload: dict): def _update(self, payload: dict):
pass 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): class MegaOutPort(MegaPushEntity):
@@ -224,11 +283,24 @@ class MegaOutPort(MegaPushEntity):
self._is_on = None self._is_on = None
self.dimmer = dimmer self.dimmer = dimmer
self.dimmer_scale = dimmer_scale 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 # @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
def max_dim(self):
if self.dimmer_scale == 1:
return 255
elif self.dimmer == 16:
return 4095
else:
return 255
@property @property
def invert(self): def invert(self):
return self.customize.get(CONF_INVERT, False) return self.customize.get(CONF_INVERT, False)
@@ -306,18 +378,73 @@ class MegaOutPort(MegaPushEntity):
else: else:
return self.port 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 brightness = brightness or self.brightness or 255
_prev = safe_int(self.brightness) or 0
self._brightness = brightness self._brightness = brightness
if self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
cmd = 255 * self.dimmer_scale cmd = self.max_dim
elif self.dimmer: 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: else:
cmd = 1 if not self.invert else 0 cmd = 1 if not self.invert else 0
if transition is None:
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"} _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: if self.addr:
_cmd['addr'] = self.addr _cmd['addr'] = self.addr
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1) await self.mega.request(**_cmd, priority=-1)
if self.index is not None: if self.index is not None:
# обновление текущего стейта для ds2413 # обновление текущего стейта для ds2413
@@ -336,13 +463,26 @@ class MegaOutPort(MegaPushEntity):
self.mega.values[self.port] = {'value': cmd} self.mega.values[self.port] = {'value': cmd}
await self.get_state() await self.get_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, 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 = "0" if not self.invert else "1"
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"} _cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
_prev = safe_int(self.brightness) or 0
if self.addr: if self.addr:
_cmd['addr'] = self.addr _cmd['addr'] = self.addr
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1) await self.mega.request(**_cmd, priority=-1)
else:
self._set_dim_brightness(
from_=_prev,
to_=0,
transition=transition,
)
if self.index is not None: if self.index is not None:
# обновление текущего стейта для ds2413 # обновление текущего стейта для ds2413
await self.mega.get_port( await self.mega.get_port(
@@ -357,6 +497,11 @@ class MegaOutPort(MegaPushEntity):
self.mega.values[self.port] = {'value': cmd} self.mega.values[self.port] = {'value': cmd}
await self.get_state() 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): def safe_int(v):
if v == 'ON': if v == 'ON':

View File

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

View File

@@ -77,7 +77,11 @@ class MegaView(HomeAssistantView):
if hub is None: if hub is None:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}') _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']: 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: elif hub is None:
return Response(status=400) return Response(status=400)
data = dict(request.query) data = dict(request.query)
@@ -87,8 +91,8 @@ 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: if data.get('st') == '1':
asyncio.create_task(self.later_restore(hub)) hass.async_create_task(self.later_restore(hub))
return Response(status=200) return Response(status=200)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
@@ -158,10 +162,11 @@ class MegaView(HomeAssistantView):
:return: :return:
""" """
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
if hub.restore_on_restart:
await hub.restore_states() await hub.restore_states()
await hub.reload()
async def later_update(self, hub): async def later_update(self, hub):
await asyncio.sleep(1) await asyncio.sleep(1)
_LOGGER.debug('force update') _LOGGER.debug('force update')
await hub.updater.async_refresh() await hub.updater.async_refresh()

View File

@@ -9,7 +9,7 @@ import re
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components import mqtt from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX 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 ( 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_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 .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock from .tools import make_ints, int_ignore, PriorityLock
@@ -66,10 +67,9 @@ class MegaD:
loop: asyncio.AbstractEventLoop, loop: asyncio.AbstractEventLoop,
host: str, host: str,
password: str, password: str,
mqtt: mqtt.MQTT,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
mqtt_inputs: bool = True, config: ConfigEntry = None,
mqtt_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
@@ -85,10 +85,16 @@ class MegaD:
ext_in=None, ext_in=None,
ext_acts=None, ext_acts=None,
i2c_sensors=None, i2c_sensors=None,
new_naming=False,
update_time=False,
smooth: list=None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False: self.skip_ports = set()
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) 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}
@@ -97,22 +103,21 @@ class MegaD:
self.http.hubs['__def'] = self self.http.hubs['__def'] = self
if mqtt_id: if mqtt_id:
self.http.hubs[mqtt_id] = self self.http.hubs[mqtt_id] = self
else: self.smooth = smooth or []
self.http = None self.new_naming = new_naming
self.extenders = extenders or [] self.extenders = extenders or []
self.ext_in = ext_in or {} self.ext_in = ext_in or {}
self.ext_act = ext_acts or {} self.ext_act = ext_acts or {}
self.i2c_sensors = i2c_sensors or [] self.i2c_sensors = i2c_sensors or []
self._update_time = update_time
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.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
self.host = host self.host = host
self.sec = password self.sec = password
self.mqtt = mqtt
self.id = id self.id = id
self.lck = asyncio.Lock() self.lck = asyncio.Lock()
self.last_long = {} self.last_long = {}
@@ -153,23 +158,15 @@ class MegaD:
if force_d is not None: if force_d is not None:
self.customize[CONF_FORCE_D] = force_d self.customize[CONF_FORCE_D] = force_d
try: try:
if allow_hosts is not None: if allow_hosts is not None and DOMAIN in hass.data:
allow_hosts = set(allow_hosts.split(';')) allow_hosts = set(allow_hosts.split(';'))
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
hass.data[DOMAIN][CONF_HTTP].protected = protected hass.data[DOMAIN][CONF_HTTP].protected = protected
except Exception: except Exception:
self.lg.exception('while setting allowed hosts') self.lg.exception('while setting allowed hosts')
async def start(self): async def start(self):
self.loop = asyncio.get_event_loop() pass
if self.mqtt is not None:
set_events_off()
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
async def stop(self): async def stop(self):
if self.subs is not None: if self.subs is not None:
@@ -189,7 +186,10 @@ class MegaD:
continue continue
if x.port in ports: if x.port in ports:
continue continue
try:
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd) await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
except asyncio.TimeoutError:
continue
ports.append(x.port) ports.append(x.port)
@property @property
@@ -236,39 +236,39 @@ class MegaD:
""" """
for x in self.ds2413_ports: for x in self.ds2413_ports:
self.lg.debug(f'poll ds2413 for %s', x) self.lg.debug(f'poll ds2413 for %s', x)
try:
await self.get_port( await self.get_port(
port=x, port=x,
force_http=True, force_http=True,
http_cmd='list', http_cmd='list',
conv=False conv=False
) )
except asyncio.TimeoutError:
continue
async def poll(self): async def poll(self):
""" """
Polling ports Polling ports
""" """
self.lg.debug('poll') self.lg.debug('poll')
if self._update_time:
await self.update_time()
for x in self.i2c_sensors: for x in self.i2c_sensors:
if not isinstance(x, dict): if not isinstance(x, dict):
continue continue
ret = await self._update_i2c(x) ret = await self._update_i2c(x)
if isinstance(ret, dict): if isinstance(ret, dict):
self.values.update(ret) self.values.update(ret)
for x in self.extenders: for x in self.extenders:
ret = await self._update_extender(x) ret = await self._update_extender(x)
if not isinstance(ret, dict): if not isinstance(ret, dict):
self.lg.warning(f'wrong updater result: {ret} from extender {x}') self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue continue
self.values.update(ret) self.values.update(ret)
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)
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_ds2413() await self._get_ds2413()
return self.values return self.values
@@ -297,7 +297,9 @@ class MegaD:
url = f"{url}/?{cmd}" url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url) self.lg.debug('request: %s', url)
async with self._http_lck(priority): async with self._http_lck(priority):
async with aiohttp.request("get", url=url) as req: for _ntry in range(3):
try:
async with aiohttp.request("get", url=url, timeout=aiohttp.ClientTimeout(total=5)) as req:
if req.status != 200: if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text()) self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return None return None
@@ -305,6 +307,11 @@ class MegaD:
ret = await req.text() ret = await req.text()
self.lg.debug('response %s', ret) self.lg.debug('response %s', ret)
return ret return ret
except asyncio.TimeoutError:
self.lg.warning(f'timeout while requesting {url}')
raise
# await asyncio.sleep(1)
raise asyncio.TimeoutError('after 3 tries')
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
@@ -338,7 +345,6 @@ class MegaD:
хранилище values хранилище values
""" """
self.lg.debug(f'get port %s', port) self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http:
if 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)
@@ -352,28 +358,16 @@ class MegaD:
self.values[port] = ret self.values[port] = ret
return 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 @property
def ports(self): def ports(self):
return {e.port for e in self.entities} return {e.port for e in self.entities}
async def get_all_ports(self, only_out=False, check_skip=False): async def get_all_ports(self, only_out=False, check_skip=False):
if not self.mqtt_inputs: try:
ret = await self.request(cmd='all') ret = await self.request(cmd='all')
except asyncio.TimeoutError:
return
for port, x in enumerate(ret.split(';')): for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports: if port in self.ds2413_ports:
continue continue
@@ -381,12 +375,6 @@ class MegaD:
continue continue
ret = self.parse_response(x) ret = self.parse_response(x)
self.values[port] = ret self.values[port] = ret
elif not check_skip:
for x in range(self.nports + 1):
await self.get_port(x)
else:
for x in self.ports:
await self.get_port(x)
async def reboot(self, save=True): async def reboot(self, save=True):
await self.save() await self.save()
@@ -440,9 +428,6 @@ class MegaD:
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, callback 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: async def authenticate(self) -> bool:
@@ -464,45 +449,6 @@ class MegaD:
async def scan_port(self, port): async def scan_port(self, port):
data = await self.request(pt=port) data = await self.request(pt=port)
return parse_config(data) 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): async def scan_ports(self, nports=37):
for x in range(0, nports+1): for x in range(0, nports+1):
@@ -517,7 +463,10 @@ class MegaD:
:param port: :param port:
:return: :return:
""" """
try:
values = await self.request(pt=port, cmd='get') values = await self.request(pt=port, cmd='get')
except asyncio.TimeoutError:
return
ret = {} ret = {}
for i, x in enumerate(values.split(';')): for i, x in enumerate(values.split(';')):
ret[f'{port}e{i}'] = x ret[f'{port}e{i}'] = x
@@ -529,10 +478,26 @@ class MegaD:
:param params: параметры url :param params: параметры url
:return: :return:
""" """
pt = params.get('pt')
if pt in self.skip_ports:
return
if pt is not None:
pass
_params = tuple(params.items()) _params = tuple(params.items())
return { delay = None
if 'delay' in params:
delay = params.pop('delay')
try:
ret = {
_params: await self.request(**params) _params: await self.request(**params)
} }
except asyncio.TimeoutError:
return
self.lg.debug('i2c response: %s', ret)
if delay:
self.lg.debug('delay %s', delay)
await asyncio.sleep(delay)
return ret
async def get_config(self, nports=37): async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list)) ret = defaultdict(lambda: defaultdict(list))
@@ -541,11 +506,17 @@ class MegaD:
ret['ext_in'] = ext_int = {} ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {} ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = [] ret['i2c_sensors'] = i2c_sensors = []
ret['smooth'] = smooth = []
async for port, cfg in self.scan_ports(nports): async for port, cfg in self.scan_ports(nports):
_cust = self.customize.get(port)
if not isinstance(_cust, dict):
_cust = {}
if cfg.pty == "0": if cfg.pty == "0":
ret['binary_sensor'][port].append({}) ret['binary_sensor'][port].append({})
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None): 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: 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)
@@ -567,7 +538,7 @@ class MegaD:
for n in range(len(values)): for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n) ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page) 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': if ext_cfg.ety == '1':
ret['light'][pt].append({}) ret['light'][pt].append({})
elif ext_cfg.ety == '0': elif ext_cfg.ety == '0':
@@ -579,8 +550,10 @@ class MegaD:
values = await self.request(pt=port, cmd='get') values = await self.request(pt=port, cmd='get')
values = values.split(';') values = values.split(';')
for n in range(len(values)): for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16}) pt = f'{port}e{n}'
elif cfg.pty == '4' and cfg.gr == '0': 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 # i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan') scan = cfg.src.find('a', text='I2C Scan')
self.lg.debug(f'find scan link: %s', scan) self.lg.debug(f'find scan link: %s', scan)
@@ -590,6 +563,9 @@ class MegaD:
self.lg.debug(f'scan results: %s', (req, parsed)) self.lg.debug(f'scan results: %s', (req, parsed))
ret['i2c'][port].extend(parsed) ret['i2c'][port].extend(parsed)
i2c_sensors.extend(req) i2c_sensors.extend(req)
elif cfg.pty == '4' and cfg.m == '2':
# scl исключаем из сканирования
continue
elif cfg.pty in ('3', '2', '4'): elif cfg.pty in ('3', '2', '4'):
http_cmd = 'get' http_cmd = 'get'
if cfg.d == '5' and cfg.pty == '3': if cfg.d == '5' and cfg.pty == '3':
@@ -633,4 +609,102 @@ class MegaD:
else: else:
await x.async_turn_off() 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 urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.const import ( from homeassistant.const import (
@@ -5,7 +6,16 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_CO2,
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): def parse_scan_page(page: str):
@@ -27,12 +37,17 @@ def parse_scan_page(page: str):
elif c is Request: elif c is Request:
req.append(params) req.append(params)
continue continue
elif isinstance(c, tuple): elif isinstance(c, Request):
suffix, c = c if c.delay:
elif isinstance(c, str): params = params.copy()
suffix = c params['delay'] = c.delay
req.append(params)
continue
elif isinstance(c, DeviceType):
c, m, suffix = c
else: else:
suffix = '' continue
suffix = suffix or c
if 'addr' in params: if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr']) suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix: if suffix:
@@ -42,10 +57,12 @@ def parse_scan_page(page: str):
params = params.copy() params = params.copy()
if i > 0: if i > 0:
params['i2c_par'] = i params['i2c_par'] = i
ret.append({ ret.append({
'id_suffix': _dev, 'id_suffix': _dev,
'device_class': c, 'device_class': c,
'params': params, 'params': params,
'unit_of_measurement': m,
}) })
req.append(params) req.append(params)
return req, ret return req, ret
@@ -55,55 +72,73 @@ class Skip:
pass pass
@dataclass
class Request: class Request:
pass delay: float = None
i2c_classes = { i2c_classes = {
'htu21d': [ 'htu21d': [
DEVICE_CLASS_HUMIDITY, DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'sht31': [ 'sht31': [
DEVICE_CLASS_HUMIDITY, DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'max44009': [ 'max44009': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'bh1750': [ 'bh1750': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'tsl2591': [ 'tsl2591': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'bmp180': [ 'bmp180': [
DEVICE_CLASS_PRESSURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'bmx280': [ 'bmx280': [
DEVICE_CLASS_PRESSURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DEVICE_CLASS_HUMIDITY DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None)
],
'dps368': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mlx90614': [ 'mlx90614': [
Skip, Skip,
('temp', DEVICE_CLASS_TEMPERATURE), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'),
('object', DEVICE_CLASS_TEMPERATURE), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'),
], ],
'ptsensor': [ 'ptsensor': [
Request, # запрос на измерение Skip,
DEVICE_CLASS_PRESSURE, Request(delay=1), # запрос на измерение
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mcp9600': [ 'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
], ],
't67xx': [ 't67xx': [
None # для co2 нет класса в HA DeviceType(DEVICE_CLASS_CO2, CONCENTRATION_PARTS_PER_MILLION, None)
], ],
'tmp117': [ 'tmp117': [
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
] ],
'ads1115': [
DeviceType(None, None, 'ch0'),
DeviceType(None, None, 'ch1'),
DeviceType(None, None, 'ch2'),
DeviceType(None, None, 'ch3'),
],
'ads1015': [
DeviceType(None, None, 'ch0'),
DeviceType(None, None, 'ch1'),
DeviceType(None, None, 'ch2'),
DeviceType(None, None, 'ch3'),
],
} }

View File

@@ -1,11 +1,20 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import logging import logging
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol import voluptuous as vol
import colorsys
import time
from homeassistant.components.light import ( from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_SCHEMA, PLATFORM_SCHEMA as LIGHT_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
LightEntity, LightEntity,
SUPPORT_TRANSITION,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -16,7 +25,7 @@ from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import MegaOutPort from .entities import MegaOutPort, BaseMegaEntity, safe_int
from .hub import MegaD from .hub import MegaD
from .const import ( from .const import (
@@ -24,12 +33,12 @@ from .const import (
CONF_SWITCH, CONF_SWITCH,
DOMAIN, DOMAIN,
CONF_CUSTOM, 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__) lg = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
# Validation of the user's configuration # Validation of the user's configuration
_EXTENDED = { _EXTENDED = {
@@ -60,16 +69,32 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) 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(): for port, cfg in config_entry.data.get('light', {}).items():
port = int_ignore(port) port = int_ignore(port)
c = customize.get(mid, {}).get(port, {}) c = customize.get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light': if c.get(CONF_SKIP, False) or port in skip or c.get(CONF_DOMAIN, 'light') != 'light':
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data) hub.lg.debug(f'add light on port %s with data %s', port, data)
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data) light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)
@@ -77,5 +102,216 @@ class MegaLight(MegaOutPort, LightEntity):
@property @property
def supported_features(self): def supported_features(self):
return SUPPORT_BRIGHTNESS if self.dimmer else 0 return (
(SUPPORT_BRIGHTNESS if self.dimmer else 0) |
(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

@@ -2,7 +2,7 @@
"domain": "mega", "domain": "mega",
"name": "mega", "name": "mega",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mega_hacs", "documentation": "https://github.com/andvikt/mega_hacs",
"requirements": [ "requirements": [
"beautifulsoup4", "beautifulsoup4",
"lxml" "lxml"
@@ -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.6.1b1" "version": "v1.0.6b4"
} }

View File

@@ -1,6 +1,7 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import voluptuous as vol import voluptuous as vol
import struct
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA, PLATFORM_SCHEMA as SENSOR_SCHEMA,
@@ -19,7 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .entities import MegaPushEntity 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, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .hub import MegaD from .hub import MegaD
import re import re
@@ -83,9 +84,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
for tp in ['sensor', 'i2c']: for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items(): for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port) port = int_ignore(port)
c = customize.get(port, {})
if c.get(CONF_SKIP):
hub.skip_ports |= {port}
continue
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 = _constructors[tp]( sensor = _constructors[tp](
@@ -94,6 +100,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
config_entry=config_entry, config_entry=config_entry,
**data, **data,
) )
if '<' in sensor.name:
continue
devices.append(sensor) devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
@@ -101,16 +109,49 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
class MegaI2C(MegaPushEntity): 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._device_class = device_class
self._params = tuple(params.items()) self._params = tuple(params.items())
self._unit_of_measurement = unit_of_measurement
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property
def customize(self):
return super().customize.get(self.id_suffix, {}) or {}
@property
def device_class(self): def device_class(self):
return self._device_class return self._device_class
@property
def unit_of_measurement(self):
return self._unit_of_measurement
@property
def state(self): 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 @property
def device_class(self): def device_class(self):
@@ -193,11 +234,20 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret) ret = str(ret)
except: except:
ret = None ret = None
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) 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: 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})
return ret except:
pass
return str(ret)
@property @property
def name(self): def name(self):

View File

@@ -20,7 +20,9 @@
"protected": "[%key:common::config_flow::data::protected%]", "protected": "[%key:common::config_flow::data::protected%]",
"allow_hosts": "[%key:common::config_flow::data::allow_hosts%]", "allow_hosts": "[%key:common::config_flow::data::allow_hosts%]",
"restore_on_restart": "[%key:common::config_flow::data::restore_on_restart%]", "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: for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data) hub.lg.debug(f'add switch on port %s with data %s', port, data)
light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data) light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)

View File

@@ -57,7 +57,7 @@ class PriorityLock(asyncio.Lock):
locked and returns True. locked and returns True.
""" """
if (not self._locked and (self._waiters is None or 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 self._locked = True
return True return True
@@ -115,3 +115,10 @@ class PriorityLock(asyncio.Lock):
# taken already, will hit this again and wake up a new waiter. # taken already, will hit this again and wake up a new waiter.
if not fut.done(): if not fut.done():
fut.set_result(True) 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", "protected": "Protected",
"allow_hosts": "Allowed hosts", "allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart", "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", "protected": "Protected",
"allow_hosts": "Allowed hosts", "allow_hosts": "Allowed hosts",
"restore_on_restart": "Restore outs on restart", "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": "Блокировать неразрешенные соединения", "protected": "Блокировать неразрешенные соединения",
"restore_on_restart": "Восстанавливать выходы при перезагрузке", "restore_on_restart": "Восстанавливать выходы при перезагрузке",
"allow_hosts": "Разрешенные ip (через ;)", "allow_hosts": "Разрешенные ip (через ;)",
"poll_outs": "Обновлять выходы (регулярно)" "poll_outs": "Обновлять выходы (регулярно)",
"update_time": "Синхронизировать время"
} }
} }
} }
@@ -48,7 +49,8 @@
"protected": "Блокировать неразрешенные соединения", "protected": "Блокировать неразрешенные соединения",
"allow_hosts": "Разрешенные ip (через ;)", "allow_hosts": "Разрешенные ip (через ;)",
"restore_on_restart": "Восстанавливать выходы при перезагрузке", "restore_on_restart": "Восстанавливать выходы при перезагрузке",
"poll_outs": "Обновлять выходы (регулярно)" "poll_outs": "Обновлять выходы (регулярно)",
"update_time": "Синхронизировать время"
} }
} }
} }

View File

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

View File

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