Compare commits

..

51 Commits

Author SHA1 Message Date
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
Andrey
f784c1c261 edit readme 2021-03-03 13:15:05 +03:00
Andrey
ab572c4db5 edit readme 2021-03-03 12:34:08 +03:00
Andrey
e55184f565 edit readme 2021-03-03 12:20:44 +03:00
Andrey
3f74f9739c edit readme 2021-03-03 12:20:09 +03:00
Andrey
49fcf880d9 add i2c sensors 2021-03-03 12:01:02 +03:00
Andrey
289f52ef73 fix errors 2021-02-28 22:27:57 +03:00
Andrey
ce589c97b9 fix errors 2021-02-28 22:22:17 +03:00
Andrey
22a6f8f444 fix errors 2021-02-28 22:06:36 +03:00
Andrey
9a53de1d5d fix errors 2021-02-28 22:02:22 +03:00
Andrey
bd8b07dd90 fix errors 2021-02-28 21:52:34 +03:00
Andrey
d9b6ba3a50 fix errors 2021-02-28 21:52:06 +03:00
Andrey
1042592a31 fix errors 2021-02-28 21:33:26 +03:00
Andrey
137eb8b6ba fix errors 2021-02-28 21:15:48 +03:00
Andrey
a2f412b89e fix errors 2021-02-28 20:16:04 +03:00
Andrey
8fa14cdbc5 fix errors 2021-02-28 20:15:47 +03:00
Andrey
fc17b82021 support response for extenders 2021-02-28 15:01:39 +03:00
Andrey
1aeaabfb3c fix errors 2021-02-28 14:50:47 +03:00
Andrey
a0bd8acac0 fix errors 2021-02-28 14:49:38 +03:00
andvikt
c48a3632d2 Update http.py 2021-02-28 13:04:00 +03:00
Andrey
e06ba65ead fix errors 2021-02-28 09:49:16 +03:00
Andrey
22720a27bd fix errors 2021-02-28 09:46:06 +03:00
Andrey
d0769b5b02 fix errors 2021-02-27 17:39:54 +03:00
Andrey
9ae093dd91 fix errors 2021-02-27 14:44:20 +03:00
Andrey
f88109c3a6 fix int(port) 2021-02-26 16:18:05 +03:00
21 changed files with 1222 additions and 355 deletions

View File

@@ -1,43 +1,15 @@
import re
order ='brg'
rgb = 'rgb'
map_to_order = [rgb.index(x) for x in order]
map_from_order = [order.index(x) for x in rgb]
PATT_FW = re.compile(r'fw:\s(.+)\)')
data = """
<html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
<head></head>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
MegaD-2561 by
<a href="http://ab-log.ru">ab-log.ru</a>
(fw: 4.48b7)
<br>
<a href="/sec/?cf=1">Config</a>
<br>
-- MODS --
<br>
<a href="/sec/?cf=3">XP1</a>
<br>
<a href="/sec/?cf=4">XP2</a>
<br>
-- XT2 --
<br>
<a href="/sec/?pt=30">P30 - OUT</a>
<br>
<a href="/sec/?pt=31">P31 - OUT</a>
<br>
<a href="/sec/?pt=32">P32 - IN</a>
<br>
<a href="/sec/?pt=33">P33 - I2C/SCL</a>
<br>
<a href="/sec/?pt=34">P34 - DS</a>
<br>
<a href="/sec/?pt=35">P35 - NC</a>
<br>
-- XP5/6 --
<br>
<a href="/sec/?pt=36">P36 - ADC</a>
<br>
<a href="/sec/?pt=37">P37 - NC</a>
<body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body>
<html><head></head><body>MegaD-2561 by <a href="http://ab-log.ru">ab-log.ru</a> (fw: 4.48b7)<br><a href="/sec/?cf=1">Config</a><br>-- MODS --<br><a href="/sec/?cf=3">XP1</a><br><a href="/sec/?cf=4">XP2</a><br>-- XT2 --<br><a href="/sec/?pt=30">P30 - OUT</a><br><a href="/sec/?pt=31">P31 - OUT</a><br><a href="/sec/?pt=32">P32 - IN</a><br><a href="/sec/?pt=33">P33 - I2C/SCL</a><br><a href="/sec/?pt=34">P34 - DS</a><br><a href="/sec/?pt=35">P35 - NC</a><br>-- XP5/6 --<br><a href="/sec/?pt=36">P36 - ADC</a><br><a href="/sec/?pt=37">P37 - NC</a></body></html>
"""
print(PATT_FW.search(data).groups()[0])
_rgb = [
rgb[x] for x in map_to_order
]
_order = [
_rgb[x] for x in map_from_order
]
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_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,
@@ -34,6 +60,10 @@ CUSTOMIZE_PORT = {
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(CONF_DEVICE_CLASS):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional(
CONF_RESPONSE_TEMPLATE,
description='шаблон ответа когда на этот порт приходит'
@@ -42,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
@@ -66,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,
)
),
}
}
},
@@ -117,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
@@ -168,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)
@@ -189,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

