Compare commits

...

16 Commits

Author SHA1 Message Date
Andrey
ad1210d5cc fix some timeouts 2021-03-29 21:43:25 +03:00
Andrey Viktorov
687e80f729 fix empty config while first setup 2021-03-29 19:54:48 +03:00
Andrey Viktorov
7d777c9e82 fix sensors 2021-03-24 18:14:15 +03:00
Andrey Viktorov
c9f0e85f6a edit readme 2021-03-24 17:04:37 +03:00
andvikt
e75f8b91ef Update sensor.py 2021-03-24 12:40:39 +03:00
andvikt
bf15d4f3f9 Update hub.py 2021-03-24 10:57:42 +03:00
Andrey Viktorov
124ef36564 - fix order for ws28xx 2021-03-24 08:09:09 +03:00
Andrey Viktorov
9e73191a91 - add rgbw support
- add ws28 support
- add transitions to pwm
- add units of measurement for all i2c sensors
- remove mqtt support
2021-03-23 15:30:34 +03:00
Andrey Viktorov
1270ea2ee2 - add rgbw support
- add ws28 support
- add transitions to pwm
- add units of measurement for all i2c sensors
- remove mqtt support
2021-03-23 15:10:50 +03:00
andvikt
36433a7fdd Update readme.md 2021-03-11 15:46:44 +03:00
Andrey
5edf000ce8 tune restore 2021-03-05 12:38:14 +03:00
Andrey
b821d182b2 edit readme 2021-03-05 12:25:42 +03:00
Andrey
149d30e921 add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:45:41 +03:00
Andrey
84f677656a add hex_to_float option
add auto config reload on megad restart
fix reloading issues
2021-03-05 11:02:49 +03:00
Andrey
a0900052dc fix bugs 2021-03-05 00:02:43 +03:00
Andrey
b8d355f412 fix bugs 2021-03-04 22:54:21 +03:00
16 changed files with 777 additions and 372 deletions

View File

