diff --git a/.experiment.py b/.experiment.py index d3b0496..0ead2f2 100644 --- a/.experiment.py +++ b/.experiment.py @@ -1,3 +1,5 @@ -import struct +from itertools import permutations -print(struct.unpack('!f', bytes.fromhex('435c028f'))[0]) \ No newline at end of file +RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')] + +print(RGB_COMBINATIONS) \ No newline at end of file diff --git a/custom_components/mega/__init__.py b/custom_components/mega/__init__.py index 6d79d2b..9ecb4f1 100644 --- a/custom_components/mega/__init__.py +++ b/custom_components/mega/__init__.py @@ -6,23 +6,49 @@ from functools import partial import voluptuous as vol from homeassistant.const import ( - CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN, - CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS + CONF_NAME, CONF_DOMAIN, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.service import bind_hass from homeassistant.helpers import config_validation as cv -from homeassistant.components import mqtt from homeassistant.config_entries import ConfigEntry from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \ CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ - CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT + 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, @@ -48,6 +74,14 @@ CUSTOMIZE_PORT = { 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 @@ -72,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, - ) + ), } } }, @@ -123,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, config=entry, **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 diff --git a/custom_components/mega/binary_sensor.py b/custom_components/mega/binary_sensor.py index 668e69c..b314b44 100644 --- a/custom_components/mega/binary_sensor.py +++ b/custom_components/mega/binary_sensor.py @@ -97,20 +97,4 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity): def _update(self, payload: dict): self.mega.values[self.port] = payload - if not self.mega.mqtt_inputs: - return - - template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None) - if template is not None: - template.hass = self.hass - ret = template.async_render(payload) - self.mega.lg.debug(f'response: %s', ret) - self.hass.async_create_task( - self.mega.request(pt=self.port, cmd=ret) - ) - elif self.mega.force_d: - self.mega.lg.debug(f'response d') - self.hass.async_create_task( - self.mega.request(pt=self.port, cmd='d') - ) diff --git a/custom_components/mega/config_flow.py b/custom_components/mega/config_flow.py index 64812da..c421f3b 100644 --- a/custom_components/mega/config_flow.py +++ b/custom_components/mega/config_flow.py @@ -5,14 +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, CONF_UPDATE_TIME, \ - REMOVE_CONFIG # pylint:disable=unused-import + CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME from .hub import MegaD from . import exceptions @@ -23,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_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, @@ -40,10 +38,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema( 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 @@ -65,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 = 23 + VERSION = 24 CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED async def async_step_user(self, user_input=None): @@ -80,6 +78,7 @@ 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) @@ -128,7 +127,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if reload: id = self.config_entry.data.get('id', self.config_entry.entry_id) hub: MegaD = self.hass.data[DOMAIN].get(id) - await hub.reload(reload_entry=False) + cfg = await hub.reload(reload_entry=False) return self.async_create_entry( title='', @@ -141,7 +140,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): 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_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, diff --git a/custom_components/mega/config_parser.py b/custom_components/mega/config_parser.py index 49860c2..f8f92a0 100644 --- a/custom_components/mega/config_parser.py +++ b/custom_components/mega/config_parser.py @@ -44,6 +44,9 @@ def parse_config(page: str): v = page.find('input', attrs={'name': x}) if v: ret[x] = v['value'] + smooth = page.find('input', attrs={'name': 'misc'}) + if smooth is None or smooth.get('checked') is None: + ret['misc'] = None return Config(**ret, src=page) diff --git a/custom_components/mega/const.py b/custom_components/mega/const.py index d564266..78cb64e 100644 --- a/custom_components/mega/const.py +++ b/custom_components/mega/const.py @@ -1,5 +1,6 @@ """Constants for the mega integration.""" import re +from itertools import permutations DOMAIN = "mega" CONF_MEGA_ID = "mega_id" @@ -37,6 +38,13 @@ 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", @@ -67,4 +75,6 @@ REMOVE_CONFIG = [ 'light', 'i2c', 'sensor', -] \ No newline at end of file + 'smooth', +] +RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')] \ No newline at end of file diff --git a/custom_components/mega/entities.py b/custom_components/mega/entities.py index 3708bf1..07e5e56 100644 --- a/custom_components/mega/entities.py +++ b/custom_components/mega/entities.py @@ -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,17 +64,57 @@ 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 "") - _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 + 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: @@ -78,6 +124,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): @property def customize(self): + if self._customize is not None: + return self._customize if self.hass is None: return {} if self._customize is None: @@ -93,16 +141,23 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): @property def device_info(self): - _pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port + 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 {_pt}', + "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, @@ -123,8 +178,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity): def name(self): c = self.customize.get(CONF_NAME) if not isinstance(c, str): - _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}" + 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 @@ -135,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): @@ -161,9 +216,6 @@ class MegaPushEntity(BaseMegaEntity): 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 @@ -215,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): @@ -236,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) @@ -318,19 +378,74 @@ class MegaOutPort(MegaPushEntity): else: return self.port - async def async_turn_on(self, brightness=None, **kwargs) -> None: + @property + def smooth(self) -> timedelta: + ret = self.customize.get(CONF_SMOOTH) + if ret is None and self._smooth: + ret = timedelta(seconds=self._smooth) + return ret + + @property + def smooth_dim(self): + if not self.dimmer: + return False + return self.smooth or self.can_smooth_hardware + + def update_from_smooth(self, value, update_state=False): + if isinstance(self.port, str): + self.mega.values[self.port] = value[0] + else: + self.mega.values[self.port] = { + 'value': value[0] + } + if update_state: + self.async_write_ha_state() + + def _set_dim_brightness(self, from_, to_, transition): + pct = abs(to_ - from_) / (255 if self.dimmer_scale == 1 else 4095) + update_state = transition is not None and transition > 3 + tm = (self.smooth.total_seconds() * pct) if transition is None else transition + if self.task is not None: + self.task.cancel() + self.task = asyncio.create_task(self.mega.smooth_dim( + (self.cmd_port, from_, to_), + time=tm, + can_smooth_hardware=self.can_smooth_hardware, + max_values=[255 if self.dimmer_scale == 1 else 4095], + updater=partial(self.update_from_smooth, update_state=update_state), + )) + + async def async_turn_on(self, brightness=None, transition=None, **kwargs): + if (time.time() - self._last_called) < 0.1: + return + self._last_called = time.time() + if not self.dimmer: + transition = None + if not self.is_on: + brightness = self._restore_brightness brightness = brightness or self.brightness or 255 + _prev = safe_int(self.brightness) or 0 self._brightness = brightness if self.dimmer and brightness == 0: - cmd = 255 * self.dimmer_scale + cmd = self.max_dim elif self.dimmer: - cmd = brightness * self.dimmer_scale + cmd = min((brightness * self.dimmer_scale, self.max_dim)) + if self.smooth_dim or transition: + self._set_dim_brightness(from_=_prev, to_=cmd, transition=transition) else: cmd = 1 if not self.invert else 0 - _cmd = {"cmd": f"{self.cmd_port}:{cmd}"} + if transition is None: + _cmd = {"cmd": f"{self.cmd_port}:{cmd}"} + else: + _cmd = { + "pt": f"{self.cmd_port}", + "pwm": cmd, + "cnt": round(transition / (abs(_prev - brightness) / 255)), + } if self.addr: _cmd['addr'] = self.addr - await self.mega.request(**_cmd, priority=-1) + if not (self.smooth_dim or transition): + await self.mega.request(**_cmd, priority=-1) if self.index is not None: # обновление текущего стейта для ds2413 await self.mega.get_port( @@ -348,13 +463,26 @@ class MegaOutPort(MegaPushEntity): self.mega.values[self.port] = {'value': cmd} await self.get_state() - async def async_turn_off(self, **kwargs) -> None: - + async def async_turn_off(self, transition=None, **kwargs) -> None: + if (time.time() - self._last_called) < 0.1: + return + self._last_called = time.time() + self._restore_brightness = safe_int(self._brightness) + if not self.dimmer: + transition = None cmd = "0" if not self.invert else "1" _cmd = {"cmd": f"{self.cmd_port}:{cmd}"} + _prev = safe_int(self.brightness) or 0 if self.addr: _cmd['addr'] = self.addr - await self.mega.request(**_cmd, priority=-1) + if not (self.smooth_dim or transition): + await self.mega.request(**_cmd, priority=-1) + else: + self._set_dim_brightness( + from_=_prev, + to_=0, + transition=transition, + ) if self.index is not None: # обновление текущего стейта для ds2413 await self.mega.get_port( @@ -369,6 +497,11 @@ class MegaOutPort(MegaPushEntity): self.mega.values[self.port] = {'value': cmd} await self.get_state() + async def async_will_remove_from_hass(self) -> None: + if self.task is not None: + self.task.cancel() + + def safe_int(v): if v == 'ON': diff --git a/custom_components/mega/exceptions.py b/custom_components/mega/exceptions.py index ec65f3f..773cff9 100644 --- a/custom_components/mega/exceptions.py +++ b/custom_components/mega/exceptions.py @@ -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""" diff --git a/custom_components/mega/hub.py b/custom_components/mega/hub.py index a2a2930..fac6796 100644 --- a/custom_components/mega/hub.py +++ b/custom_components/mega/hub.py @@ -9,7 +9,6 @@ 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, @@ -24,7 +23,7 @@ from .const import ( CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN, REMOVE_CONFIG ) -from .entities import set_events_off, BaseMegaEntity, MegaOutPort +from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int from .exceptions import CannotConnect, NoPort from .i2c import parse_scan_page from .tools import make_ints, int_ignore, PriorityLock @@ -68,11 +67,9 @@ class MegaD: loop: asyncio.AbstractEventLoop, host: str, password: str, - mqtt: mqtt.MQTT, lg: logging.Logger, id: str, config: ConfigEntry = None, - mqtt_inputs: bool = True, mqtt_id: str = None, scan_interval=60, port_to_scan=0, @@ -90,21 +87,22 @@ class MegaD: i2c_sensors=None, new_naming=False, update_time=False, + smooth: list=None, **kwargs, ): """Initialize.""" + if config is not None: + lg.debug(f'load config: %s', config.data) self.config = config - 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 + 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 {} @@ -115,12 +113,10 @@ class MegaD: 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 = {} @@ -170,14 +166,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: @@ -270,15 +259,9 @@ class MegaD: self.lg.warning(f'wrong updater result: {ret} from extender {x}') continue self.values.update(ret) - if self.mqtt is None: - await self.get_all_ports() - await self.get_sensors(only_list=True) - elif self.poll_outs: - await self.get_all_ports(check_skip=True) - elif len(self.sensors) > 0: - await self.get_sensors() - # else: - # await self.get_port(self.port_to_scan) + + await self.get_all_ports() + await self.get_sensors(only_list=True) await self._get_ds2413() return self.values @@ -348,55 +331,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() @@ -450,10 +411,7 @@ class MegaD: self.lg.debug( f'subscribe %s %s', port, callback ) - if self.mqtt_inputs: - self._callbacks[port].append(callback) - else: - self.http.callbacks[self.id][port].append(callback) + self.http.callbacks[self.id][port].append(callback) async def authenticate(self) -> bool: """Test if we can authenticate with the host.""" @@ -520,6 +478,7 @@ class MegaD: ret['ext_in'] = ext_int = {} ret['ext_acts'] = ext_acts = {} ret['i2c_sensors'] = i2c_sensors = [] + ret['smooth'] = smooth = [] async for port, cfg in self.scan_ports(nports): _cust = self.customize.get(port) if not isinstance(_cust, dict): @@ -527,7 +486,9 @@ class MegaD: 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) @@ -561,9 +522,10 @@ class MegaD: values = await self.request(pt=port, cmd='get') values = values.split(';') for n in range(len(values)): - pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}' - ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16}) - elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)): + 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) @@ -627,11 +589,94 @@ class MegaD: async def reload(self, reload_entry=True): new = await self.get_config(nports=self.nports) - self.lg.debug(f'new config: %s', new) cfg = dict(self.config.data) for x in REMOVE_CONFIG: cfg.pop(x, None) cfg.update(new) + self.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].ljust(2, '0').upper() for x in _next_val]) + ) + await self.request(**cmd) + + if _next_val == last_step: + return + c = _next_val diff --git a/custom_components/mega/i2c.py b/custom_components/mega/i2c.py index 59193b4..a8ee4c8 100644 --- a/custom_components/mega/i2c.py +++ b/custom_components/mega/i2c.py @@ -6,7 +6,15 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, + PERCENTAGE, + LIGHT_LUX, + TEMP_CELSIUS, + CONCENTRATION_PARTS_PER_MILLION, + PRESSURE_BAR, ) +from collections import namedtuple + +DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix') def parse_scan_page(page: str): @@ -34,12 +42,11 @@ def parse_scan_page(page: str): params['delay'] = c.delay req.append(params) continue - elif isinstance(c, tuple): - suffix, c = c - elif isinstance(c, str): - suffix = c + elif isinstance(c, DeviceType): + c, m, suffix = c else: - suffix = '' + continue + suffix = suffix or c if 'addr' in params: suffix += f"_{params['addr']}" if suffix else str(params['addr']) if suffix: @@ -49,10 +56,12 @@ def parse_scan_page(page: str): params = params.copy() if i > 0: params['i2c_par'] = i + ret.append({ 'id_suffix': _dev, 'device_class': c, 'params': params, + 'unit_of_measurement': m, }) req.append(params) return req, ret @@ -69,50 +78,50 @@ class Request: i2c_classes = { 'htu21d': [ - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, + DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), ], 'sht31': [ - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, + DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), ], 'max44009': [ - DEVICE_CLASS_ILLUMINANCE + DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) ], 'bh1750': [ - DEVICE_CLASS_ILLUMINANCE + DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) ], 'tsl2591': [ - DEVICE_CLASS_ILLUMINANCE + DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) ], 'bmp180': [ - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, + DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), ], 'bmx280': [ - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_HUMIDITY + DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), + DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None) ], 'mlx90614': [ Skip, - ('temp', DEVICE_CLASS_TEMPERATURE), - ('object', DEVICE_CLASS_TEMPERATURE), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'), ], 'ptsensor': [ Skip, Request(delay=1), # запрос на измерение - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, + DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), ], 'mcp9600': [ - DEVICE_CLASS_TEMPERATURE, # термопара - DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему ], 't67xx': [ - None # для co2 нет класса в HA + DeviceType(None, CONCENTRATION_PARTS_PER_MILLION, None) # для co2 нет класса в HA ], 'tmp117': [ - DEVICE_CLASS_TEMPERATURE, + DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), ] } diff --git a/custom_components/mega/light.py b/custom_components/mega/light.py index 4ceca20..d2d7e06 100644 --- a/custom_components/mega/light.py +++ b/custom_components/mega/light.py @@ -1,11 +1,20 @@ """Platform for light integration.""" +import asyncio import logging +from datetime import timedelta, datetime +from functools import partial + import voluptuous as vol +import colorsys +import time from homeassistant.components.light import ( PLATFORM_SCHEMA as LIGHT_SCHEMA, SUPPORT_BRIGHTNESS, LightEntity, + SUPPORT_TRANSITION, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -16,7 +25,7 @@ from homeassistant.const import ( CONF_DOMAIN, ) from homeassistant.core import HomeAssistant -from .entities import MegaOutPort +from .entities import MegaOutPort, BaseMegaEntity, safe_int from .hub import MegaD from .const import ( @@ -24,12 +33,12 @@ from .const import ( CONF_SWITCH, DOMAIN, CONF_CUSTOM, - CONF_SKIP, + CONF_SKIP, CONF_LED, CONF_WS28XX, CONF_PORTS, CONF_WHITE_SEP, CONF_SMOOTH, CONF_ORDER, CONF_CHIP, ) from .tools import int_ignore lg = logging.getLogger(__name__) - +SCAN_INTERVAL = timedelta(seconds=5) # Validation of the user's configuration _EXTENDED = { @@ -60,11 +69,24 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn mid = config_entry.data[CONF_ID] hub: MegaD = hass.data['mega'][mid] devices = [] - customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) + customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {}) + skip = [] + if CONF_LED in customize: + for entity_id, conf in customize[CONF_LED].items(): + ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)] + skip.extend(ports) + devices.append(MegaRGBW( + mega=hub, + port=ports, + name=entity_id, + customize=conf, + id_suffix=entity_id, + config_entry=config_entry + )) for port, cfg in config_entry.data.get('light', {}).items(): port = int_ignore(port) - c = customize.get(mid, {}).get(port, {}) - if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light': + c = customize.get(port, {}) + if c.get(CONF_SKIP, False) or port in skip or c.get(CONF_DOMAIN, 'light') != 'light': continue for data in cfg: hub.lg.debug(f'add light on port %s with data %s', port, data) @@ -72,6 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn if '<' in light.name: continue devices.append(light) + async_add_devices(devices) @@ -79,5 +102,221 @@ 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_map = self.customize.get(CONF_ORDER, 'rgb') + self._last_called: float = 0 + if self._color_map == 'rgb': + self._color_map = None + else: + self._color_map = { + x: i for i, x in enumerate(self._color_map) + } + 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 and self._color_map is not None: + # восстанавливаем мэпинг + rgb = [ + rgb[self._color_map[x]] for x in 'rgb' + ] + 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 + + 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 \ No newline at end of file diff --git a/custom_components/mega/manifest.json b/custom_components/mega/manifest.json index 2dd5efe..fcc9a88 100644 --- a/custom_components/mega/manifest.json +++ b/custom_components/mega/manifest.json @@ -15,5 +15,5 @@ "@andvikt" ], "issue_tracker": "https://github.com/andvikt/mega_hacs/issues", - "version": "v0.6.3b1" + "version": "v1.0.0b1" } \ No newline at end of file diff --git a/custom_components/mega/sensor.py b/custom_components/mega/sensor.py index d88bffa..2581902 100644 --- a/custom_components/mega/sensor.py +++ b/custom_components/mega/sensor.py @@ -104,14 +104,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn class MegaI2C(MegaPushEntity): - def __init__(self, *args, device_class: str, params: dict, **kwargs): + def __init__( + self, + *args, + device_class: str, + params: dict, + unit_of_measurement: str = None, + **kwargs + ): self._device_class = device_class self._params = tuple(params.items()) + self._unit_of_measurement = unit_of_measurement super().__init__(*args, **kwargs) + @property + def customize(self): + return super().customize.get(self.id_suffix, {}) or {} + + @property def device_class(self): return self._device_class + @property + def unit_of_measurement(self): + return self._unit_of_measurement + @property def state(self): # self.lg.debug(f'get % all states: %', self._params, self.mega.values) @@ -122,9 +139,13 @@ class MegaI2C(MegaPushEntity): 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}) + 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 ret @property @@ -214,10 +235,13 @@ class Mega1WSensor(MegaPushEntity): 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: + return ret @property def name(self): diff --git a/custom_components/mega/tools.py b/custom_components/mega/tools.py index cca2c2f..356736b 100644 --- a/custom_components/mega/tools.py +++ b/custom_components/mega/tools.py @@ -115,3 +115,5 @@ class PriorityLock(asyncio.Lock): # taken already, will hit this again and wake up a new waiter. if not fut.done(): fut.set_result(True) + + diff --git a/readme.md b/readme.md index ba2d4f4..2c4eaed 100644 --- a/readme.md +++ b/readme.md @@ -20,8 +20,12 @@ - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков `sensor`) +- Поддержка rgb+w лент как с использованием диммеров, так и адресных лент на чипах ws28xx и подобных, + [подробнее про rgbw](https://github.com/andvikt/mega_hacs/wiki/rgbw) +- Плавное диммирование с поддержкой [transition](https://www.home-assistant.io/integrations/scene#using-scene-transitions) + для любых диммируемых объектов (в том числе с аппаратной поддержкой и без) - Возможность работы с несколькими megad -- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt +- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) будет выключена в версиях >= 1.0.0, тк в нем нет необходимости) - Автоматическое восстановление состояний выходов после перезагрузки контроллера - Автоматическое добавление/изменение объектов после перезагрузки контроллера