@@ -20,7 +20,7 @@ from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT, CONF_RESPONSE_TEMPLATE
from .entities import MegaPushEntity
from .hub import MegaD
from .tools import int_ignore
lg = logging.getLogger(__name__)
@@ -51,12 +51,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('binary_sensor', {}).items():
port = int(port)
port = int_ignore(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False):
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)
@@ -67,6 +69,10 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
super().__init__(*args, **kwargs)
self._is_on = None
self._attrs = None
self._click_task = None
async def _click(self):
await self.customize.get
@property
def state_attributes(self):
@@ -78,32 +84,17 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
@property
def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}).get("value") \
or self.mega.values.get(self.port, {}).get('m')
val = self.mega.values.get(self.port, {})
if isinstance(val, dict):
val = val.get("value", val.get('m'))
if val is None and self._state is not None:
return self._state == 'ON'
elif val is not None:
if val in ['ON', 'OFF']:
return val == 'ON' if not self.invert else val == 'OFF'
else:
if val in ['ON', 'OFF', '1', '0']:
return val in ['ON', '1'] if not self.invert else val in ['OFF', '0']
elif isinstance(val, int):
return val != 1 if not self.invert else val == 1
def _update(self, payload: dict):
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 = 12
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, self.config_entry.data)
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

@@ -1,6 +1,19 @@
from dataclasses import dataclass, field
from bs4 import BeautifulSoup
inputs = [
'eact',
'inta',
'misc',
]
selectors = [
'pty',
'm',
'gr',
'd',
'ety',
]
@dataclass(frozen=True, eq=True)
class Config:
@@ -8,21 +21,17 @@ class Config:
m: str = None
gr: str = None
d: str = None
inta: str = field(compare=False, hash=False, default=None)
ety: str = None
inta: str = field(compare=False, hash=False, default=None)
misc: str = field(compare=False, hash=False, default=None)
eact: str = field(compare=False, hash=False, default=None)
src: BeautifulSoup = field(compare=False, hash=False, default=None)
def parse_config(page: str):
page = BeautifulSoup(page, features="lxml")
ret = {}
for x in [
'pty',
'm',
'gr',
'd',
'ety',
]:
for x in selectors:
v = page.find('select', attrs={'name': x})
if v is None:
continue
@@ -31,13 +40,14 @@ def parse_config(page: str):
if v:
v = v['value']
ret[x] = v
v = page.find('input', attrs={'name': 'inta'})
if v:
ret['inta'] = v['value']
v = page.find('input', attrs={'name': 'misc'})
if v:
ret['misc'] = v.get('checked', False)
return Config(**ret)
for x in inputs:
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)
DIGITAL_IN = Config(pty="0")

View File