@@ -1,121 +1,15 @@
from urllib.parse import urlparse, parse_qsl order ='brg'
rgb = 'rgb'
from bs4 import BeautifulSoup map_to_order = [rgb.index(x) for x in order]
map_from_order = [order.index(x) for x in rgb]
page = '''
<html><head></head><body><a href="/sec/?pt=33">Back</a><br>0x15 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=t67xx">T67XX</a><br>0x40 - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=htu21d">HTU21D</a>/PCA9685/HM3301<br>0x4a - <a href="/sec/?pt=33&amp;scl=32&amp;i2c_dev=max44009">MAX44009</a><br>
</body></html>
'''
from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
)
def parse_scan_page(page: str): _rgb = [
ret = [] rgb[x] for x in map_to_order
req = [] ]
page = BeautifulSoup(page, features="lxml") _order = [
for x in page.find_all('a'): _rgb[x] for x in map_from_order
params = x.get('href')
if params is None:
continue
params = dict(parse_qsl(urlparse(params).query))
if 'i2c_dev' in params:
dev = params['i2c_dev']
classes = i2c_classes.get(dev, [])
for i, c in enumerate(classes):
if c is Skip:
continue
elif c is Request:
req.append(params)
continue
elif isinstance(c, tuple):
suffix, c = c
elif isinstance(c, str):
suffix = c
else:
suffix = ''
if 'addr' in params:
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
if suffix:
_dev = f'{dev}_{suffix}'
else:
_dev = dev
params = params.copy()
if i > 0:
params['i2c_par'] = i
ret.append({
'id_suffix': _dev,
'device_class': c,
'params': params,
})
req.append(params)
return req, ret
class Skip:
pass
class Request:
pass
i2c_classes = {
'htu21d': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'sht31': [
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
],
'max44009': [
DEVICE_CLASS_ILLUMINANCE
],
'bh1750': [
DEVICE_CLASS_ILLUMINANCE
],
'tsl2591': [
DEVICE_CLASS_ILLUMINANCE
],
'bmp180': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'bmx280': [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY
],
'mlx90614': [
Skip,
('temp', DEVICE_CLASS_TEMPERATURE),
('object', DEVICE_CLASS_TEMPERATURE),
],
'ptsensor': [
Request, # запрос на измерение
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
],
'mcp9600': [
DEVICE_CLASS_TEMPERATURE, # термопара
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
],
't67xx': [
None # для co2 нет класса в HA
],
'tmp117': [
DEVICE_CLASS_TEMPERATURE,
] ]
}
print(parse_scan_page(page)) print(_rgb, _order)

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_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,
@@ -47,6 +73,15 @@ CUSTOMIZE_PORT = {
vol.Optional(CONF_CONV_TEMPLATE): cv.template, vol.Optional(CONF_CONV_TEMPLATE): cv.template,
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_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
@@ -71,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,
) ),
} }
} }
}, },
@@ -122,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, **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
@@ -173,18 +198,14 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
:param entry: :param entry:
:return: :return:
""" """
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]] # hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL] # hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0) # hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
entry.data = entry.options await hass.config_entries.async_reload(entry.entry_id)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
await async_remove_entry(hass, entry)
await async_setup_entry(hass, entry)
return True return True
async def async_remove_entry(hass, entry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle removal of an entry.""" """Handle removal of an entry."""
id = entry.data.get('id', entry.entry_id) id = entry.data.get('id', entry.entry_id)
hub: MegaD = hass.data[DOMAIN].get(id) hub: MegaD = hass.data[DOMAIN].get(id)
@@ -194,12 +215,17 @@ async def async_remove_entry(hass, entry) -> None:
_hubs.pop(id, None) _hubs.pop(id, None)
hass.data[DOMAIN].pop(id, None) hass.data[DOMAIN].pop(id, None)
hass.data[DOMAIN][CONF_ALL].pop(id, None) hass.data[DOMAIN][CONF_ALL].pop(id, None)
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
task: asyncio.Task = _POLL_TASKS.pop(id, None) task: asyncio.Task = _POLL_TASKS.pop(id, None)
if task is not None: if task is not None:
task.cancel() task.cancel()
if hub is None: if hub is None:
return return
await hub.stop() await hub.stop()
return True
async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_migrate_entry(hass, config_entry: ConfigEntry):

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 = 21 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)
@@ -117,26 +116,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options."""
new_naming = self.config_entry.data.get('new_naming', False) new_naming = self.config_entry.data.get('new_naming', False)
hub = await get_hub(self.hass, self.config_entry.data)
if user_input is not None: if user_input is not None:
reload = user_input.pop(CONF_RELOAD) reload = user_input.pop(CONF_RELOAD)
cfg = dict(self.config_entry.data) cfg = dict(self.config_entry.data)
cfg.update(user_input) cfg.update(user_input)
hub = await get_hub(self.hass, cfg)
if reload:
await hub.start()
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
await hub.stop()
_LOGGER.debug(f'new config: %s', new)
cfg = dict(self.config_entry.data)
for x in PLATFORMS:
cfg.pop(x, None)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
cfg['new_naming'] = new_naming cfg['new_naming'] = new_naming
self.config_entry.data = cfg
await get_hub(self.hass, cfg)
if reload:
id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id)
cfg = await hub.reload(reload_entry=False)
return self.async_create_entry( return self.async_create_entry(
title='', title='',
data=cfg, data=cfg,
@@ -148,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"
@@ -36,6 +37,14 @@ CONF_CLICK_TIME = 'click_time'
CONF_LONG_TIME = 'long_time' 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_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",
@@ -66,4 +75,7 @@ REMOVE_CONFIG = [
'light', 'light',
'i2c', 'i2c',
'sensor', 'sensor',
'smooth',
] ]
RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')]
RGB = '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
if not isinstance(port, list):
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \ self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
_pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port _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}" + \ self._name = name or f"{mega.id}_{_pt}" + \
(f"_{id_suffix}" if id_suffix else "") (f"_{id_suffix}" if id_suffix else "")
self._customize: dict = None 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, 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):
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 _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}" c = self._name or f"{self.mega.id}_p{_pt}"
else:
c = self.id_suffix
return c return c
@property @property
@@ -135,12 +193,9 @@ 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()
@@ -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,18 +378,73 @@ 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
if transition is None:
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"} _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
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1) await self.mega.request(**_cmd, priority=-1)
if self.index is not None: if self.index is not None:
# обновление текущего стейта для ds2413 # обновление текущего стейта для ds2413
@@ -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
if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1) 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

