mirror of
https://github.com/andvikt/mega_hacs.git
synced 2025-12-11 00:54:28 +05:00
- 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:
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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')]
|
||||
@@ -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':
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -15,5 +15,5 @@
|
||||
"@andvikt"
|
||||
],
|
||||
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
|
||||
"version": "v0.6.3b1"
|
||||
"version": "v1.0.0b1"
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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, тк в нем нет необходимости)
|
||||
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
|
||||
- Автоматическое добавление/изменение объектов после перезагрузки контроллера
|
||||
|
||||
Reference in New Issue
Block a user