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

View File

@@ -97,20 +97,4 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def _update(self, payload: dict): def _update(self, payload: dict):
self.mega.values[self.port] = payload self.mega.values[self.port] = payload
if not self.mega.mqtt_inputs:
return
template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None)
if template is not None:
template.hass = self.hass
ret = template.async_render(payload)
self.mega.lg.debug(f'response: %s', ret)
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd=ret)
)
elif self.mega.force_d:
self.mega.lg.debug(f'response d')
self.hass.async_create_task(
self.mega.request(pt=self.port, cmd='d')
)

View File

@@ -5,14 +5,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core from homeassistant import config_entries, core
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \ from .const import DOMAIN, CONF_RELOAD, \
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \ CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME, \ CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME
REMOVE_CONFIG # pylint:disable=unused-import
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -23,10 +21,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_ID, default='mega'): str, vol.Required(CONF_ID, default='mega'): str,
vol.Required(CONF_HOST, default="192.168.0.14"): str, vol.Required(CONF_HOST, default="192.168.0.14"): str,
vol.Required(CONF_PASSWORD, default="sec"): str, vol.Required(CONF_PASSWORD, default="sec"): str,
vol.Optional(CONF_SCAN_INTERVAL, default=0): int, vol.Optional(CONF_SCAN_INTERVAL, default=30): int,
vol.Optional(CONF_POLL_OUTS, default=False): bool, vol.Optional(CONF_POLL_OUTS, default=False): bool,
# vol.Optional(CONF_PORT_TO_SCAN, default=0): int, # vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=False): bool, # vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
vol.Optional(CONF_NPORTS, default=37): int, vol.Optional(CONF_NPORTS, default=37): int,
vol.Optional(CONF_UPDATE_ALL, default=True): bool, vol.Optional(CONF_UPDATE_ALL, default=True): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool, vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
@@ -40,10 +38,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
async def get_hub(hass: HomeAssistant, data): async def get_hub(hass: HomeAssistant, data):
_mqtt = hass.data.get(mqtt.DOMAIN) # _mqtt = hass.data.get(mqtt.DOMAIN)
# if not isinstance(_mqtt, mqtt.MQTT): # if not isinstance(_mqtt, mqtt.MQTT):
# raise exceptions.MqttNotConfigured("mqtt must be configured first") # raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt, loop=asyncio.get_event_loop()) hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) #mqtt=_mqtt,
hub.mqtt_id = await hub.get_mqtt_id() hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate(): if not await hub.authenticate():
raise exceptions.InvalidAuth raise exceptions.InvalidAuth
@@ -65,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for mega.""" """Handle a config flow for mega."""
VERSION = 23 VERSION = 24
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
@@ -80,6 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
hub = await validate_input(self.hass, user_input) hub = await validate_input(self.hass, user_input)
await hub.start() await hub.start()
hub.new_naming=True
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37)) config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop() await hub.stop()
hub.lg.debug(f'config loaded: %s', config) hub.lg.debug(f'config loaded: %s', config)
@@ -128,7 +127,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if reload: if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id) id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(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( return self.async_create_entry(
title='', 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_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool, vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, # vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool, # vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int, vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_RELOAD, default=False): bool, vol.Optional(CONF_RELOAD, default=False): bool,
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool, vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,

View File

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

View File

@@ -1,5 +1,6 @@
"""Constants for the mega integration.""" """Constants for the mega integration."""
import re import re
from itertools import permutations
DOMAIN = "mega" DOMAIN = "mega"
CONF_MEGA_ID = "mega_id" CONF_MEGA_ID = "mega_id"
@@ -37,6 +38,13 @@ CONF_LONG_TIME = 'long_time'
CONF_FORCE_I2C_SCAN = 'force_i2c_scan' CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
CONF_UPDATE_TIME = 'update_time' CONF_UPDATE_TIME = 'update_time'
CONF_HEX_TO_FLOAT = 'hex_to_float' CONF_HEX_TO_FLOAT = 'hex_to_float'
CONF_LED = 'led'
CONF_WS28XX = 'ws28xx'
CONF_ORDER = 'order'
CONF_SMOOTH = 'smooth'
CONF_WHITE_SEP = 'white_sep'
CONF_CHIP = 'chip'
CONF_RANGE = 'range'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",
@@ -67,4 +75,6 @@ REMOVE_CONFIG = [
'light', 'light',
'i2c', 'i2c',
'sensor', 'sensor',
] 'smooth',
]
RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')]

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import re
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
@@ -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, CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN,
REMOVE_CONFIG REMOVE_CONFIG
) )
from .entities import set_events_off, BaseMegaEntity, MegaOutPort from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int
from .exceptions import CannotConnect, NoPort from .exceptions import CannotConnect, NoPort
from .i2c import parse_scan_page from .i2c import parse_scan_page
from .tools import make_ints, int_ignore, PriorityLock from .tools import make_ints, int_ignore, PriorityLock
@@ -68,11 +67,9 @@ class MegaD:
loop: asyncio.AbstractEventLoop, loop: asyncio.AbstractEventLoop,
host: str, host: str,
password: str, password: str,
mqtt: mqtt.MQTT,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
config: ConfigEntry = None, config: ConfigEntry = None,
mqtt_inputs: bool = True,
mqtt_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
@@ -90,21 +87,22 @@ class MegaD:
i2c_sensors=None, i2c_sensors=None,
new_naming=False, new_naming=False,
update_time=False, update_time=False,
smooth: list=None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
if config is not None:
lg.debug(f'load config: %s', config.data)
self.config = config 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)
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP) if not self.http is None:
if not self.http is None: self.http.allowed_hosts |= {host}
self.http.allowed_hosts |= {host} self.http.hubs[host] = self
self.http.hubs[host] = self if len(self.http.hubs) == 1:
if len(self.http.hubs) == 1: self.http.hubs['__def'] = self
self.http.hubs['__def'] = self if mqtt_id:
if mqtt_id: self.http.hubs[mqtt_id] = self
self.http.hubs[mqtt_id] = self self.smooth = smooth or []
else:
self.http = None
self.new_naming = new_naming self.new_naming = new_naming
self.extenders = extenders or [] self.extenders = extenders or []
self.ext_in = ext_in or {} self.ext_in = ext_in or {}
@@ -115,12 +113,10 @@ class MegaD:
self.update_all = update_all if update_all is not None else True self.update_all = update_all if update_all is not None else True
self.nports = nports self.nports = nports
self.fake_response = fake_response self.fake_response = fake_response
self.mqtt_inputs = mqtt_inputs
self.loop: asyncio.AbstractEventLoop = None self.loop: asyncio.AbstractEventLoop = None
self.hass = hass self.hass = hass
self.host = host self.host = host
self.sec = password self.sec = password
self.mqtt = mqtt
self.id = id self.id = id
self.lck = asyncio.Lock() self.lck = asyncio.Lock()
self.last_long = {} self.last_long = {}
@@ -170,14 +166,7 @@ class MegaD:
self.lg.exception('while setting allowed hosts') self.lg.exception('while setting allowed hosts')
async def start(self): async def start(self):
self.loop = asyncio.get_event_loop() pass
if self.mqtt is not None:
set_events_off()
self.subs = await self.mqtt.async_subscribe(
topic=f"{self.mqtt_id}/+",
msg_callback=self._process_msg,
qos=0,
)
async def stop(self): async def stop(self):
if self.subs is not None: if self.subs is not None:
@@ -270,15 +259,9 @@ class MegaD:
self.lg.warning(f'wrong updater result: {ret} from extender {x}') self.lg.warning(f'wrong updater result: {ret} from extender {x}')
continue continue
self.values.update(ret) self.values.update(ret)
if self.mqtt is None:
await self.get_all_ports() await self.get_all_ports()
await self.get_sensors(only_list=True) await self.get_sensors(only_list=True)
elif self.poll_outs:
await self.get_all_ports(check_skip=True)
elif len(self.sensors) > 0:
await self.get_sensors()
# else:
# await self.get_port(self.port_to_scan)
await self._get_ds2413() await self._get_ds2413()
return self.values return self.values
@@ -348,55 +331,33 @@ class MegaD:
хранилище values хранилище values
""" """
self.lg.debug(f'get port %s', port) self.lg.debug(f'get port %s', port)
if self.mqtt is None or force_http: if http_cmd == 'list' and conv:
if http_cmd == 'list' and conv: await self.request(pt=port, cmd='conv')
await self.request(pt=port, cmd='conv') await asyncio.sleep(1)
await asyncio.sleep(1) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd) ntry = 0
ntry = 0 while http_cmd == 'list' and ret is None and ntry < 3:
while http_cmd == 'list' and ret is None and ntry < 3: await asyncio.sleep(1)
await asyncio.sleep(1) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) ntry += 1
ntry += 1 self.lg.debug('parsed: %s', ret)
self.lg.debug('parsed: %s', ret) self.values[port] = ret
self.values[port] = ret return ret
return ret
async with self._notif_lck:
async with self.notifiers[port]:
cnd = self.notifiers[port]
await self.mqtt.async_publish(
topic=f'{self.mqtt_id}/cmd',
payload=f'get:{port}',
qos=2,
retain=False,
)
try:
await asyncio.wait_for(cnd.wait(), timeout=10)
return self.values.get(port)
except asyncio.TimeoutError:
self.lg.error(f'timeout when getting port {port}')
@property @property
def ports(self): def ports(self):
return {e.port for e in self.entities} return {e.port for e in self.entities}
async def get_all_ports(self, only_out=False, check_skip=False): async def get_all_ports(self, only_out=False, check_skip=False):
if not self.mqtt_inputs: ret = await self.request(cmd='all')
ret = await self.request(cmd='all') for port, x in enumerate(ret.split(';')):
for port, x in enumerate(ret.split(';')): if port in self.ds2413_ports:
if port in self.ds2413_ports: continue
continue if check_skip and not port in self.ports:
if check_skip and not port in self.ports: continue
continue ret = self.parse_response(x)
ret = self.parse_response(x) self.values[port] = ret
self.values[port] = ret
elif not check_skip:
for x in range(self.nports + 1):
await self.get_port(x)
else:
for x in self.ports:
await self.get_port(x)
async def reboot(self, save=True): async def reboot(self, save=True):
await self.save() await self.save()
@@ -450,10 +411,7 @@ class MegaD:
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, callback f'subscribe %s %s', port, callback
) )
if self.mqtt_inputs: self.http.callbacks[self.id][port].append(callback)
self._callbacks[port].append(callback)
else:
self.http.callbacks[self.id][port].append(callback)
async def authenticate(self) -> bool: async def authenticate(self) -> bool:
"""Test if we can authenticate with the host.""" """Test if we can authenticate with the host."""
@@ -520,6 +478,7 @@ class MegaD:
ret['ext_in'] = ext_int = {} ret['ext_in'] = ext_int = {}
ret['ext_acts'] = ext_acts = {} ret['ext_acts'] = ext_acts = {}
ret['i2c_sensors'] = i2c_sensors = [] ret['i2c_sensors'] = i2c_sensors = []
ret['smooth'] = smooth = []
async for port, cfg in self.scan_ports(nports): async for port, cfg in self.scan_ports(nports):
_cust = self.customize.get(port) _cust = self.customize.get(port)
if not isinstance(_cust, dict): if not isinstance(_cust, dict):
@@ -527,7 +486,9 @@ class MegaD:
if cfg.pty == "0": if cfg.pty == "0":
ret['binary_sensor'][port].append({}) ret['binary_sensor'][port].append({})
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None): elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
ret['light'][port].append({'dimmer': cfg.m == '1'}) if cfg.misc is not None:
smooth.append(port)
ret['light'][port].append({'dimmer': cfg.m == '1', 'smooth': safe_int(cfg.misc)})
elif cfg == DS2413: elif cfg == DS2413:
# ds2413 # ds2413
_data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False) _data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
@@ -561,9 +522,10 @@ class MegaD:
values = await self.request(pt=port, cmd='get') values = await self.request(pt=port, cmd='get')
values = values.split(';') values = values.split(';')
for n in range(len(values)): for n in range(len(values)):
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}' pt = f'{port}e{n}'
ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16}) name = pt if not self.new_naming else f'{port:02}e{n:02}'
elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)): ret['light'][pt].append({'dimmer': True, 'dimmer_scale': 16, 'name': f'{self.id}_{name}'})
if cfg.pty == '4': #and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN))
# i2c в режиме ANY # i2c в режиме ANY
scan = cfg.src.find('a', text='I2C Scan') scan = cfg.src.find('a', text='I2C Scan')
self.lg.debug(f'find scan link: %s', scan) self.lg.debug(f'find scan link: %s', scan)
@@ -627,11 +589,94 @@ class MegaD:
async def reload(self, reload_entry=True): async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports) new = await self.get_config(nports=self.nports)
self.lg.debug(f'new config: %s', new)
cfg = dict(self.config.data) cfg = dict(self.config.data)
for x in REMOVE_CONFIG: for x in REMOVE_CONFIG:
cfg.pop(x, None) cfg.pop(x, None)
cfg.update(new) cfg.update(new)
self.lg.debug(f'new config: %s', cfg)
self.config.data = cfg self.config.data = cfg
if reload_entry: if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id) 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_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE, 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): def parse_scan_page(page: str):
@@ -34,12 +42,11 @@ def parse_scan_page(page: str):
params['delay'] = c.delay params['delay'] = c.delay
req.append(params) req.append(params)
continue continue
elif isinstance(c, tuple): elif isinstance(c, DeviceType):
suffix, c = c c, m, suffix = c
elif isinstance(c, str):
suffix = c
else: else:
suffix = '' continue
suffix = suffix or c
if 'addr' in params: if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr']) suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix: if suffix:
@@ -49,10 +56,12 @@ def parse_scan_page(page: str):
params = params.copy() params = params.copy()
if i > 0: if i > 0:
params['i2c_par'] = i params['i2c_par'] = i
ret.append({ ret.append({
'id_suffix': _dev, 'id_suffix': _dev,
'device_class': c, 'device_class': c,
'params': params, 'params': params,
'unit_of_measurement': m,
}) })
req.append(params) req.append(params)
return req, ret return req, ret
@@ -69,50 +78,50 @@ class Request:
i2c_classes = { i2c_classes = {
'htu21d': [ 'htu21d': [
DEVICE_CLASS_HUMIDITY, DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'sht31': [ 'sht31': [
DEVICE_CLASS_HUMIDITY, DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'max44009': [ 'max44009': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'bh1750': [ 'bh1750': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'tsl2591': [ 'tsl2591': [
DEVICE_CLASS_ILLUMINANCE DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
], ],
'bmp180': [ 'bmp180': [
DEVICE_CLASS_PRESSURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'bmx280': [ 'bmx280': [
DEVICE_CLASS_PRESSURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DEVICE_CLASS_HUMIDITY DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None)
], ],
'mlx90614': [ 'mlx90614': [
Skip, Skip,
('temp', DEVICE_CLASS_TEMPERATURE), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'),
('object', DEVICE_CLASS_TEMPERATURE), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'),
], ],
'ptsensor': [ 'ptsensor': [
Skip, Skip,
Request(delay=1), # запрос на измерение Request(delay=1), # запрос на измерение
DEVICE_CLASS_PRESSURE, DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mcp9600': [ 'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
], ],
't67xx': [ 't67xx': [
None # для co2 нет класса в HA DeviceType(None, CONCENTRATION_PARTS_PER_MILLION, None) # для co2 нет класса в HA
], ],
'tmp117': [ 'tmp117': [
DEVICE_CLASS_TEMPERATURE, DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
] ]
} }

View File

@@ -1,11 +1,20 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio
import logging import logging
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol import voluptuous as vol
import colorsys
import time
from homeassistant.components.light import ( from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_SCHEMA, PLATFORM_SCHEMA as LIGHT_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
LightEntity, LightEntity,
SUPPORT_TRANSITION,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -16,7 +25,7 @@ from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .entities import MegaOutPort from .entities import MegaOutPort, BaseMegaEntity, safe_int
from .hub import MegaD from .hub import MegaD
from .const import ( from .const import (
@@ -24,12 +33,12 @@ from .const import (
CONF_SWITCH, CONF_SWITCH,
DOMAIN, DOMAIN,
CONF_CUSTOM, CONF_CUSTOM,
CONF_SKIP, CONF_SKIP, CONF_LED, CONF_WS28XX, CONF_PORTS, CONF_WHITE_SEP, CONF_SMOOTH, CONF_ORDER, CONF_CHIP,
) )
from .tools import int_ignore from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
# Validation of the user's configuration # Validation of the user's configuration
_EXTENDED = { _EXTENDED = {
@@ -60,11 +69,24 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
mid = config_entry.data[CONF_ID] mid = config_entry.data[CONF_ID]
hub: MegaD = hass.data['mega'][mid] hub: MegaD = hass.data['mega'][mid]
devices = [] devices = []
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
skip = []
if CONF_LED in customize:
for entity_id, conf in customize[CONF_LED].items():
ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)]
skip.extend(ports)
devices.append(MegaRGBW(
mega=hub,
port=ports,
name=entity_id,
customize=conf,
id_suffix=entity_id,
config_entry=config_entry
))
for port, cfg in config_entry.data.get('light', {}).items(): for port, cfg in config_entry.data.get('light', {}).items():
port = int_ignore(port) port = int_ignore(port)
c = customize.get(mid, {}).get(port, {}) c = customize.get(port, {})
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light': if c.get(CONF_SKIP, False) or port in skip or c.get(CONF_DOMAIN, 'light') != 'light':
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add light on port %s with data %s', port, data) hub.lg.debug(f'add light on port %s with data %s', port, data)
@@ -72,6 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
if '<' in light.name: if '<' in light.name:
continue continue
devices.append(light) devices.append(light)
async_add_devices(devices) async_add_devices(devices)
@@ -79,5 +102,221 @@ class MegaLight(MegaOutPort, LightEntity):
@property @property
def supported_features(self): def supported_features(self):
return SUPPORT_BRIGHTNESS if self.dimmer else 0 return (
(SUPPORT_BRIGHTNESS if self.dimmer else 0) |
(SUPPORT_TRANSITION if self.dimmer else 0)
)
class MegaRGBW(LightEntity, BaseMegaEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_on = None
self._brightness = None
self._hs_color = None
self._white_value = None
self._task: asyncio.Task = None
self._restore = None
self.smooth: timedelta = self.customize[CONF_SMOOTH]
self._color_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" "@andvikt"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues", "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): class MegaI2C(MegaPushEntity):
def __init__(self, *args, device_class: str, params: dict, **kwargs): def __init__(
self,
*args,
device_class: str,
params: dict,
unit_of_measurement: str = None,
**kwargs
):
self._device_class = device_class self._device_class = device_class
self._params = tuple(params.items()) self._params = tuple(params.items())
self._unit_of_measurement = unit_of_measurement
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property
def customize(self):
return super().customize.get(self.id_suffix, {}) or {}
@property
def device_class(self): def device_class(self):
return self._device_class return self._device_class
@property
def unit_of_measurement(self):
return self._unit_of_measurement
@property @property
def state(self): def state(self):
# self.lg.debug(f'get % all states: %', self._params, self.mega.values) # self.lg.debug(f'get % all states: %', self._params, self.mega.values)
@@ -122,9 +139,13 @@ class MegaI2C(MegaPushEntity):
except: except:
self.lg.warning(f'could not convert {ret} form hex to float') self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None: try:
tmpl.hass = self.hass ret = float(ret)
ret = tmpl.async_render({'value': 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 return ret
@property @property
@@ -214,10 +235,13 @@ class Mega1WSensor(MegaPushEntity):
except: except:
self.lg.warning(f'could not convert {ret} form hex to float') self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if tmpl is not None and self.hass is not None: try:
tmpl.hass = self.hass ret = float(ret)
ret = tmpl.async_render({'value': ret}) if tmpl is not None and self.hass is not None:
return ret tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
except:
return ret
@property @property
def name(self): 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. # taken already, will hit this again and wake up a new waiter.
if not fut.done(): if not fut.done():
fut.set_result(True) fut.set_result(True)

View File

@@ -20,8 +20,12 @@
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`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 - Возможность работы с несколькими 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, тк в нем нет необходимости) будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера - Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Автоматическое добавление/изменение объектов после перезагрузки контроллера - Автоматическое добавление/изменение объектов после перезагрузки контроллера