@@ -77,7 +77,11 @@ class MegaView(HomeAssistantView):
if hub is None: if hub is None:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}') _LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
if hub is None and request.remote in ['::1', '127.0.0.1']: if hub is None and request.remote in ['::1', '127.0.0.1']:
hub = self.hubs.get('__def') try:
hub = list(self.hubs.values())[0]
except IndexError:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
return Response(status=400)
elif hub is None: elif hub is None:
return Response(status=400) return Response(status=400)
data = dict(request.query) data = dict(request.query)
@@ -87,8 +91,8 @@ class MegaView(HomeAssistantView):
) )
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote) _LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
make_ints(data) make_ints(data)
if data.get('st') == '1' and hub.restore_on_restart: if data.get('st') == '1':
asyncio.create_task(self.later_restore(hub)) hass.async_create_task(self.later_restore(hub))
return Response(status=200) return Response(status=200)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
@@ -158,10 +162,11 @@ class MegaView(HomeAssistantView):
:return: :return:
""" """
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
if hub.restore_on_restart:
await hub.restore_states() await hub.restore_states()
await hub.reload()
async def later_update(self, hub): async def later_update(self, hub):
await asyncio.sleep(1) await asyncio.sleep(1)
_LOGGER.debug('force update') _LOGGER.debug('force update')
await hub.updater.async_refresh() await hub.updater.async_refresh()

View File

