- add rgbw support

- add ws28 support
- add transitions to pwm
- add units of measurement for all i2c sensors
- remove mqtt support
This commit is contained in:
Andrey Viktorov
2021-03-23 15:10:50 +03:00
parent 36433a7fdd
commit 1270ea2ee2
15 changed files with 687 additions and 213 deletions

View File

@@ -1,3 +1,5 @@
import struct
from itertools import permutations
print(struct.unpack('!f', bytes.fromhex('435c028f'))[0])
RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')]
print(RGB_COMBINATIONS)

View File

@@ -6,23 +6,49 @@ from functools import partial
import voluptuous as vol
from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
CONF_NAME, CONF_DOMAIN,
CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.service import bind_hass
from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_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

View File

@@ -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')
)

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
"""Constants for the mega integration."""
import re
from itertools import permutations
DOMAIN = "mega"
CONF_MEGA_ID = "mega_id"
@@ -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',
]
'smooth',
]
RGB_COMBINATIONS = [''.join(x) for x in permutations('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,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':

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

@@ -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

View File

@@ -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),
]
}

View File

@@ -1,11 +1,20 @@
"""Platform for light integration."""
import asyncio
import logging
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol
import colorsys
import time
from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_SCHEMA,
SUPPORT_BRIGHTNESS,
LightEntity,
SUPPORT_TRANSITION,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -16,7 +25,7 @@ from homeassistant.const import (
CONF_DOMAIN,
)
from homeassistant.core import HomeAssistant
from .entities import MegaOutPort
from .entities import MegaOutPort, BaseMegaEntity, safe_int
from .hub import MegaD
from .const import (
@@ -24,12 +33,12 @@ from .const import (
CONF_SWITCH,
DOMAIN,
CONF_CUSTOM,
CONF_SKIP,
CONF_SKIP, CONF_LED, CONF_WS28XX, CONF_PORTS, CONF_WHITE_SEP, CONF_SMOOTH, CONF_ORDER, CONF_CHIP,
)
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

View File

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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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, тк в нем нет необходимости)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Автоматическое добавление/изменение объектов после перезагрузки контроллера