@@ -1,5 +1,6 @@
"""Constants for the mega integration."""
import re
from itertools import permutations
DOMAIN = "mega"
CONF_MEGA_ID = "mega_id"
@@ -32,6 +33,18 @@ CONF_POLL_OUTS = 'poll_outs'
CONF_FORCE_D = 'force_d'
CONF_DEF_RESPONSE = 'def_response'
CONF_RESTORE_ON_RESTART = 'restore_on_restart'
CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time'
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
CONF_UPDATE_TIME = 'update_time'
CONF_HEX_TO_FLOAT = 'hex_to_float'
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",
@@ -51,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)
@@ -247,8 +319,8 @@ class MegaOutPort(MegaPushEntity):
val = 0
if val == 0:
return self._brightness
else:
return val
elif isinstance(val, (int, float)):
return int(val / self.dimmer_scale)
elif val is not None:
val = val.get("value")
if val is None:
@@ -269,7 +341,8 @@ class MegaOutPort(MegaPushEntity):
return
if self.dimmer:
val = safe_int(val)
return val > 0 if not self.invert else val == 0
if val is not None:
return val > 0 if not self.invert else val == 0
else:
return val == 'ON' if not self.invert else val == 'OFF'
elif val is not None:
@@ -305,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)
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(
@@ -335,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)
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(
@@ -356,10 +497,17 @@ 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 in ['ON', 'OFF']:
return None
if v == 'ON':
return 1
elif v == 'OFF':
return 0
try:
return int(v)
except (ValueError, TypeError):

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

@@ -15,7 +15,11 @@ from .tools import make_ints
from . import hub as h
_LOGGER = logging.getLogger(__name__).getChild('http')
ext = {f'ext{x}' for x in range(16)}
def is_ext(data: typing.Dict[str, typing.Any]):
for x in data:
if x.startswith('ext'):
return True
class MegaView(HomeAssistantView):
@@ -73,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)
@@ -83,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()
@@ -95,17 +103,37 @@ class MegaView(HomeAssistantView):
data['mega_id'] = hub.id
ret = 'd' if hub.force_d else ''
if port is not None:
if set(data).issubset(ext):
ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
for e in ext:
if e in data:
idx = e[-1]
pt = f'{port}e{idx}'
data['value'] = 'ON' if data[e] == '1' else 'OFF'
data['m'] = 1 if data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
hub.values[pt] = data
if is_ext(data):
# ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
if port in hub.extenders:
pt_orig = port
else:
pt_orig = hub.ext_in.get(port, hub.ext_in.get(str(port)))
if pt_orig is None:
hub.lg.warning(f'can not find extender for int port {port}, '
f'have ext_int: {hub.ext_in}, ext: {hub.extenders}')
return Response(status=200)
for e, v in data.items():
_data = data.copy()
if e.startswith('ext'):
idx = e[3:]
pt = f'{pt_orig}e{idx}'
_data['pt_orig'] = pt_orig
_data['value'] = 'ON' if v == '1' else 'OFF'
_data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
hub.values[pt] = _data
for cb in self.callbacks[hub.id][pt]:
cb(data)
cb(_data)
act = hub.ext_act.get(pt)
hub.lg.debug(f'act on port {pt}: {act}, all acts are: {hub.ext_act}')
template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
if template is not None:
template.hass = hass
ret = template.async_render(_data)
hub.lg.debug(f'response={ret}, template={template}')
if ret == 'd' and act:
await hub.request(cmd=act.replace(':3', f':{v}'))
ret = 'd' if hub.force_d else ''
else:
hub.values[port] = data
for cb in self.callbacks[hub.id][port]:
@@ -134,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,11 +20,13 @@ 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 .tools import make_ints
from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -65,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,
@@ -81,35 +82,47 @@ class MegaD:
protected=True,
restore_on_restart=False,
extenders=None,
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 DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
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 = {}
self._http_lck = asyncio.Lock()
self._http_lck = PriorityLock()
self._notif_lck = asyncio.Lock()
self.cnd = asyncio.Condition()
self.online = True
@@ -155,14 +168,7 @@ class MegaD:
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:
@@ -241,21 +247,23 @@ 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
ret = await self._update_i2c(x)
if isinstance(ret, dict):
self.values.update(ret)
for x in self.extenders:
ret = await self._update_extender(x)
if not isinstance(ret, dict):
self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue
self.values.update(ret)
if self.mqtt is None:
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
@@ -277,13 +285,13 @@ class MegaD:
async def send_command(self, port=None, cmd=None):
return await self.request(pt=port, cmd=cmd)
async def request(self, **kwargs):
async def request(self, priority=0, **kwargs):
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}"
if cmd:
url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url)
async with self._http_lck:
async with self._http_lck(priority):
async with aiohttp.request("get", url=url) as req:
if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
@@ -325,55 +333,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()
@@ -394,7 +380,7 @@ class MegaD:
if port == 'cmd':
return
try:
port = int(port)
port = int_ignore(port)
except:
self.lg.warning('can not process %s', msg)
return
@@ -423,14 +409,11 @@ class MegaD:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
def subscribe(self, port, callback):
port = int(port)
port = int_ignore(port)
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."""
@@ -451,45 +434,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):
@@ -510,15 +454,43 @@ class MegaD:
ret[f'{port}e{i}'] = x
return ret
async def _update_i2c(self, params):
"""
Обновление портов i2c
:param params: параметры url
:return:
"""
_params = tuple(params.items())
delay = None
if 'delay' in params:
delay = params.pop('delay')
ret = {
_params: await self.request(**params)
}
self.lg.debug('i2c response: %s', ret)
if delay:
self.lg.debug('delay %s', delay)
await asyncio.sleep(delay)
return ret
async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list))
ret['mqtt_id'] = await self.get_mqtt_id()
ret['extenders'] = extenders = []
ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = []
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)
@@ -533,21 +505,41 @@ class MegaD:
])
elif cfg == MCP230:
extenders.append(port)
if cfg.inta:
ext_int[int_ignore(cfg.inta)] = port
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page)
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
if ext_cfg.ety == '1':
ret['light'][f'{port}e{n}'].append({})
ret['light'][pt].append({})
elif ext_cfg.ety == '0':
ret['binary_sensor'][f'{port}e{n}'].append({})
if ext_cfg.eact:
ext_acts[pt] = ext_cfg.eact
ret['binary_sensor'][pt].append({})
elif cfg == PCA9685:
extenders.append(port)
values = await self.request(pt=port, cmd='get')
values = values.split(';')
for n in range(len(values)):
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
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)
if scan is not None:
page = await self.request(pt=port, cmd='scan')
req, parsed = parse_scan_page(page)
self.lg.debug(f'scan results: %s', (req, parsed))
ret['i2c'][port].extend(parsed)
i2c_sensors.extend(req)
elif cfg.pty == '4' and cfg.m == '2':
# scl исключаем из сканирования
continue
elif cfg.pty in ('3', '2', '4'):
http_cmd = 'get'
if cfg.d == '5' and cfg.pty == '3':
@@ -591,4 +583,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

@@ -0,0 +1,127 @@
from dataclasses import dataclass, field
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
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):
ret = []
req = []
page = BeautifulSoup(page, features="lxml")
for x in page.find_all('a'):
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
dev = params.get('i2c_dev')
if dev is None:
continue
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, Request):
if c.delay:
params = params.copy()
params['delay'] = c.delay
req.append(params)
continue
elif isinstance(c, DeviceType):
c, m, suffix = c
else:
continue
suffix = suffix or c
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,
'unit_of_measurement': m,
})
req.append(params)
return req, ret
class Skip:
pass
@dataclass
class Request:
delay: float = None
i2c_classes = {
'htu21d': [
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'sht31': [
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'max44009': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'bh1750': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'tsl2591': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
],
'bmp180': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'bmx280': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None)
],
'mlx90614': [
Skip,
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'),
],
'ptsensor': [
Skip,
Request(delay=1), # запрос на измерение
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'mcp9600': [
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
],
't67xx': [
DeviceType(None, CONCENTRATION_PARTS_PER_MILLION, None) # для co2 нет класса в HA
],
'tmp117': [
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,11 +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, map_reorder_rgb
lg = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
# Validation of the user's configuration
_EXTENDED = {
@@ -59,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(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
port = int_ignore(port)
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)
@@ -76,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.5.1b2"
"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,
@@ -14,14 +15,17 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS,
)
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
from .tools import int_ignore
lg = logging.getLogger(__name__)
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
@@ -80,21 +84,75 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid]
devices = []
for port, cfg in config_entry.data.get('sensor', {}).items():
port = int(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = Mega1WSensor(
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
devices.append(sensor)
for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port)
for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data)
sensor = _constructors[tp](
mega=hub,
port=port,
config_entry=config_entry,
**data,
)
if '<' in sensor.name:
continue
devices.append(sensor)
async_add_devices(devices)
class MegaI2C(MegaPushEntity):
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):
# 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):
return self._device_class
class Mega1WSensor(MegaPushEntity):
def __init__(
@@ -139,7 +197,15 @@ class Mega1WSensor(MegaPushEntity):
@property
def device_class(self):
return self._device_class
_u = self.customize.get(CONF_DEVICE_CLASS, None)
if _u is None:
return self._device_class
elif isinstance(_u, str):
return _u
elif isinstance(_u, dict) and self.key in _u:
return _u[self.key]
else:
return self._device_class
@property
def state(self):
@@ -163,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):
@@ -175,4 +250,10 @@ class Mega1WSensor(MegaPushEntity):
c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n
return c or n
_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

@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
from . import hub as h
from .entities import MegaOutPort
from .const import CONF_DIMMER, CONF_SWITCH, DOMAIN, CONF_CUSTOM, CONF_SKIP
from .tools import int_ignore
_LOGGER = lg = logging.getLogger(__name__)
@@ -50,13 +51,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
for port, cfg in config_entry.data.get('light', {}).items():
port = int(port)
port = int_ignore(port)
c = customize.get(mid, {}).get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'switch':
continue
for data in cfg:
hub.lg.debug(f'add switch on port %s with data %s', port, data)
light = MegaSwitch(mega=hub, port=port, config_entry=config_entry, **data)
if '<' in light.name:
continue
devices.append(light)
async_add_devices(devices)

View File

@@ -1,3 +1,9 @@
import asyncio
import itertools
from heapq import heappush
from contextlib import asynccontextmanager
_params = ['m', 'click', 'cnt', 'pt']
@@ -10,4 +16,109 @@ def make_ints(d: dict):
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0
d['click'] = 0
def int_ignore(x):
try:
return int(x)
except (TypeError, ValueError):
return x
class PriorityLock(asyncio.Lock):
"""
You can acquire lock with some kind of priority in mind, so that locks with higher priority will be released first.
priority can be set with lck.acquire(1)
or by using context manager:
>>> lck = PriorityLock()
... async with lck(1):
... # do something
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cnt = itertools.count()
def __call__(self, priority=0):
return self._with_priority(priority)
@asynccontextmanager
async def _with_priority(self, p):
await self.acquire(p)
try:
yield
finally:
self.release()
async def acquire(self, priority=0) -> bool:
"""Acquire a lock.
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for _, _, w in self._waiters))):
self._locked = True
return True
if self._waiters is None:
self._waiters = []
fut = self._loop.create_future()
cnt = next(self._cnt)
heappush(self._waiters, (priority, cnt, fut))
# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
try:
await fut
finally:
self._waiters.remove((priority, cnt, fut))
except asyncio.exceptions.CancelledError:
if not self._locked:
self._wake_up_first()
raise
self._locked = True
return True
def release(self):
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
if self._locked:
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
def _wake_up_first(self):
"""Wake up the first waiter if it isn't done."""
if not self._waiters:
return
try:
_, _, fut = self._waiters[0]
except IndexError:
return
# .done() necessarily means that a waiter will wake up later on and
# either take the lock, or, if it was cancelled and lock wasn't
# taken already, will hit this again and wake up a new waiter.
if not fut.done():
fut.set_result(True)
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,22 @@
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`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)
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):