@@ -9,7 +9,7 @@ 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.const import ( from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
@@ -20,9 +20,10 @@ from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN,
from .const import ( from .const import (
TEMP, HUM, PRESS, TEMP, HUM, PRESS,
LUX, PATT_SPLIT, DOMAIN, LUX, PATT_SPLIT, DOMAIN,
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
) )
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
@@ -66,10 +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,
mqtt_inputs: bool = True, config: ConfigEntry = None,
mqtt_id: str = None, mqtt_id: str = None,
scan_interval=60, scan_interval=60,
port_to_scan=0, port_to_scan=0,
@@ -87,10 +87,13 @@ 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 mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False: if config is not None:
lg.debug(f'load config: %s', config.data)
self.config = config
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP) 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}
@@ -99,8 +102,7 @@ class MegaD:
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
else: self.smooth = smooth or []
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 {}
@@ -111,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 = {}
@@ -157,23 +157,15 @@ class MegaD:
if force_d is not None: if force_d is not None:
self.customize[CONF_FORCE_D] = force_d self.customize[CONF_FORCE_D] = force_d
try: try:
if allow_hosts is not None: if allow_hosts is not None and DOMAIN in hass.data:
allow_hosts = set(allow_hosts.split(';')) allow_hosts = set(allow_hosts.split(';'))
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
hass.data[DOMAIN][CONF_HTTP].protected = protected hass.data[DOMAIN][CONF_HTTP].protected = protected
except Exception: except Exception:
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:
@@ -266,15 +258,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
@@ -303,6 +289,8 @@ class MegaD:
url = f"{url}/?{cmd}" url = f"{url}/?{cmd}"
self.lg.debug('request: %s', url) self.lg.debug('request: %s', url)
async with self._http_lck(priority): async with self._http_lck(priority):
for _ntry in range(3):
try:
async with aiohttp.request("get", url=url) as req: async with aiohttp.request("get", url=url) as req:
if req.status != 200: if req.status != 200:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text()) self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
@@ -311,6 +299,10 @@ class MegaD:
ret = await req.text() ret = await req.text()
self.lg.debug('response %s', ret) self.lg.debug('response %s', ret)
return ret return ret
except asyncio.TimeoutError:
self.lg.warning(f'timeout while requesting {url}')
await asyncio.sleep(1)
raise asyncio.TimeoutError('after 3 tries')
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
@@ -344,7 +336,6 @@ 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)
@@ -358,27 +349,12 @@ class MegaD:
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:
@@ -387,12 +363,6 @@ class MegaD:
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()
@@ -446,9 +416,6 @@ 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._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: async def authenticate(self) -> bool:
@@ -516,6 +483,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):
@@ -523,7 +491,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)
@@ -545,7 +515,7 @@ class MegaD:
for n in range(len(values)): for n in range(len(values)):
ext_page = await self.request(pt=port, ext=n) ext_page = await self.request(pt=port, ext=n)
ext_cfg = parse_config(ext_page) ext_cfg = parse_config(ext_page)
pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n}' pt = f'{port}e{n}' if not self.new_naming else f'{port:02}e{n:02}'
if ext_cfg.ety == '1': if ext_cfg.ety == '1':
ret['light'][pt].append({}) ret['light'][pt].append({})
elif ext_cfg.ety == '0': elif ext_cfg.ety == '0':
@@ -557,9 +527,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}' 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)
@@ -620,3 +591,97 @@ class MegaD:
cf=7, cf=7,
stime=datetime.now().strftime('%H:%M:%S') stime=datetime.now().strftime('%H:%M:%S')
) )
async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports)
cfg = dict(self.config.data)
for x in REMOVE_CONFIG:
cfg.pop(x, None)
cfg.update(new)
self.lg.debug(f'new config: %s', cfg)
self.config.data = cfg
if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id)
return cfg
def _wrap_port_smooth(self, from_, to_, time):
self.lg.debug('dim from %s to %s for %s seconds', from_, to_, time)
if time <= 0:
return
beg = datetime.now()
diff = to_ - from_
while True:
_pct = (datetime.now() - beg).total_seconds() / time
if _pct > 1:
return
val = from_ + round(diff * _pct)
yield val
async def smooth_dim(
self,
*config: typing.Tuple[typing.Any, int, int],
time: float,
jitter: int = 50,
ws=False,
updater=None,
can_smooth_hardware=False,
max_values=None,
chip=None,
):
"""
Плавное диммирование силами сервера, сразу нескольких портов (одной командой)
:param config: [(port, from, to), (port, from, to)]
:param time: время на диммирование
:param jitter: дополнительное замедление между командами в милисекундах
:param ws: если True, используется режим ws21xx
:param updater: функция, в которую передается текущее состояние
:param can_smooth_hardware: если True, используется аппаратная реализация smooth
:param max_values: максимальные значения (необходимы для расчета тайминга аппаратного smooth)
:param chip: кол-во чипов для ws-лент
:return:
"""
if can_smooth_hardware:
for i, (pt, from_, to_) in enumerate(config):
pct = abs(from_ - to_) / max_values[i]
tm = max([round(time / pct), 1])
await self.request(pt=pt, pwm=to_, cnt=tm)
last_step = tuple([to_ for (_, _, to_) in config])
gen = [self._wrap_port_smooth(f, t, time) for (_, f, t) in config]
c = None
stop = False
while True:
if stop:
return
await asyncio.sleep(jitter / 1000)
try:
_next_val = tuple([next(x) for x in gen])
except StopIteration:
_next_val = last_step
stop = True
if _next_val == c:
continue
if updater is not None:
updater(_next_val)
if can_smooth_hardware:
if _next_val == last_step:
return
continue
if not ws:
cmd = dict(
cmd=';'.join([f'{pt}:{_next_val[i]}' for i, (pt, _, _) in enumerate(config)])
)
await self.request(**cmd)
else:
# для адресных лент
cmd = dict(
pt=config[0][0],
chip=chip,
ws=''.join([hex(x).split('x')[1].rjust(2, '0').upper() for x in _next_val])
)
await self.request(**cmd)
if _next_val == last_step:
return
c = _next_val

View File

@@ -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, RGB,
) )
from .tools import int_ignore from .tools import int_ignore, map_reorder_rgb
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,216 @@ 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_order = self.customize.get(CONF_ORDER, 'rgb')
self._last_called: float = 0
self._max_values = None
@property
def max_values(self) -> list:
if self._max_values is None:
if self.is_ws:
self._max_values = [255] * 3
else:
self._max_values = [
255 if isinstance(x, int) else 4095 for x in self.port
]
return self._max_values
@property
def chip(self) -> int:
return self.customize.get(CONF_CHIP, 100)
@property
def is_ws(self):
return self.customize.get(CONF_WS28XX)
@property
def white_value(self):
if self.supported_features & SUPPORT_WHITE_VALUE:
return float(self.get_attribute('white_value', 0))
@property
def brightness(self):
return float(self.get_attribute('brightness', 0))
@property
def hs_color(self):
return self.get_attribute('hs_color', [0, 0])
@property
def is_on(self):
return self.get_attribute('is_on', False)
@property
def supported_features(self):
return (
SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION |
SUPPORT_COLOR |
(SUPPORT_WHITE_VALUE if len(self.port) == 4 else 0)
)
def get_rgbw(self):
if not self.is_on:
return [0 for x in range(len(self.port))] if not self.is_ws else [0] * 3
rgb = colorsys.hsv_to_rgb(
self.hs_color[0]/360, self.hs_color[1]/100, self.brightness / 255
)
rgb = [x for x in rgb]
if self.white_value is not None:
white = self.white_value
if not self.customize.get(CONF_WHITE_SEP):
white = white * (self.brightness / 255)
rgb.append(white / 255)
rgb = [
round(x * self.max_values[i]) for i, x in enumerate(rgb)
]
if self.is_ws:
# восстанавливаем мэпинг
rgb = map_reorder_rgb(rgb, RGB, self._color_order)
return rgb
async def async_turn_on(self, **kwargs):
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
self.lg.debug(f'turn on %s with kwargs %s', self.entity_id, kwargs)
if self._restore is not None:
self._restore.update(kwargs)
kwargs = self._restore
self._restore = None
_before = self.get_rgbw()
self._is_on = True
if self._task is not None:
self._task.cancel()
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def async_turn_off(self, **kwargs):
if (time.time() - self._last_called) < 0.1:
return
self._last_called = time.time()
self._restore = {
'hs_color': self.hs_color,
'brightness': self.brightness,
'white_value': self.white_value,
}
_before = self.get_rgbw()
self._is_on = False
if self._task is not None:
self._task.cancel()
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def set_color(self, _before, **kwargs):
transition = kwargs.get('transition')
update_state = transition is not None and transition > 3
for item, value in kwargs.items():
setattr(self, f'_{item}', value)
_after = self.get_rgbw()
if transition is None:
transition = self.smooth.total_seconds()
ratio = self.calc_speed_ratio(_before, _after)
transition = transition * ratio
self.async_write_ha_state()
ports = self.port if not self.is_ws else self.port*3
config = [(port, _before[i], _after[i]) for i, port in enumerate(ports)]
try:
await self.mega.smooth_dim(
*config,
time=transition,
ws=self.is_ws,
jitter=50,
updater=partial(self._update_from_rgb, update_state=update_state),
can_smooth_hardware=self.can_smooth_hardware,
max_values=self.max_values,
chip=self.chip,
)
except asyncio.CancelledError:
return
except:
self.lg.exception('while dimming')
async def async_will_remove_from_hass(self) -> None:
await super().async_will_remove_from_hass()
if self._task is not None:
self._task.cancel()
def _update_from_rgb(self, rgbw, update_state=False):
if len(self.port) == 4:
w = rgbw[-1]
rgb = rgbw[:3]
else:
w = None
rgb = rgbw
if self.is_ws:
rgb = map_reorder_rgb(
rgb, self._color_order, RGB
)
h, s, v = colorsys.rgb_to_hsv(*[x/self.max_values[i] for i, x in enumerate(rgb)])
h *= 360
s *= 100
v *= 255
self._hs_color = [h, s]
if self.is_on:
self._brightness = v
if w is not None:
if not self.customize.get(CONF_WHITE_SEP):
w = w/(self._brightness / 255)
else:
w = w
w = w / (self.max_values[-1] / 255)
self._white_value = w
# print(f'updated state {self.hs_color=} {self.brightness=}')
if update_state:
self.async_write_ha_state()
async def async_update(self):
"""
Эта штука нужна для синхронизации статуса вкл/выкл с реальностью. Если все цвета сброшены в ноль, значит мега
рестартнулась и не запомнила настройки, поэтому извещаем HA о выключении
Если вручную править цвет на стороне меги, тут изменения отражаться не будут
:return:
"""
if not self.enabled:
return
rgbw = []
for x in self.port:
data = self.coordinator.data
if not isinstance(data, dict):
return
data = data.get(x, None)
if isinstance(data, dict):
data = data.get('value')
data = safe_int(data)
if data is None:
return
rgbw.append(data)
if sum(rgbw) == 0:
self._is_on = False
self.async_write_ha_state()
def calc_speed_ratio(self, _before, _after):
ret = None
for i, x in enumerate(_before):
r = abs(x - _after[i]) / self.max_values[i]
if ret is None:
ret = r
else:
ret = max([r, ret])
return ret

View File

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

View File

@@ -1,6 +1,7 @@
"""Platform for light integration.""" """Platform for light integration."""
import logging import logging
import voluptuous as vol import voluptuous as vol
import struct
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA, PLATFORM_SCHEMA as SENSOR_SCHEMA,
@@ -19,7 +20,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .entities import MegaPushEntity from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT
from .hub import MegaD from .hub import MegaD
import re import re
@@ -103,18 +104,49 @@ 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)
return self.mega.values.get(self._params) ret = self.mega.values.get(self._params)
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex('41973333'))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
try:
ret = float(ret)
if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret})
except:
ret = ret
return str(ret)
@property @property
def device_class(self): def device_class(self):
@@ -197,11 +229,20 @@ class Mega1WSensor(MegaPushEntity):
ret = str(ret) ret = str(ret)
except: except:
ret = None ret = None
if self.customize.get(CONF_HEX_TO_FLOAT):
try:
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except:
self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
try:
ret = float(ret)
if tmpl is not None and self.hass is not None: if tmpl is not None and self.hass is not None:
tmpl.hass = self.hass tmpl.hass = self.hass
ret = tmpl.async_render({'value': ret}) ret = tmpl.async_render({'value': ret})
return ret except:
pass
return str(ret)
@property @property
def name(self): def name(self):

View File

@@ -115,3 +115,10 @@ 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)
def map_reorder_rgb(rgb: list, from_: str, to_: str):
if from_ == to_:
return rgb
mapping = [from_.index(x) for x in to_]
return [rgb[x] for x in mapping]

View File

@@ -20,16 +20,20 @@
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается - Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков `light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
`sensor`) `sensor`)
- Поддержка rgb+w лент как с использованием диммеров, так и адресных лент на чипах ws28xx и подобных,
[подробнее про rgbw](https://github.com/andvikt/mega_hacs/wiki/rgbw)
- Плавное диммирование для любых диммируемых объектов (в том числе с аппаратной поддержкой и без),
[подробнее про smooth](https://github.com/andvikt/mega_hacs/wiki/smooth)
- Возможность работы с несколькими megad - Возможность работы с несколькими megad
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http)
- Автоматическое восстановление состояний выходов после перезагрузки контроллера - Автоматическое восстановление состояний выходов после перезагрузки контроллера
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt - Автоматическое добавление/изменение объектов после перезагрузки контроллера
будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия - [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения - Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей. выполнении предыдущей.
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1) - поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) в том числе несколько шиной (начиная с версии 0.4.1)
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1) - поддержка расширителей MegaD-16I-XT, MegaD-16R-XT, MegaD-16PWM (начиная с версии 0.5.1)
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков - поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
[по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1) [по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1)