Compare commits

..

4 Commits

Author SHA1 Message Date
Викторов Андрей Германович
b7383130c2 Bump version: 1.0.9 → 1.1.0b0 2021-09-07 10:24:54 +03:00
Викторов Андрей Германович
b31922d570 add bumb2version 2021-09-07 10:24:52 +03:00
Викторов Андрей Германович
9ca1326db2 add bumb2version 2021-09-07 10:24:18 +03:00
Викторов Андрей Германович
13f4e138e5 add bumb2version 2021-09-07 10:21:28 +03:00
18 changed files with 828 additions and 1371 deletions

View File

@@ -1,6 +1,6 @@
[bumpversion] [bumpversion]
current_version = 1.1.8b14 current_version = 1.1.0b0
parse = (?P<major>\d+)(\.(?P<minor>\d+))(\.(?P<patch>\d+))(?P<release>[bf]*)(?P<build>\d*) parse = (?P<major>\d+)(\.(?P<minor>\d+))(\.(?P<patch>\d))(?P<release>[bf]*)(?P<build>\d*)
commit = True commit = True
tag = True tag = True
serialize = serialize =

18
.gitignore vendored
View File

@@ -1,18 +0,0 @@
*sync-conflict*
*.db
*.log*
*.yaml
**/__pycache__
test_*
temp_*
tr.py
experiment_*
example*
.HA_VERSION
.idea
.storage
site
.DS_Store
.venv
pyproject.toml
poetry.lock

View File

@@ -6,52 +6,17 @@ from functools import partial
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME, CONF_DOMAIN,
CONF_DOMAIN, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT, CONF_UNIQUE_ID
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.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from .const import ( from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
DOMAIN, CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_INVERT, CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \
PLATFORMS, RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE
CONF_PORTS,
CONF_CUSTOM,
CONF_SKIP,
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,
RGB_COMBINATIONS,
CONF_WS28XX,
CONF_ORDER,
CONF_SMOOTH,
CONF_LED,
CONF_WHITE_SEP,
CONF_CHIP,
CONF_RANGE,
CONF_FILTER_VALUES,
CONF_FILTER_SCALE,
CONF_FILTER_LOW,
CONF_FILTER_HIGH,
CONF_FILL_NA,
CONF_MEGA_ID,
CONF_ADDR,
CONF_1WBUS,
)
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
@@ -60,13 +25,14 @@ _LOGGER = logging.getLogger(__name__)
_port_n = vol.Any(int, str) _port_n = vol.Any(int, str)
LED_LIGHT = { LED_LIGHT = \
{
str: vol.Any( str: vol.Any(
{ {
vol.Required(CONF_PORTS): vol.Any( vol.Required(CONF_PORTS): vol.Any(
vol.ExactSequence([_port_n, _port_n, _port_n]), vol.ExactSequence([_port_n, _port_n, _port_n]),
vol.ExactSequence([_port_n, _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", msg='ports must be [R, G, B] or [R, G, B, W] of integers 0..255'
), ),
vol.Optional(CONF_NAME): str, vol.Optional(CONF_NAME): str,
vol.Optional(CONF_WHITE_SEP, default=True): bool, vol.Optional(CONF_WHITE_SEP, default=True): bool,
@@ -76,46 +42,45 @@ LED_LIGHT = {
vol.Required(CONF_PORT): int, vol.Required(CONF_PORT): int,
vol.Required(CONF_WS28XX): True, vol.Required(CONF_WS28XX): True,
vol.Optional(CONF_CHIP, default=100): int, vol.Optional(CONF_CHIP, default=100): int,
vol.Optional(CONF_ORDER, default="rgb"): vol.Any( vol.Optional(CONF_ORDER, default='rgb'): vol.Any(*RGB_COMBINATIONS, msg=f'order must be one of {RGB_COMBINATIONS}'),
*RGB_COMBINATIONS, msg=f"order must be one of {RGB_COMBINATIONS}"
),
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds, vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
vol.Optional(CONF_NAME): str, vol.Optional(CONF_NAME): str,
}, },
) )
} }
ENCODER = \
{
vol.Optional('int', description='int порт'): int,
vol.Required('scl', description='scl порт'): int,
vol.Optional('sync_to', description='entity_id с которым синхронизироваться'): str,
}
CUSTOMIZE_PORT = { CUSTOMIZE_PORT = {
vol.Optional( vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
CONF_SKIP, description="исключить порт из сканирования", default=False
): bool,
vol.Optional(CONF_FILL_NA, default="last"): vol.Any("last", "none"),
vol.Optional(CONF_RANGE, description="диапазон диммирования"): [
vol.Range(0, 255),
vol.Range(0, 255),
],
vol.Optional(CONF_INVERT, default=False): bool, vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_NAME): vol.Any(str, {vol.Required(str): str}), vol.Optional(CONF_NAME): vol.Any(str, {
vol.Optional(CONF_DOMAIN): vol.Any("light", "switch"), vol.Required(str): str
vol.Optional( }),
CONF_UNIT_OF_MEASUREMENT, vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
description="единицы измерений, либо строка либо мепинг", vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'):
): vol.Any(str, {vol.Required(str): str}), vol.Any(str, {
vol.Optional(CONF_DEVICE_CLASS): vol.Any(str, {vol.Required(str): str}), vol.Required(str): str
}),
vol.Optional(CONF_DEVICE_CLASS):
vol.Any(str, {
vol.Required(str): str
}),
vol.Optional( vol.Optional(
CONF_RESPONSE_TEMPLATE, CONF_RESPONSE_TEMPLATE,
description="шаблон ответа когда на этот порт приходит" "сообщение из меги ", description='шаблон ответа когда на этот порт приходит'
): cv.template, 'сообщение из меги '): cv.template,
vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано
vol.Optional(CONF_GET_VALUE, default=True): bool, vol.Optional(CONF_GET_VALUE, default=True): bool,
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_HEX_TO_FLOAT): bool,
vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)],
vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float),
vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
vol.Optional(CONF_SMOOTH): cv.time_period_seconds, vol.Optional(CONF_SMOOTH): cv.time_period_seconds,
# vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений # vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений
vol.Optional(str): { vol.Optional(str): {
@@ -123,64 +88,45 @@ CUSTOMIZE_PORT = {
vol.Optional(CONF_DEVICE_CLASS): str, vol.Optional(CONF_DEVICE_CLASS): str,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, 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
} }
def extender(x): def extender(x):
if isinstance(x, str) and "e" in x: if isinstance(x, str) and 'e' in x:
return x return x
else: else:
raise ValueError('must has "e" in port name') raise ValueError('must has "e" in port name')
OWBUS = vol.Schema(
{
vol.Required(CONF_PORT): vol.Any(vol.Coerce(int), vol.Coerce(str)),
vol.Required(CONF_MEGA_ID): vol.Coerce(str),
vol.Required(CONF_ADDR): [str],
}
)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str], vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Optional("entities"): { vol.Optional(vol.Any(str, int), description='id меги из веб-интерфейса'): {
vol.Optional(str): vol.Any(CUSTOMIZE_PORT, CUSTOMIZE_DS2413) vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
},
vol.Optional(vol.Any(str, int), description="id меги из веб-интерфейса"): {
vol.Optional( vol.Optional(
CONF_FORCE_D, CONF_DEF_RESPONSE,
description="Принудительно слать d после срабатывания входа", description='Ответ по умолчанию',
default=False, default=None
): bool,
vol.Optional(
CONF_DEF_RESPONSE, description="Ответ по умолчанию", default=None
): vol.Any(cv.template, None), ): vol.Any(cv.template, None),
vol.Optional(CONF_LED): LED_LIGHT, vol.Optional(CONF_LED): LED_LIGHT,
vol.Optional( vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
vol.Any(int, extender), description="номер порта"
): vol.Any(
CUSTOMIZE_PORT, CUSTOMIZE_PORT,
CUSTOMIZE_DS2413, CUSTOMIZE_DS2413,
), ),
vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)], vol.Optional('encoders'): {str: ENCODER}
vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float), }
vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
},
vol.Optional(CONF_1WBUS): [OWBUS],
} }
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
ALIVE_STATE = "alive" ALIVE_STATE = 'alive'
DEF_ID = "def" DEF_ID = 'def'
_POLL_TASKS = {} _POLL_TASKS = {}
_hubs = {} _hubs = {}
_subs = {} _subs = {}
@@ -194,40 +140,29 @@ async def async_setup(hass: HomeAssistant, config: dict):
view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, [])) view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
hass.http.register_view(view) hass.http.register_view(view)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
"save", vol.Optional('mega_id'): str
partial(_save_service, hass), })
schema=vol.Schema({vol.Optional("mega_id"): str}),
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({
"get_port", vol.Optional('mega_id'): str,
partial(_get_port, hass), vol.Optional('port'): vol.Any(int, [int]),
schema=vol.Schema( })
{
vol.Optional("mega_id"): str,
vol.Optional("port"): vol.Any(int, [int]),
}
),
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({
"run_cmd", vol.Optional('port'): int,
partial(_run_cmd, hass), vol.Required('cmd'): str,
schema=vol.Schema( vol.Optional('mega_id'): str,
{ })
vol.Optional("port"): int,
vol.Required("cmd"): str,
vol.Optional("mega_id"): str,
}
),
) )
return True return True
async def get_hub(hass, entry): async def get_hub(hass, entry):
id = entry.data.get("id", entry.entry_id) id = entry.data.get('id', entry.entry_id)
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)
@@ -237,7 +172,7 @@ async def get_hub(hass, entry):
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry): async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
id = entry.data.get("id", entry.entry_id) id = entry.data.get('id', entry.entry_id)
hub = await get_hub(hass, entry) hub = await get_hub(hass, entry)
hub.fw = await hub.get_fw() hub.fw = await hub.get_fw()
hass.data[DOMAIN][id] = hub hass.data[DOMAIN][id] = hub
@@ -254,8 +189,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_hubs[entry.entry_id] = hub _hubs[entry.entry_id] = hub
_subs[entry.entry_id] = entry.add_update_listener(updater) _subs[entry.entry_id] = entry.add_update_listener(updater)
await hub.start() await hub.start()
# for platform in PLATFORMS: for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
entry, platform
)
)
await hub.updater.async_refresh() await hub.updater.async_refresh()
return True return True
@@ -276,11 +215,11 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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)
if hub is None: if hub is None:
return True return
_LOGGER.debug(f"remove {id}") _LOGGER.debug(f'remove {id}')
_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)
@@ -290,29 +229,25 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
if task is not None: if task is not None:
task.cancel() task.cancel()
if hub is None: if hub is None:
return True return
await hub.stop() await hub.stop()
return True return True
async_unload_entry = async_remove_entry async_unload_entry = async_remove_entry
async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry.""" """Migrate old entry."""
_LOGGER.debug( _LOGGER.debug("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION)
"Migrating from version %s to version %s",
config_entry.version,
ConfigFlow.VERSION,
)
hub = await get_hub(hass, config_entry) hub = await get_hub(hass, config_entry)
new = dict(config_entry.data) new = dict(config_entry.data)
await hub.start() await hub.start()
cfg = await hub.get_config() cfg = await hub.get_config()
await hub.stop() await hub.stop()
new.update(cfg) new.update(cfg)
_LOGGER.debug(f"new config: %s", new) _LOGGER.debug(f'new config: %s', new)
hass.config_entries.async_update_entry(config_entry, data=new, version=ConfigFlow.VERSION) config_entry.data = new
config_entry.version = ConfigFlow.VERSION
_LOGGER.info("Migration to version %s successful", config_entry.version) _LOGGER.info("Migration to version %s successful", config_entry.version)
@@ -320,7 +255,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
async def _save_service(hass: HomeAssistant, call: ServiceCall): async def _save_service(hass: HomeAssistant, call: ServiceCall):
mega_id = call.data.get("mega_id") mega_id = call.data.get('mega_id')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.save() await hub.save()
@@ -332,8 +267,8 @@ async def _save_service(hass: HomeAssistant, call: ServiceCall):
@bind_hass @bind_hass
async def _get_port(hass: HomeAssistant, call: ServiceCall): async def _get_port(hass: HomeAssistant, call: ServiceCall):
port = call.data.get("port") port = call.data.get('port')
mega_id = call.data.get("mega_id") mega_id = call.data.get('mega_id')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
if port is None: if port is None:
@@ -343,7 +278,6 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
elif isinstance(port, list): elif isinstance(port, list):
for x in port: for x in port:
await hub.get_port(x) await hub.get_port(x)
hub.updater.async_set_updated_data(hub.values)
else: else:
for hub in hass.data[DOMAIN][CONF_ALL].values(): for hub in hass.data[DOMAIN][CONF_ALL].values():
if not isinstance(hub, MegaD): if not isinstance(hub, MegaD):
@@ -355,13 +289,12 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
elif isinstance(port, list): elif isinstance(port, list):
for x in port: for x in port:
await hub.get_port(x) await hub.get_port(x)
hub.updater.async_set_updated_data(hub.values)
@bind_hass @bind_hass
async def _run_cmd(hass: HomeAssistant, call: ServiceCall): async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
mega_id = call.data.get("mega_id") mega_id = call.data.get('mega_id')
cmd = call.data.get("cmd") cmd = call.data.get('cmd')
if mega_id: if mega_id:
hub: MegaD = hass.data[DOMAIN][mega_id] hub: MegaD = hass.data[DOMAIN][mega_id]
await hub.request(cmd=cmd) await hub.request(cmd=cmd)

View File

@@ -17,7 +17,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 .const import DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_CUSTOM, CONF_SKIP, CONF_INVERT, CONF_RESPONSE_TEMPLATE
from .entities import MegaPushEntity from .entities import MegaPushEntity
from .hub import MegaD from .hub import MegaD
from .tools import int_ignore from .tools import int_ignore
@@ -67,7 +67,6 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.mega.binary_sensors.append(self.port)
self._is_on = None self._is_on = None
self._attrs = None self._attrs = None
self._click_task = None self._click_task = None

View File

@@ -8,19 +8,9 @@ from homeassistant import config_entries, core
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 ( from .const import DOMAIN, CONF_RELOAD, \
DOMAIN, CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
CONF_RELOAD, CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME
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,
)
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -28,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema( 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=30): int, vol.Optional(CONF_SCAN_INTERVAL, default=30): int,
@@ -41,7 +31,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_FORCE_D, default=True): bool, vol.Optional(CONF_FORCE_D, default=True): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool, vol.Optional(CONF_RESTORE_ON_RESTART, default=True): bool,
vol.Optional(CONF_PROTECTED, default=True): bool, vol.Optional(CONF_PROTECTED, default=True): bool,
vol.Optional(CONF_ALLOW_HOSTS, default="::1;127.0.0.1"): str, vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
vol.Optional(CONF_UPDATE_TIME, default=True): bool, vol.Optional(CONF_UPDATE_TIME, default=True): bool,
}, },
) )
@@ -64,7 +54,7 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
""" """
if data[CONF_ID] in hass.data.get(DOMAIN, []): if data[CONF_ID] in hass.data.get(DOMAIN, []):
raise exceptions.DuplicateId("duplicate_id") raise exceptions.DuplicateId('duplicate_id')
hub = await get_hub(hass, data) hub = await get_hub(hass, data)
return hub return hub
@@ -73,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 = 27 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):
@@ -91,9 +81,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
hub.new_naming=True 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)
config.update(user_input) config.update(user_input)
config["new_naming"] = True config['new_naming'] = True
return self.async_create_entry( return self.async_create_entry(
title=user_input.get(CONF_ID, user_input[CONF_HOST]), title=user_input.get(CONF_ID, user_input[CONF_HOST]),
data=config, data=config,
@@ -119,66 +109,48 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: ConfigEntry): def __init__(self, config_entry: ConfigEntry):
self.config_entry = config_entry self.config_entry = config_entry
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)
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)
cfg["new_naming"] = new_naming cfg['new_naming'] = new_naming
self.hass.config_entries.async_update_entry(entry=self.config_entry, data=cfg) self.config_entry.data = cfg
await get_hub(self.hass, cfg) await get_hub(self.hass, cfg)
if reload: if reload:
id = self.config_entry.data.get("id", self.config_entry.entry_id) id = self.config_entry.data.get('id', self.config_entry.entry_id)
hub: MegaD = self.hass.data[DOMAIN].get(id) hub: MegaD = self.hass.data[DOMAIN].get(id)
cfg = await hub.reload(reload_entry=False) cfg = await hub.reload(reload_entry=False)
return self.async_create_entry( return self.async_create_entry(
title="", title='',
data=cfg, data=cfg,
) )
e = self.config_entry.data e = self.config_entry.data
ret = self.async_show_form( ret = self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema( data_schema=vol.Schema({
{ vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
vol.Optional( vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
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_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( vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True) vol.Optional(CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)): bool,
): bool, vol.Optional(CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)): bool,
vol.Optional( vol.Optional(CONF_RESTORE_ON_RESTART, default=e.get(CONF_RESTORE_ON_RESTART, False)): bool,
CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True) vol.Optional(CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)): bool,
): bool, vol.Optional(CONF_ALLOW_HOSTS, default='::1;127.0.0.1'): str,
vol.Optional( vol.Optional(CONF_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)): bool,
CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)
): bool,
vol.Optional(
CONF_RESTORE_ON_RESTART,
default=e.get(CONF_RESTORE_ON_RESTART, False),
): bool,
vol.Optional(
CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)
): bool,
vol.Optional(CONF_ALLOW_HOSTS, default="::1;127.0.0.1"): str,
vol.Optional(
CONF_UPDATE_TIME, default=e.get(CONF_UPDATE_TIME, False)
): bool,
# vol.Optional(CONF_INVERT, default=''): str, # vol.Optional(CONF_INVERT, default=''): str,
} }),
),
) )
return ret return ret

View File

@@ -1,7 +1,6 @@
"""Constants for the mega integration.""" """Constants for the mega integration."""
import re import re
from itertools import permutations from itertools import permutations
from homeassistant.const import Platform
DOMAIN = "mega" DOMAIN = "mega"
CONF_MEGA_ID = "mega_id" CONF_MEGA_ID = "mega_id"
@@ -46,18 +45,11 @@ CONF_SMOOTH = 'smooth'
CONF_WHITE_SEP = 'white_sep' CONF_WHITE_SEP = 'white_sep'
CONF_CHIP = 'chip' CONF_CHIP = 'chip'
CONF_RANGE = 'range' CONF_RANGE = 'range'
CONF_FILL_NA = 'fill_na'
CONF_FILTER_VALUES = 'filter_values'
CONF_FILTER_SCALE = 'filter_scale'
CONF_FILTER_LOW = 'filter_low'
CONF_FILTER_HIGH = 'filter_high'
CONF_1WBUS = '1wbus'
CONF_ADDR = 'addr'
PLATFORMS = [ PLATFORMS = [
Platform.LIGHT, "light",
Platform.SWITCH, "switch",
Platform.BINARY_SENSOR, "binary_sensor",
Platform.SENSOR, "sensor",
] ]
EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor' EVENT_BINARY_SENSOR = f'{DOMAIN}.sensor'
EVENT_BINARY = f'{DOMAIN}.binary' EVENT_BINARY = f'{DOMAIN}.binary'

View File

@@ -8,24 +8,11 @@ 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
from homeassistant.core import State from homeassistant.core import State
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 ( from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \
DOMAIN, LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY, CONF_SMOOTH
CONF_CUSTOM,
CONF_INVERT,
LONG,
LONG_RELEASE,
RELEASE,
PRESS,
SINGLE_CLICK,
DOUBLE_CLICK,
EVENT_BINARY,
CONF_SMOOTH,
CONF_RANGE,
)
_events_on = False _events_on = False
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -34,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
async def _set_events_on(): async def _set_events_on():
global _events_on, _task_set_ev_on global _events_on, _task_set_ev_on
await asyncio.sleep(10) await asyncio.sleep(10)
_LOGGER.debug("events on") _LOGGER.debug('events on')
_events_on = True _events_on = True
@@ -43,7 +30,6 @@ def set_events_off():
_events_on = False _events_on = False
_task_set_ev_on = None _task_set_ev_on = None
_task_set_ev_on = None _task_set_ev_on = None
@@ -53,22 +39,22 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
Also provides some basic entity information: unique_id, name, availiability Also provides some basic entity information: unique_id, name, availiability
All base entities are polled in order to be online or offline All base entities are polled in order to be online or offline
""" """
def __init__( def __init__(
self, self,
mega: "h.MegaD", mega: 'h.MegaD',
port: typing.Union[int, str, typing.List[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,
unique_id=None, unique_id=None,
http_cmd="get", http_cmd='get',
addr: str=None, addr: str=None,
index=None, index=None,
customize=None, customize=None,
smooth=None, smooth=None,
**kwargs, **kwargs,
): ):
super().__init__(mega.updater)
self._smooth = smooth self._smooth = smooth
self.http_cmd = http_cmd self.http_cmd = http_cmd
self._state: State = None self._state: State = None
@@ -79,19 +65,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._mega_id = mega.id self._mega_id = mega.id
self._lg = None self._lg = None
if not isinstance(port, list): 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 = ( self._name = name or f"{mega.id}_{_pt}" + \
port (f"_{id_suffix}" if id_suffix else "")
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 self._customize: dict = None
else: else:
assert id_suffix is not None assert id_suffix is not None
@@ -105,21 +83,20 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self.addr = addr self.addr = addr
self.id_suffix = id_suffix self.id_suffix = id_suffix
self._can_smooth_hard = None 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}
super().__init__(coordinator=mega.updater)
@property @property
def is_ws(self): def is_ws(self):
return False return False
def get_attribute(self, name, default=None): def get_attribute(self, name, default=None):
attr = getattr(self, f"_{name}", None) attr = getattr(self, f'_{name}', None)
if attr is None and self._state is not None: if attr is None and self._state is not None:
if name == "is_on": if name == 'is_on':
attr = self._state.state attr = self._state.state
else: else:
attr = self._state.attributes.get(f"{name}", default) attr = self._state.attributes.get(f'{name}', default)
return attr if attr is not None else default return attr if attr is not None else default
@property @property
@@ -140,7 +117,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@property @property
def enabled(self): def enabled(self):
if "<" in self.name: if '<' in self.name:
return False return False
else: else:
return super().enabled return super().enabled
@@ -149,56 +126,49 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def customize(self): def customize(self):
if self._customize is not None: if self._customize is not None:
return self._customize return self._customize
if self.hass is None or self.entity_id is None: if self.hass is None:
return {} return {}
if self._customize is None: if self._customize is None:
c_entity_id = (
self.hass.data.get(DOMAIN, {})
.get(CONF_CUSTOM)
.get("entities", {})
.get(self.entity_id, {})
)
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {} c = c.get(self._mega_id) or {}
c = c.get(self.port) or {} c = c.get(self.port) or {}
if self.addr is not None and self.index is not None and isinstance(c, dict): if self.addr is not None and self.index is not None and isinstance(c, dict):
idx = self.addr.lower() + f"_a" if self.index == 0 else "_b" idx = self.addr.lower() + f'_a' if self.index == 0 else '_b'
c = c.get(idx, {}) c = c.get(idx, {})
c.update(c_entity_id)
self._customize = c self._customize = c
return self._customize return self._customize
@property @property
def device_info(self) -> DeviceInfo: def device_info(self):
if isinstance(self.port, list): if isinstance(self.port, list):
pt_idx = self.id_suffix pt_idx = self.id_suffix
else: else:
_pt = ( _pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
self.port if isinstance(_pt, str) and 'e' in _pt:
if not self.mega.new_naming pt_idx, _ = _pt.split('e')
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: else:
pt_idx = _pt pt_idx = _pt
return DeviceInfo( 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}", pt_idx) (DOMAIN, f'{self._mega_id}', pt_idx),
}, },
name=self.name, "config_entries": [
manufacturer="ab-log.ru", self.config_entry,
sw_version=self.mega.fw, ],
via_device=(DOMAIN, self._mega_id), "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,
"via_device": (DOMAIN, self._mega_id),
}
@property @property
def lg(self) -> logging.Logger: def lg(self) -> logging.Logger:
return _LOGGER if self._lg is None:
self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return self._lg
@property @property
def available(self) -> bool: def available(self) -> bool:
@@ -209,13 +179,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
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): if not isinstance(self.port, list):
_pt = ( _pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
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: else:
c = self.id_suffix c = self.id_suffix
@@ -231,7 +195,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._state = await self.async_get_last_state() self._state = await self.async_get_last_state()
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)
self.async_write_ha_state() self.async_write_ha_state()
@@ -251,42 +215,51 @@ class MegaPushEntity(BaseMegaEntity):
if self.hass is None: if self.hass is None:
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 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
ll: bool = self.mega.last_long.get(self.port, False) ll: bool = self.mega.last_long.get(self.port, False)
if safe_int(value.get("click", 0)) == 1: if safe_int(value.get('click', 0)) == 1:
self.hass.bus.async_fire( self.hass.bus.async_fire(
event_type=EVENT_BINARY, event_type=EVENT_BINARY,
event_data={"entity_id": self.entity_id, "type": SINGLE_CLICK}, event_data={
'entity_id': self.entity_id,
'type': SINGLE_CLICK
}
) )
elif safe_int(value.get("click", 0)) == 2: elif safe_int(value.get('click', 0)) == 2:
self.hass.bus.async_fire( self.hass.bus.async_fire(
event_type=EVENT_BINARY, event_type=EVENT_BINARY,
event_data={"entity_id": self.entity_id, "type": DOUBLE_CLICK}, event_data={
'entity_id': self.entity_id,
'type': DOUBLE_CLICK
}
) )
elif safe_int(value.get("m", 0)) == 2: elif safe_int(value.get('m', 0)) == 2:
self.mega.last_long[self.port] = True self.mega.last_long[self.port] = True
self.hass.bus.async_fire( self.hass.bus.async_fire(
event_type=EVENT_BINARY, event_type=EVENT_BINARY,
event_data={"entity_id": self.entity_id, "type": LONG}, event_data={
'entity_id': self.entity_id,
'type': LONG
}
) )
elif safe_int(value.get("m", 0)) == 1: elif safe_int(value.get('m', 0)) == 1:
self.hass.bus.async_fire( self.hass.bus.async_fire(
event_type=EVENT_BINARY, event_type=EVENT_BINARY,
event_data={ event_data={
"entity_id": self.entity_id, 'entity_id': self.entity_id,
"type": LONG_RELEASE if ll else RELEASE, 'type': LONG_RELEASE if ll else RELEASE,
}, }
) )
elif safe_int(value.get("m", None)) == 0: elif safe_int(value.get('m', None)) == 0:
self.hass.bus.async_fire( self.hass.bus.async_fire(
event_type=EVENT_BINARY, event_type=EVENT_BINARY,
event_data={ event_data={
"entity_id": self.entity_id, 'entity_id': self.entity_id,
"type": PRESS, 'type': PRESS,
}, }
) )
self.mega.last_long[self.port] = False self.mega.last_long[self.port] = False
return return
@@ -296,13 +269,21 @@ class MegaPushEntity(BaseMegaEntity):
class MegaOutPort(MegaPushEntity): class MegaOutPort(MegaPushEntity):
def __init__(self, dimmer=False, dimmer_scale=1, *args, **kwargs):
super().__init__(*args, **kwargs) def __init__(
self,
dimmer=False,
dimmer_scale=1,
*args, **kwargs
):
super().__init__(
*args, **kwargs
)
self._brightness = None self._brightness = None
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.is_extender = isinstance(self.port, str) and 'e' in self.port
self.task: asyncio.Task = None self.task: asyncio.Task = None
self._restore_brightness = None self._restore_brightness = None
self._last_called: float = 0 self._last_called: float = 0
@@ -320,115 +301,80 @@ class MegaOutPort(MegaPushEntity):
else: else:
return 255 return 255
@property
def range(self) -> typing.List[int]:
return self.customize.get(CONF_RANGE, [0, 255])
@property @property
def invert(self): def invert(self):
return self.customize.get(CONF_INVERT, False) return self.customize.get(CONF_INVERT, False)
@property @property
def brightness(self): def brightness(self):
ret = None
if not self.dimmer: if not self.dimmer:
return return
val = self.mega.values.get(self.port, {}) val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None: if isinstance(val, dict) and len(val) == 0 and self._state is not None:
ret = safe_int( return self._state.attributes.get("brightness")
self._state.attributes.get("brightness"), elif isinstance(self.port, str) and 'e' in self.port:
def_on=self.max_dim,
def_off=0,
def_val=0,
)
ret = self._calc_brightness(ret)
elif isinstance(self.port, str) and "e" in self.port:
if isinstance(val, str): if isinstance(val, str):
val = safe_int(val, def_on=self.max_dim, def_off=0, def_val=0) val = safe_int(val)
else: else:
val = 0 val = 0
if val == 0: if val == 0:
ret = self._brightness return self._brightness
elif isinstance(val, (int, float)): elif isinstance(val, (int, float)):
ret = int(val / self.dimmer_scale) return int(val / self.dimmer_scale)
elif val is not None: elif val is not None:
val = val.get("value") val = val.get("value")
if val is None: if val is None:
return return
try: try:
val = int(val) val = int(val)
ret = val return val
except Exception: except Exception:
pass pass
ret = self._cal_reverse_brightness(ret)
return ret
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
val = self.mega.values.get(self.port, {}) val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None: if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state == "ON" return self._state == 'ON'
elif isinstance(self.port, str) and "e" in self.port and val: elif isinstance(self.port, str) and 'e' in self.port and val:
if val is None: if val is None:
return return
if hasattr(self, "dimmer") and self.dimmer: if self.dimmer:
val = safe_int(val) val = safe_int(val)
if val is not None: if val is not None:
return val > 0 if not self.invert else val == 0 return val > 0 if not self.invert else val == 0
else: else:
return val == "ON" if not self.invert else val == "OFF" return val == 'ON' if not self.invert else val == 'OFF'
elif val is not None: elif val is not None:
val = val.get("value") val = val.get("value")
if ( if not isinstance(val, str) and self.index is not None and self.addr is not None:
not isinstance(val, str)
and self.index is not None
and self.addr is not None
):
if not isinstance(val, dict): if not isinstance(val, dict):
self.mega.lg.warning(f"{self.entity_id}: {val} is not a dict") self.mega.lg.warning(f'{self.entity_id}: {val} is not a dict')
return return
_val = val.get( _val = val.get(self.addr, val.get(self.addr.lower(), val.get(self.addr.upper())))
self.addr, val.get(self.addr.lower(), val.get(self.addr.upper()))
)
if not isinstance(_val, str): if not isinstance(_val, str):
self.mega.lg.warning( self.mega.lg.warning(f'{self.entity_id}: can not get {self.addr} from {val}, recieved {_val}')
f"{self.entity_id}: can not get {self.addr} from {val}, recieved {_val}"
)
return return
_val = _val.split("/") _val = _val.split('/')
if len(_val) >= 2: if len(_val) >= 2:
self.mega.lg.debug( self.mega.lg.debug('%s parsed values: %s[%s]="%s"', self.entity_id, _val, self.index, _val)
'%s parsed values: %s[%s]="%s"',
self.entity_id,
_val,
self.index,
_val,
)
val = _val[self.index] val = _val[self.index]
else: else:
self.mega.lg.warning(f"{self.entity_id}: {_val} has wrong length") self.mega.lg.warning(f'{self.entity_id}: {_val} has wrong length')
return return
elif self.index is not None and self.addr is None: elif self.index is not None and self.addr is None:
self.mega.lg.warning(f"{self.entity_id} does not has addr") self.mega.lg.warning(f'{self.entity_id} does not has addr')
return return
self.mega.lg.debug("%s.state = %s", self.entity_id, val) self.mega.lg.debug('%s.state = %s', self.entity_id, val)
if not self.invert: if not self.invert:
return ( return val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0)
val == "ON"
or str(val) == "1"
or (safe_int(val) is not None and safe_int(val) > 0)
)
else: else:
return ( return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0)
val == "OFF"
or str(val) == "0"
or (safe_int(val) is not None and safe_int(val) == 0)
)
@property @property
def cmd_port(self): def cmd_port(self):
if self.index is not None: if self.index is not None:
return f"{self.port}A" if self.index == 0 else f"{self.port}B" return f'{self.port}A' if self.index == 0 else f'{self.port}B'
else: else:
return self.port return self.port
@@ -449,7 +395,9 @@ class MegaOutPort(MegaPushEntity):
if isinstance(self.port, str): if isinstance(self.port, str):
self.mega.values[self.port] = value[0] self.mega.values[self.port] = value[0]
else: else:
self.mega.values[self.port] = {"value": value[0]} self.mega.values[self.port] = {
'value': value[0]
}
if update_state: if update_state:
self.async_write_ha_state() self.async_write_ha_state()
@@ -459,37 +407,13 @@ class MegaOutPort(MegaPushEntity):
tm = (self.smooth.total_seconds() * pct) if transition is None else transition tm = (self.smooth.total_seconds() * pct) if transition is None else transition
if self.task is not None: if self.task is not None:
self.task.cancel() self.task.cancel()
self.task = asyncio.create_task( self.task = asyncio.create_task(self.mega.smooth_dim(
self.mega.smooth_dim(
(self.cmd_port, from_, to_), (self.cmd_port, from_, to_),
time=tm, time=tm,
can_smooth_hardware=self.can_smooth_hardware, can_smooth_hardware=self.can_smooth_hardware,
max_values=[255 if self.dimmer_scale == 1 else 4095], max_values=[255 if self.dimmer_scale == 1 else 4095],
updater=partial(self.update_from_smooth, update_state=update_state), updater=partial(self.update_from_smooth, update_state=update_state),
) ))
)
def _calc_brightness(self, brightness):
if brightness is None:
brightness = 0
pct = brightness / 255
pct = max((0, pct))
pct = min((pct, 1))
l, h = self.range
d = h - l
brightness = round(pct * d + l)
return brightness
def _cal_reverse_brightness(self, brightness):
if brightness is None:
brightness = 0
l, h = self.range
d = h - l
pct = (brightness - l) / d
pct = max((0, pct))
pct = min((pct, 1))
brightness = round(pct * 255)
return brightness
async def async_turn_on(self, brightness=None, transition=None, **kwargs): async def async_turn_on(self, brightness=None, transition=None, **kwargs):
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
@@ -500,12 +424,11 @@ class MegaOutPort(MegaPushEntity):
if not self.is_on and (brightness is None or brightness == 0): if not self.is_on and (brightness is None or brightness == 0):
brightness = self._restore_brightness brightness = self._restore_brightness
brightness = brightness or self.brightness or 255 brightness = brightness or self.brightness or 255
brightness = self._calc_brightness(brightness)
_prev = safe_int(self.brightness) or 0 _prev = safe_int(self.brightness) or 0
self._brightness = brightness self._brightness = brightness
if hasattr(self, "dimmer") and self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
cmd = self.max_dim cmd = self.max_dim
elif hasattr(self, "dimmer") and self.dimmer: elif self.dimmer:
cmd = min((brightness * self.dimmer_scale, self.max_dim)) cmd = min((brightness * self.dimmer_scale, self.max_dim))
if self.smooth_dim or transition: if self.smooth_dim or transition:
self._set_dim_brightness(from_=_prev, to_=cmd, transition=transition) self._set_dim_brightness(from_=_prev, to_=cmd, transition=transition)
@@ -520,7 +443,7 @@ class MegaOutPort(MegaPushEntity):
"cnt": round(transition / (abs(_prev - brightness) / 255)), "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): 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:
@@ -529,31 +452,29 @@ class MegaOutPort(MegaPushEntity):
port=self.port, port=self.port,
force_http=True, force_http=True,
conv=False, conv=False,
http_cmd="list", http_cmd='list',
) )
elif isinstance(self.port, str) and "e" in self.port: elif isinstance(self.port, str) and 'e' in self.port:
if not self.dimmer: if not self.dimmer:
self.mega.values[self.port] = "ON" if not self.invert else "OFF" self.mega.values[self.port] = 'ON' if not self.invert else 'OFF'
else: else:
self.mega.values[self.port] = cmd self.mega.values[self.port] = cmd
else: else:
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, transition=None, **kwargs) -> None: async def async_turn_off(self, transition=None, **kwargs) -> None:
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
return return
self._last_called = time.time() self._last_called = time.time()
self._restore_brightness = self._cal_reverse_brightness( self._restore_brightness = safe_int(self._brightness)
safe_int(self._brightness)
)
if not self.dimmer: if not self.dimmer:
transition = None 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 _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): if not (self.smooth_dim or transition):
await self.mega.request(**_cmd, priority=-1) await self.mega.request(**_cmd, priority=-1)
else: else:
@@ -568,12 +489,12 @@ class MegaOutPort(MegaPushEntity):
port=self.port, port=self.port,
force_http=True, force_http=True,
conv=False, conv=False,
http_cmd="list", http_cmd='list',
) )
elif isinstance(self.port, str) and "e" in self.port: elif isinstance(self.port, str) and 'e' in self.port:
self.mega.values[self.port] = "OFF" if not self.invert else "ON" self.mega.values[self.port] = 'OFF' if not self.invert else 'ON'
else: else:
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: async def async_will_remove_from_hass(self) -> None:
@@ -581,19 +502,13 @@ class MegaOutPort(MegaPushEntity):
self.task.cancel() self.task.cancel()
def safe_int(v, def_on=1, def_off=0, def_val=None):
if v == "ON": def safe_int(v):
return def_on if v == 'ON':
elif v == "OFF": return 1
return def_off elif v == 'OFF':
return 0
try: try:
return int(v) return int(v)
except (ValueError, TypeError): except (ValueError, TypeError):
return def_val
def safe_float(v):
try:
return float(v)
except:
return None return None

View File

@@ -10,28 +10,28 @@ from aiohttp.web_response import Response
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import EVENT_BINARY_SENSOR, CONF_RESPONSE_TEMPLATE from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from .tools import make_ints from .tools import make_ints
from . import hub as h from . import hub as h
_LOGGER = logging.getLogger(__name__).getChild('http')
_LOGGER = logging.getLogger(__name__).getChild("http")
def is_ext(data: typing.Dict[str, typing.Any]): def is_ext(data: typing.Dict[str, typing.Any]):
for x in data: for x in data:
if x.startswith("ext"): if x.startswith('ext'):
return True return True
class MegaView(HomeAssistantView): class MegaView(HomeAssistantView):
url = "/mega"
name = "mega" url = '/mega'
name = 'mega'
requires_auth = False requires_auth = False
def __init__(self, cfg: dict): def __init__(self, cfg: dict):
self._try = 0 self._try = 0
self.protected = True self.protected = True
self.allowed_hosts = {"::1", "127.0.0.1"} self.allowed_hosts = {'::1', '127.0.0.1'}
self.notified_attempts = defaultdict(lambda : False) self.notified_attempts = defaultdict(lambda : False)
self.callbacks = defaultdict(lambda: defaultdict(list)) self.callbacks = defaultdict(lambda: defaultdict(list))
self.templates: typing.Dict[str, typing.Dict[str, Template]] = { self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
@@ -39,16 +39,14 @@ class MegaView(HomeAssistantView):
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE] pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
for pt in cfg[mid] for pt in cfg[mid]
if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt] if isinstance(pt, int) and CONF_RESPONSE_TEMPLATE in cfg[mid][pt]
} for mid in cfg if isinstance(cfg[mid], dict)
} }
for mid in cfg _LOGGER.debug('templates: %s', self.templates)
if isinstance(cfg[mid], dict)
}
_LOGGER.debug("templates: %s", self.templates)
self.hubs = {} self.hubs = {}
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
_LOGGER.debug("request from %s %s", request.remote, request.headers) _LOGGER.debug('request from %s %s', request.remote, request.headers)
hass: HomeAssistant = request.app["hass"] hass: HomeAssistant = request.app['hass']
if self.protected: if self.protected:
auth = False auth = False
for x in self.allowed_hosts: for x in self.allowed_hosts:
@@ -56,39 +54,33 @@ class MegaView(HomeAssistantView):
auth = True auth = True
break break
if not auth: if not auth:
msg = ( msg = f"Non-authorised request from {request.remote} to `/mega`. "\
f"Non-authorised request from {request.remote} to `/mega`. " f"If you want to accept requests from this host "\
f"If you want to accept requests from this host "
f"please add it to allowed hosts in `mega` UI-configuration" f"please add it to allowed hosts in `mega` UI-configuration"
)
if not self.notified_attempts[request.remote]: if not self.notified_attempts[request.remote]:
await hass.services.async_call( await hass.services.async_call(
"persistent_notification", 'persistent_notification',
"create", 'create',
{ {
"notification_id": request.remote, "notification_id": request.remote,
"title": "Non-authorised request", "title": "Non-authorised request",
"message": msg, "message": msg
}, }
) )
_LOGGER.warning(msg) _LOGGER.warning(msg)
return Response(status=401) return Response(status=401)
remote = request.headers.get("X-Real-IP", request.remote) remote = request.headers.get('X-Real-IP', request.remote)
hub: "h.MegaD" = self.hubs.get(remote) hub: 'h.MegaD' = self.hubs.get(remote)
if hub is None and "mdid" in request.query: if hub is None and 'mdid' in request.query:
hub = self.hubs.get(request.query["mdid"]) hub = self.hubs.get(request.query['mdid'])
if hub is None: if hub is None:
_LOGGER.warning( _LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
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"]:
try: try:
hub = list(self.hubs.values())[0] hub = list(self.hubs.values())[0]
except IndexError: except IndexError:
_LOGGER.warning( _LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}'
)
return Response(status=400) return Response(status=400)
elif hub is None: elif hub is None:
return Response(status=400) return Response(status=400)
@@ -99,17 +91,17 @@ 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": if data.get('st') == '1':
hass.async_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()
update_all = True update_all = True
if "v" in data: if 'v' in data:
update_all = False update_all = False
data["value"] = data.pop("v") data['value'] = data.pop('v')
data["mega_id"] = hub.id data['mega_id'] = hub.id
ret = "d" if hub.force_d else "" ret = 'd' if hub.force_d else ''
if port is not None: if port is not None:
if is_ext(data): if is_ext(data):
# ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ # ret = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
@@ -118,70 +110,48 @@ class MegaView(HomeAssistantView):
else: else:
pt_orig = hub.ext_in.get(port, hub.ext_in.get(str(port))) pt_orig = hub.ext_in.get(port, hub.ext_in.get(str(port)))
if pt_orig is None: if pt_orig is None:
hub.lg.warning( hub.lg.warning(f'can not find extender for int port {port}, '
f"can not find extender for int port {port}, " f'have ext_int: {hub.ext_in}, ext: {hub.extenders}')
f"have ext_int: {hub.ext_in}, ext: {hub.extenders}"
)
return Response(status=200) return Response(status=200)
for e, v in data.items(): for e, v in data.items():
_data = data.copy() _data = data.copy()
if e.startswith("ext"): if e.startswith('ext'):
idx = e[3:] idx = e[3:]
pt = ( pt = f'{pt_orig}e{idx}' if not hub.new_naming else f'{int(pt_orig):02d}e{int(idx):02d}'
f"{pt_orig}e{idx}" _data['pt_orig'] = pt_orig
if not hub.new_naming _data['value'] = 'ON' if v == '1' else 'OFF'
else f"{int(pt_orig):02d}e{int(idx):02d}" _data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
)
_data["pt_orig"] = pt_orig
_data["value"] = "ON" if v == "1" else "OFF"
_data["m"] = (
1 if _data[e] == "0" else 0
) # имитация поведения обычного входа, чтобы события обрабатывались аналогично
hub.values[pt] = _data hub.values[pt] = _data
for cb in self.callbacks[hub.id][pt]: for cb in self.callbacks[hub.id][pt]:
cb(_data) cb(_data)
act = hub.ext_act.get(pt) act = hub.ext_act.get(pt)
hub.lg.debug( hub.lg.debug(f'act on port {pt}: {act}, all acts are: {hub.ext_act}')
f"act on port {pt}: {act}, all acts are: {hub.ext_act}" template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
)
template: Template = self.templates.get(hub.id, {}).get(
port, hub.def_response
)
if template is not None: if template is not None:
template.hass = hass template.hass = hass
ret = template.async_render(_data) ret = template.async_render(_data)
hub.lg.debug(f"response={ret}, template={template}") hub.lg.debug(f'response={ret}, template={template}')
if ret == "d" and act: if ret == 'd' and act:
await hub.request(cmd=act.replace(":3", f":{v}")) await hub.request(cmd=act.replace(':3', f':{v}'))
ret = "d" if hub.force_d else "" ret = 'd' if hub.force_d else ''
else: else:
# elif port in hub.binary_sensors:
hub.values[port] = data hub.values[port] = data
for cb in self.callbacks[hub.id][port]: for cb in self.callbacks[hub.id][port]:
cb(data) cb(data)
template: Template = self.templates.get(hub.id, {}).get( template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response)
port, hub.def_response
)
if template is not None: if template is not None:
template.hass = hass template.hass = hass
ret = template.async_render(data) ret = template.async_render(data)
if hub.update_all and update_all: if hub.update_all and update_all:
asyncio.create_task(self.later_update(hub)) asyncio.create_task(self.later_update(hub))
_LOGGER.debug("response %s", ret) _LOGGER.debug('response %s', ret)
Response(body="" if hub.fake_response else ret, content_type="text/plain") Response(body='' if hub.fake_response else ret, content_type='text/plain')
if ( if hub.fake_response and 'value' not in data and 'pt' in data:
hub.fake_response if 'd' in ret:
and "value" not in data
and "pt" in data
and port in hub.binary_sensors
):
if "d" in ret:
await hub.request(pt=port, cmd=ret) await hub.request(pt=port, cmd=ret)
else: else:
await hub.request(cmd=ret) await hub.request(cmd=ret)
if not isinstance(ret, str):
return ""
return ret return ret
async def later_restore(self, hub): async def later_restore(self, hub):
@@ -198,5 +168,5 @@ class MegaView(HomeAssistantView):
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,43 +9,46 @@ import re
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, LIGHT_LUX, UnitOfTemperature from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .config_parser import parse_config, DS2413, MCP230, PCA9685 from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
from .const import ( from .const import (
TEMP, TEMP, HUM, PRESS,
HUM, LUX, PATT_SPLIT, DOMAIN,
PRESS, CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN,
LUX, REMOVE_CONFIG
DOMAIN,
CONF_HTTP,
EVENT_BINARY_SENSOR,
CONF_CUSTOM,
CONF_FORCE_D,
CONF_DEF_RESPONSE,
PATT_FW,
REMOVE_CONFIG,
) )
from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int, safe_float 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
TEMP_PATT = re.compile(r"temp:([01234567890\.]+)") TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r"hum:([01234567890\.]+)") HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PRESS_PATT = re.compile(r"press:([01234567890\.]+)") PRESS_PATT = re.compile(r'press:([01234567890\.]+)')
LUX_PATT = re.compile(r"lux:([01234567890\.]+)") LUX_PATT = re.compile(r'lux:([01234567890\.]+)')
PATTERNS = {TEMP: TEMP_PATT, HUM: HUM_PATT, PRESS: PRESS_PATT, LUX: LUX_PATT} PATTERNS = {
UNITS = {TEMP: UnitOfTemperature.CELSIUS, HUM: PERCENTAGE, PRESS: "mmHg", LUX: LIGHT_LUX} TEMP: TEMP_PATT,
HUM: HUM_PATT,
PRESS: PRESS_PATT,
LUX: LUX_PATT
}
UNITS = {
TEMP: TEMP_CELSIUS,
HUM: PERCENTAGE,
PRESS: 'mmHg',
LUX: LIGHT_LUX
}
CLASSES = { CLASSES = {
TEMP: SensorDeviceClass.TEMPERATURE, TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: SensorDeviceClass.HUMIDITY, HUM: DEVICE_CLASS_HUMIDITY,
PRESS: SensorDeviceClass.PRESSURE, PRESS: DEVICE_CLASS_PRESSURE,
LUX: SensorDeviceClass.ILLUMINANCE, LUX: DEVICE_CLASS_ILLUMINANCE
} }
I2C_DEVICE_TYPES = { I2C_DEVICE_TYPES = {
"2": LUX, # BH1750 "2": LUX, # BH1750
@@ -90,14 +93,14 @@ class MegaD:
"""Initialize.""" """Initialize."""
self.skip_ports = set() self.skip_ports = set()
if config is not None: if config is not None:
lg.debug(f"load config: %s", config.data) lg.debug(f'load config: %s', config.data)
self.config = config 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}
self.http.hubs[host] = self self.http.hubs[host] = self
if len(self.http.hubs) == 1: if len(self.http.hubs) == 1:
self.http.hubs["__def"] = self self.http.hubs['__def'] = self
if mqtt_id: if mqtt_id:
self.http.hubs[mqtt_id] = self self.http.hubs[mqtt_id] = self
self.smooth = smooth or [] self.smooth = smooth or []
@@ -131,9 +134,7 @@ class MegaD:
self.sensors = [] self.sensors = []
self.port_to_scan = port_to_scan self.port_to_scan = port_to_scan
self.last_update = datetime.now() self.last_update = datetime.now()
self._callbacks: typing.DefaultDict[ self._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list)
int, typing.List[typing.Callable[[dict], typing.Coroutine]]
] = defaultdict(list)
self._loop = loop self._loop = loop
self._customize = None self._customize = None
self.values = {} self.values = {}
@@ -143,12 +144,10 @@ class MegaD:
self.lg, self.lg,
name="megad", name="megad",
update_method=self.poll, update_method=self.poll,
update_interval=timedelta(seconds=self.poll_interval) update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
if self.poll_interval
else None,
) )
self.updaters = [] self.updaters = []
self.fw = "" self.fw = ''
self.notifiers = defaultdict(asyncio.Condition) self.notifiers = defaultdict(asyncio.Condition)
if not mqtt_id: if not mqtt_id:
_id = host.split(".")[-1] _id = host.split(".")[-1]
@@ -160,15 +159,11 @@ class MegaD:
self.customize[CONF_FORCE_D] = force_d self.customize[CONF_FORCE_D] = force_d
try: try:
if allow_hosts is not None and DOMAIN in hass.data: 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')
self.binary_sensors = []
self.sht31inited = (
set()
) # список портов sht31 которые уже успешно проинициализированы были
async def start(self): async def start(self):
pass pass
@@ -187,7 +182,7 @@ class MegaD:
self.lg.debug(self.sensors) self.lg.debug(self.sensors)
ports = [] ports = []
for x in self.sensors: for x in self.sensors:
if only_list and x.http_cmd != "list": if only_list and x.http_cmd != 'list':
continue continue
if x.port in ports: if x.port in ports:
continue continue
@@ -215,24 +210,22 @@ class MegaD:
@property @property
def is_online(self): def is_online(self):
return (datetime.now() - self.last_update).total_seconds() < ( return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
self.poll_interval + 10
)
def _warn_offline(self): def _warn_offline(self):
if self.online: if self.online:
self.lg.warning("mega is offline") self.lg.warning('mega is offline')
self.hass.states.async_set( self.hass.states.async_set(
f"mega.{self.id}", f'mega.{self.id}',
"offline", 'offline',
) )
self.online = False self.online = False
def _notify_online(self): def _notify_online(self):
if not self.online: if not self.online:
self.hass.states.async_set( self.hass.states.async_set(
f"mega.{self.id}", f'mega.{self.id}',
"online", 'online',
) )
self.online = True self.online = True
@@ -242,10 +235,13 @@ class MegaD:
:return: :return:
""" """
for x in self.ds2413_ports: for x in self.ds2413_ports:
self.lg.debug(f"poll ds2413 for %s", x) self.lg.debug(f'poll ds2413 for %s', x)
try: try:
await self.get_port( await self.get_port(
port=x, force_http=True, http_cmd="list", conv=False port=x,
force_http=True,
http_cmd='list',
conv=False
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
continue continue
@@ -254,7 +250,7 @@ class MegaD:
""" """
Polling ports Polling ports
""" """
self.lg.debug("poll") self.lg.debug('poll')
if self._update_time: if self._update_time:
await self.update_time() await self.update_time()
for x in self.i2c_sensors: for x in self.i2c_sensors:
@@ -267,7 +263,7 @@ class MegaD:
for x in self.extenders: for x in self.extenders:
ret = await self._update_extender(x) ret = await self._update_extender(x)
if not isinstance(ret, dict): if not isinstance(ret, dict):
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)
@@ -278,14 +274,14 @@ class MegaD:
async def get_mqtt_id(self): async def get_mqtt_id(self):
async with aiohttp.request( async with aiohttp.request(
"get", f"http://{self.host}/{self.sec}/?cf=2" 'get', f'http://{self.host}/{self.sec}/?cf=2'
) as req: ) as req:
data = await req.text(encoding="iso-8859-5") data = await req.text()
data = BeautifulSoup(data, features="lxml") data = BeautifulSoup(data, features="lxml")
_id = data.find(attrs={"name": "mdid"}) _id = data.find(attrs={'name': 'mdid'})
if _id: if _id:
_id = _id["value"] _id = _id['value']
return _id or "megad/" + self.host.split(".")[-1] return _id or 'megad/' + self.host.split('.')[-1]
async def get_fw(self): async def get_fw(self):
data = await self.request() data = await self.request()
@@ -295,90 +291,84 @@ class MegaD:
return await self.request(pt=port, cmd=cmd) return await self.request(pt=port, cmd=cmd)
async def request(self, priority=0, **kwargs): async def request(self, priority=0, **kwargs):
cmd = "&".join([f"{k}={v}" for k, v in kwargs.items() if v is not None]) cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
url = f"http://{self.host}/{self.sec}" url = f"http://{self.host}/{self.sec}"
if cmd: if cmd:
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): for _ntry in range(3):
try: try:
async with aiohttp.request( async with aiohttp.request("get", url=url, timeout=aiohttp.ClientTimeout(total=5)) as req:
"get", url=url, timeout=aiohttp.ClientTimeout(total=5)
) as req:
if req.status != 200: if req.status != 200:
self.lg.warning( self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
"%s returned %s (%s)",
url,
req.status,
await req.text(encoding="iso-8859-5"),
)
return None return None
else: else:
ret = await req.text(encoding="iso-8859-5") ret = await req.text()
self.lg.debug("response %s", ret) self.lg.debug('response %s', ret)
return ret return ret
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.lg.warning(f"timeout while requesting {url}") self.lg.warning(f'timeout while requesting {url}')
# raise raise
await asyncio.sleep(1) # await asyncio.sleep(1)
raise asyncio.TimeoutError("after 3 tries") 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')
def parse_response(self, ret, cmd="get"): def parse_response(self, ret, cmd='get'):
if ret is None: if ret is None:
raise NoPort() raise NoPort()
if "busy" in ret: if 'busy' in ret:
return None return None
if ":" in ret: if ':' in ret:
if ";" in ret: if ';' in ret:
ret = ret.split(";") ret = ret.split(';')
elif "/" in ret and not cmd == "list": elif '/' in ret and not cmd == 'list':
ret = ret.split("/") ret = ret.split('/')
else: else:
ret = [ret] ret = [ret]
ret = {"value": dict([x.split(":") for x in ret if x.count(":") == 1])} ret = {'value': dict([
elif "ON" in ret: x.split(':') for x in ret if x.count(':') == 1
ret = {"value": "ON"} ])}
elif "OFF" in ret: elif 'ON' in ret:
ret = {"value": "OFF"} ret = {'value': 'ON'}
elif 'OFF' in ret:
ret = {'value': 'OFF'}
else: else:
ret = {"value": ret} ret = {'value': ret}
return ret return ret
async def get_port(self, port, force_http=False, http_cmd="get", conv=True): async def get_port(self, port, force_http=False, http_cmd='get', conv=True):
""" """
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values хранилище values
""" """
self.lg.debug(f"get port %s", port) self.lg.debug(f'get port %s', port)
if http_cmd == "list" and conv: if http_cmd == 'list' and conv:
await self.request(pt=port, cmd="conv") await self.request(pt=port, cmd='conv')
await asyncio.sleep(1) await asyncio.sleep(1)
ret = self.parse_response( ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
await self.request(pt=port, cmd=http_cmd), cmd=http_cmd
)
ntry = 0 ntry = 0
while http_cmd == "list" and ret is None and ntry < 3: while http_cmd == 'list' and ret is None and ntry < 3:
await asyncio.sleep(1) await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ntry += 1 ntry += 1
self.lg.debug("parsed: %s", ret) self.lg.debug('parsed: %s', ret)
self.values[port] = ret self.values[port] = ret
return ret return ret
@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):
try: try:
ret = await self.request(cmd="all") ret = await self.request(cmd='all')
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
for port, x in enumerate(ret.split(";")): for port, x in enumerate(ret.split(';')):
if port in self.ds2413_ports: if port in self.ds2413_ports:
continue continue
if check_skip and not port in self.ports: if check_skip and not port in self.ports:
@@ -396,20 +386,22 @@ class MegaD:
def _process_msg(self, msg): def _process_msg(self, msg):
try: try:
d = msg.topic.split("/") d = msg.topic.split('/')
port = d[-1] port = d[-1]
except ValueError: except ValueError:
self.lg.warning("can not process %s", msg) self.lg.warning('can not process %s', msg)
return return
if port == "cmd": if port == 'cmd':
return return
try: try:
port = int_ignore(port) port = int_ignore(port)
except: except:
self.lg.warning("can not process %s", msg) self.lg.warning('can not process %s', msg)
return return
self.lg.debug("process incomming message: %s", msg) self.lg.debug(
'process incomming message: %s', msg
)
value = None value = None
try: try:
value = json.loads(msg.payload) value = json.loads(msg.payload)
@@ -420,13 +412,13 @@ class MegaD:
cb(value) cb(value)
if isinstance(value, dict): if isinstance(value, dict):
value = value.copy() value = value.copy()
value["mega_id"] = self.id value['mega_id'] = self.id
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_BINARY_SENSOR, EVENT_BINARY_SENSOR,
value, value,
) )
except Exception as exc: except Exception as exc:
self.lg.warning(f"could not parse json ({msg.payload}): {exc}") self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
return return
finally: finally:
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop) asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
@@ -434,15 +426,14 @@ class MegaD:
def subscribe(self, port, callback): def subscribe(self, port, callback):
port = int_ignore(port) port = int_ignore(port)
self.lg.debug( self.lg.debug(
f"subscribe %s", f'subscribe %s %s', port, callback
port,
) )
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:
"""Test if we can authenticate with the host.""" """Test if we can authenticate with the host."""
async with aiohttp.request("get", url=f"http://{self.host}/{self.sec}") as req: async with aiohttp.request("get", url=f"http://{self.host}/{self.sec}") as req:
if "Unauthorized" in await req.text(encoding="iso-8859-5"): if "Unauthorized" in await req.text():
return False return False
else: else:
if req.status != 200: if req.status != 200:
@@ -450,10 +441,10 @@ class MegaD:
return True return True
async def get_port_page(self, port): async def get_port_page(self, port):
url = f"http://{self.host}/{self.sec}/?pt={port}" url = f'http://{self.host}/{self.sec}/?pt={port}'
self.lg.debug(f"get page for port {port} {url}") self.lg.debug(f'get page for port {port} {url}')
async with aiohttp.request("get", url) as req: async with aiohttp.request('get', url) as req:
return await req.text(encoding="iso-8859-5") return await req.text()
async def scan_port(self, port): async def scan_port(self, port):
data = await self.request(pt=port) data = await self.request(pt=port)
@@ -473,12 +464,12 @@ class MegaD:
:return: :return:
""" """
try: try:
values = await self.request(pt=port, cmd="get") values = await self.request(pt=port, cmd='get')
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
ret = {} ret = {}
for i, x in enumerate(values.split(";")): for i, x in enumerate(values.split(';')):
ret[f"{port}e{i}" if not self.new_naming else f"{port:02d}e{i:02d}"] = x ret[f'{port}e{i}'] = x
return ret return ret
async def _update_i2c(self, params): async def _update_i2c(self, params):
@@ -487,188 +478,127 @@ class MegaD:
:param params: параметры url :param params: параметры url
:return: :return:
""" """
params = params.copy() pt = params.get('pt')
pt = params.get("pt")
i2c_dev = params.get("i2c_dev", None)
if pt in self.skip_ports: if pt in self.skip_ports:
return return
if pt is None: if pt is not None:
return pass
_params = tuple(params.items()) _params = tuple(params.items())
if i2c_dev is not None and i2c_dev == "sht31" and pt not in self.sht31inited:
__params = params.copy()
__params["i2c_par"] = 9
# инициализация сенсора
await self.request(**__params)
await asyncio.sleep(0.1)
self.sht31inited.add(pt)
delay = None delay = None
idx: int = params.pop("idx", None) if 'delay' in params:
pt: int = params.get("pt", None) delay = params.pop('delay')
if "delay" in params:
delay = params.pop("delay")
try: try:
if idx is None or idx == 0: ret = {
v: str = await self.request(**params) _params: await self.request(**params)
# scd4x фактически отдает сразу 3 датчика на одном запросе, не ложится }
# в общую архитектуру, поэтому используется такой костыль с кешем except asyncio.TimeoutError:
self.values[f"chache_{pt}"] = v
elif idx is not None and idx > 0:
v: str = self.values.get(f"chache_{pt}")
if idx is not None:
vv = v.split("/")
if len(vv) == 3:
v = vv[idx]
else:
v: None
ret = {_params: safe_float(v)}
except Exception:
self.lg.exception(f"while getting i2c {params=}")
return return
self.lg.debug("i2c response: %s", ret) self.lg.debug('i2c response: %s', ret)
if delay: if delay:
self.lg.debug("delay %s", delay) self.lg.debug('delay %s', delay)
await asyncio.sleep(delay) await asyncio.sleep(delay)
return ret return ret
async def get_config(self, nports=37): async def get_config(self, nports=37):
ret = defaultdict(lambda: defaultdict(list)) ret = defaultdict(lambda: defaultdict(list))
ret["mqtt_id"] = await self.get_mqtt_id() ret['mqtt_id'] = await self.get_mqtt_id()
ret["extenders"] = extenders = [] ret['extenders'] = extenders = []
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 = [] 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):
_cust = {} _cust = {}
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):
if cfg.misc is not None: if cfg.misc is not None:
smooth.append(port) smooth.append(port)
ret["light"][port].append( ret['light'][port].append({'dimmer': cfg.m == '1', 'smooth': safe_int(cfg.misc)})
{"dimmer": cfg.m == "1", "smooth": safe_int(cfg.misc)}
)
elif cfg == DS2413: elif cfg == DS2413:
# ds2413 # ds2413
_data = await self.get_port( _data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
port=port, force_http=True, http_cmd="list", conv=False data = _data.get('value', {})
)
data = _data.get("value", {})
if not isinstance(data, dict): if not isinstance(data, dict):
self.lg.warning( self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}')
f"can not add ds2413 on port {port}, it has wrong data: {_data}"
)
continue continue
for addr, state in data.items(): for addr, state in data.items():
ret["light"][port].extend( ret['light'][port].extend([
[ {"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'},
{ {"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'},
"index": 0, ])
"addr": addr,
"id_suffix": f"{addr}_a",
"http_cmd": "ds2413",
},
{
"index": 1,
"addr": addr,
"id_suffix": f"{addr}_b",
"http_cmd": "ds2413",
},
]
)
elif cfg == MCP230: elif cfg == MCP230:
extenders.append(port) extenders.append(port)
if cfg.inta: if cfg.inta:
ext_int[int_ignore(cfg.inta)] = port ext_int[int_ignore(cfg.inta)] = port
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)):
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:02d}e{n:02d}" pt = f'{port}e{n}' if not self.new_naming else f'{port:02d}e{n:02d}'
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':
if ext_cfg.eact: if ext_cfg.eact:
ext_acts[pt] = ext_cfg.eact ext_acts[pt] = ext_cfg.eact
ret["binary_sensor"][pt].append({}) ret['binary_sensor'][pt].append({})
elif cfg == PCA9685: elif cfg == PCA9685:
extenders.append(port) extenders.append(port)
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}" pt = f'{port}e{n}'
name = pt if not self.new_naming else f"{port:02}e{n:02}" name = pt if not self.new_naming else f'{port:02}e{n:02}'
ret["light"][pt].append( 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))
"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)
if scan is not None: if scan is not None:
page = await self.request(pt=port, cmd="scan") page = await self.request(pt=port, cmd='scan')
req, parsed = parse_scan_page(page) req, parsed = parse_scan_page(page)
self.lg.debug(f"scan results: %s", (req, parsed)) self.lg.debug(f'scan results: %s', (req, parsed))
ret["i2c"][port].extend(parsed) ret['i2c'][port].extend(parsed)
i2c_sensors.extend(req) i2c_sensors.extend(req)
elif cfg.pty == "4" and cfg.m == "2": elif cfg.pty == '4' and cfg.m == '2':
# scl исключаем из сканирования # scl исключаем из сканирования
continue continue
elif cfg.pty is None and nports < 30: elif cfg.pty in ('3', '2', '4'):
# вроде как это ADC на 328 меге http_cmd = 'get'
ret["sensor"][port].append(dict()) if cfg.d == '5' and cfg.pty == '3':
elif cfg.pty in ("3", "2", "4"):
http_cmd = "get"
if cfg.d == "5" and cfg.pty == "3":
# 1-wire bus # 1-wire bus
values = await self.get_port(port, force_http=True, http_cmd="list") values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = "list" http_cmd = 'list'
else: else:
values = await self.get_port(port, force_http=True) values = await self.get_port(port, force_http=True)
if values is None or ( if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
isinstance(values, dict) values = await self.get_port(port, force_http=True, http_cmd='list')
and str(values.get("value")) in ("", "None") http_cmd = 'list'
): self.lg.debug(f'values: %s', values)
values = await self.get_port(
port, force_http=True, http_cmd="list"
)
http_cmd = "list"
self.lg.debug(f"values: %s", values)
if values is None: if values is None:
self.lg.warning( self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
f"port {port} is of type sensor but response is None, skipping it"
)
continue continue
if isinstance(values, dict) and "value" in values: if isinstance(values, dict) and 'value' in values:
values = values["value"] values = values['value']
if isinstance(values, str) and TEMP_PATT.search(values): if isinstance(values, str) and TEMP_PATT.search(values):
values = {TEMP: values} values = {TEMP: values}
elif not isinstance(values, dict): elif not isinstance(values, dict):
if cfg.pty == "4" and cfg.d in I2C_DEVICE_TYPES: if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
values = {I2C_DEVICE_TYPES.get(cfg.m): values} values = {I2C_DEVICE_TYPES.get(cfg.m): values}
else: else:
values = {None: values} values = {None: values}
for key in values: for key in values:
self.lg.debug(f"add sensor {key}") self.lg.debug(f'add sensor {key}')
ret["sensor"][port].append( ret['sensor'][port].append(dict(
dict(
key=key, key=key,
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
device_class=CLASSES.get(key, CLASSES[TEMP]), device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key, id_suffix=key,
http_cmd=http_cmd, http_cmd=http_cmd,
) ))
)
return ret return ret
async def restore_states(self): async def restore_states(self):
@@ -680,7 +610,10 @@ class MegaD:
await x.async_turn_off() await x.async_turn_off()
async def update_time(self): async def update_time(self):
await self.request(cf=7, stime=datetime.now().strftime("%H:%M:%S")) await self.request(
cf=7,
stime=datetime.now().strftime('%H:%M:%S')
)
async def reload(self, reload_entry=True): async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports) new = await self.get_config(nports=self.nports)
@@ -688,14 +621,14 @@ class MegaD:
for x in REMOVE_CONFIG: for x in REMOVE_CONFIG:
cfg.pop(x, None) cfg.pop(x, None)
cfg.update(new) cfg.update(new)
self.lg.debug(f"new config: %s", cfg) self.lg.debug(f'new config: %s', cfg)
self.hass.config_entries.async_update_entry(self.config, data=cfg) self.config.data = cfg
if reload_entry: if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id) await self.hass.config_entries.async_reload(self.config.entry_id)
return cfg return cfg
def _wrap_port_smooth(self, from_, to_, time): def _wrap_port_smooth(self, from_, to_, time):
self.lg.debug("dim from %s to %s for %s seconds", from_, to_, time) self.lg.debug('dim from %s to %s for %s seconds', from_, to_, time)
if time <= 0: if time <= 0:
return return
beg = datetime.now() beg = datetime.now()
@@ -760,9 +693,7 @@ class MegaD:
continue continue
if not ws: if not ws:
cmd = dict( cmd = dict(
cmd=";".join( cmd=';'.join([f'{pt}:{_next_val[i]}' for i, (pt, _, _) in enumerate(config)])
[f"{pt}:{_next_val[i]}" for i, (pt, _, _) in enumerate(config)]
)
) )
await self.request(**cmd) await self.request(**cmd)
else: else:
@@ -770,9 +701,7 @@ class MegaD:
cmd = dict( cmd = dict(
pt=config[0][0], pt=config[0][0],
chip=chip, chip=chip,
ws="".join( ws=''.join([hex(x).split('x')[1].rjust(2, '0').upper() for x in _next_val])
[hex(x).split("x")[1].rjust(2, "0").upper() for x in _next_val]
),
) )
await self.request(**cmd) await self.request(**cmd)

View File

@@ -1,91 +1,70 @@
import typing from dataclasses import dataclass, field
from dataclasses import dataclass, astuple
from urllib.parse import parse_qsl, urlparse from urllib.parse import parse_qsl, urlparse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_CO2,
PERCENTAGE, PERCENTAGE,
LIGHT_LUX, LIGHT_LUX,
TEMP_CELSIUS,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
UnitOfTemperature, PRESSURE_BAR,
UnitOfPressure
) )
from collections import namedtuple from collections import namedtuple
DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
# DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
@dataclass
class DeviceType:
device_class: typing.Optional[str] = None
unit_of_measurement: typing.Optional[str] = None
suffix: typing.Optional[str] = None
delay: typing.Optional[float] = None
i2c_par: typing.Optional[int] = None
idx: typing.Optional[
int
] = None # на случай если все значения представлены одной строчкой (как с scd4x)
def parse_scan_page(page: str): def parse_scan_page(page: str):
ret = [] ret = []
req = [] req = []
page = BeautifulSoup(page, features="lxml") page = BeautifulSoup(page, features="lxml")
for x in page.find_all("a"): for x in page.find_all('a'):
params = x.get("href") params = x.get('href')
if params is None: if params is None:
continue continue
params = dict(parse_qsl(urlparse(params).query)) params = dict(parse_qsl(urlparse(params).query))
dev = params.get("i2c_dev") dev = params.get('i2c_dev')
if dev is None: if dev is None:
continue continue
classes = i2c_classes.get(dev, []) classes = i2c_classes.get(dev, [])
i2c_par, idx = (None, None)
for i, c in enumerate(classes): for i, c in enumerate(classes):
_params = params.copy()
if c is Skip: if c is Skip:
continue continue
elif c is Request: elif c is Request:
req.append(_params) req.append(params)
continue continue
elif isinstance(c, Request): elif isinstance(c, Request):
if c.delay: if c.delay:
_params["delay"] = c.delay params = params.copy()
req.append(_params) params['delay'] = c.delay
req.append(params)
continue continue
elif isinstance(c, DeviceType): elif isinstance(c, DeviceType):
c, m, suffix, delay, i2c_par, idx = astuple(c) c, m, suffix = c
if delay is not None:
_params["delay"] = delay
else: else:
continue continue
suffix = suffix or c 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:
_dev = f"{dev}_{suffix}" _dev = f'{dev}_{suffix}'
else: else:
_dev = dev _dev = dev
if i > 0 and i2c_par is None: params = params.copy()
_params["i2c_par"] = i if i > 0:
# i2c_par может быть явно указан в DeviceType params['i2c_par'] = i
elif i2c_par is not None and i2c_par > 0:
_params["i2c_par"] = i2c_par
# idx - тема фактически только для scd4x, означает номер внутри текстового значения разделенного знаком "/"
if idx is not None:
_params["idx"] = idx
ret.append( ret.append({
{ 'id_suffix': _dev,
"id_suffix": _dev, 'device_class': c,
"device_class": c, 'params': params,
"params": _params, 'unit_of_measurement': m,
"unit_of_measurement": m, })
} req.append(params)
)
req.append(_params)
return req, ret return req, ret
@@ -99,80 +78,67 @@ class Request:
i2c_classes = { i2c_classes = {
"htu21d": [ 'htu21d': [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None), DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
"sht31": [ 'sht31': [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=0.1), DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
"max44009": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)], 'max44009': [
"bh1750": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)], DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
"tsl2591": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
"bmp180": [
DeviceType(SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None),
], ],
"bmx280": [ 'bh1750': [
DeviceType(SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, None), DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
], ],
"dps368": [ 'tsl2591': [
DeviceType(SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, None), DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None)
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None),
], ],
"mlx90614": [ 'bmp180': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'bmx280': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None)
],
'dps368': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
],
'mlx90614': [
Skip, Skip,
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, "temp"), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, "object"), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'),
], ],
"ptsensor": [ 'ptsensor': [
Skip, Skip,
Request(delay=3), # запрос на измерение Request(delay=1), # запрос на измерение
DeviceType(SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, None), DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
], ],
"mcp9600": [ 'mcp9600': [
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None), # термопара DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType( DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None
), # сенсор встроенный в микросхему
], ],
"t67xx": [DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None)], 't67xx': [
"tmp117": [ DeviceType(DEVICE_CLASS_CO2, CONCENTRATION_PARTS_PER_MILLION, None)
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None),
], ],
"ads1115": [ 'tmp117': [
DeviceType(None, None, "ch0"), DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(None, None, "ch1"),
DeviceType(None, None, "ch2"),
DeviceType(None, None, "ch3"),
], ],
"ads1015": [ 'ads1115': [
DeviceType(None, None, "ch0"), DeviceType(None, None, 'ch0'),
DeviceType(None, None, "ch1"), DeviceType(None, None, 'ch1'),
DeviceType(None, None, "ch2"), DeviceType(None, None, 'ch2'),
DeviceType(None, None, "ch3"), DeviceType(None, None, 'ch3'),
], ],
"opt3001": [ 'ads1015': [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None), DeviceType(None, None, 'ch0'),
], DeviceType(None, None, 'ch1'),
"ina226": [ DeviceType(None, None, 'ch2'),
Skip, DeviceType(None, None, 'ch3'),
DeviceType(SensorDeviceClass.CURRENT, "A", None),
DeviceType(SensorDeviceClass.VOLTAGE, "V", None),
],
"scd4x": [
DeviceType(
SensorDeviceClass.CO2,
CONCENTRATION_PARTS_PER_MILLION,
None,
i2c_par=0,
idx=0,
),
DeviceType(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None, i2c_par=0, idx=1),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, i2c_par=0, idx=2),
], ],
} }

View File

@@ -1,6 +1,4 @@
"""Platform for light integration.""" """Platform for light integration."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
@@ -12,9 +10,11 @@ import time
from homeassistant.components.light import ( from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_SCHEMA, PLATFORM_SCHEMA as LIGHT_SCHEMA,
SUPPORT_BRIGHTNESS,
LightEntity, LightEntity,
ColorMode, SUPPORT_TRANSITION,
LightEntityFeature 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 (
@@ -33,15 +33,7 @@ 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,
CONF_LED,
CONF_WS28XX,
CONF_PORTS,
CONF_WHITE_SEP,
CONF_SMOOTH,
CONF_ORDER,
CONF_CHIP,
RGB,
) )
from .tools import int_ignore, map_reorder_rgb from .tools import int_ignore, map_reorder_rgb
@@ -69,17 +61,13 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
lg.warning( lg.warning('mega integration does not support yaml for lights, please use UI configuration')
"mega integration does not support yaml for lights, please use UI configuration"
)
return True return True
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices
):
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, {}).get(mid, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
skip = [] skip = []
@@ -87,29 +75,23 @@ async def async_setup_entry(
for entity_id, conf in customize[CONF_LED].items(): for entity_id, conf in customize[CONF_LED].items():
ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)] ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)]
skip.extend(ports) skip.extend(ports)
devices.append( devices.append(MegaRGBW(
MegaRGBW(
mega=hub, mega=hub,
port=ports, port=ports,
name=entity_id, name=entity_id,
customize=conf, customize=conf,
id_suffix=entity_id, id_suffix=entity_id,
config_entry=config_entry, 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(port, {}) c = customize.get(port, {})
if ( if c.get(CONF_SKIP, False) or port in skip or c.get(CONF_DOMAIN, 'light') != 'light':
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)
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data) light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
if "<" in light.name: if '<' in light.name:
continue continue
devices.append(light) devices.append(light)
@@ -117,36 +99,27 @@ async def async_setup_entry(
class MegaLight(MegaOutPort, LightEntity): class MegaLight(MegaOutPort, LightEntity):
@property @property
def supported_features(self): def supported_features(self):
return LightEntityFeature.TRANSITION if self.dimmer else LightEntityFeature(0) return (
(SUPPORT_BRIGHTNESS if self.dimmer else 0) |
(SUPPORT_TRANSITION if self.dimmer else 0)
)
@property
def supported_color_modes(self):
if self.dimmer:
return {ColorMode.BRIGHTNESS}
else:
return {ColorMode.ONOFF}
@property
def color_mode(self):
if self.dimmer:
return ColorMode.BRIGHTNESS
else:
return ColorMode.ONOFF
class MegaRGBW(LightEntity, BaseMegaEntity): class MegaRGBW(LightEntity, BaseMegaEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_on = None self._is_on = None
self._brightness = None self._brightness = None
self._hs_color = None self._hs_color = None
self._rgb_color: tuple[int, int, int] | None = None
self._white_value = None self._white_value = None
self._task: asyncio.Task = None self._task: asyncio.Task = None
self._restore = None self._restore = None
self.smooth: timedelta = self.customize[CONF_SMOOTH] self.smooth: timedelta = self.customize[CONF_SMOOTH]
self._color_order = self.customize.get(CONF_ORDER, "rgb") self._color_order = self.customize.get(CONF_ORDER, 'rgb')
self._last_called: float = 0 self._last_called: float = 0
self._max_values = None self._max_values = None
@@ -154,7 +127,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
def max_values(self) -> list: def max_values(self) -> list:
if self._max_values is None: if self._max_values is None:
if self.is_ws: if self.is_ws:
self._max_values = [255] * 4 self._max_values = [255] * 3
else: else:
self._max_values = [ self._max_values = [
255 if isinstance(x, int) else 4095 for x in self.port 255 if isinstance(x, int) else 4095 for x in self.port
@@ -169,49 +142,31 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
def is_ws(self): def is_ws(self):
return self.customize.get(CONF_WS28XX) return self.customize.get(CONF_WS28XX)
@property
def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
return {
ColorMode.BRIGHTNESS,
ColorMode.RGB if len(self.port) != 4 else ColorMode.RGBW,
}
@property
def color_mode(self) -> ColorMode | str | None:
if len(self.port) == 4:
return ColorMode.RGBW
else:
return ColorMode.RGB
@property @property
def white_value(self): def white_value(self):
# if self.supported_features & SUPPORT_WHITE_VALUE: if self.supported_features & SUPPORT_WHITE_VALUE:
return float(self.get_attribute("white_value", 0)) return float(self.get_attribute('white_value', 0))
@property
def rgb_color(self) -> tuple[int, int, int] | None:
return self._rgb_color
@property
def rgbw_color(self) -> tuple[int, int, int, int] | None:
if self._white_value is not None and self._rgb_color is not None:
return (*self._rgb_color, self._white_value)
@property @property
def brightness(self): def brightness(self):
return float(self.get_attribute("brightness", 0)) return float(self.get_attribute('brightness', 0))
@property @property
def hs_color(self): def hs_color(self):
return self.get_attribute("hs_color", [0, 0]) return self.get_attribute('hs_color', [0, 0])
@property @property
def is_on(self): def is_on(self):
return self.get_attribute("is_on", False) return self.get_attribute('is_on', False)
@property @property
def supported_features(self): def supported_features(self):
return LightEntityFeature.TRANSITION return (
SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION |
SUPPORT_COLOR |
(SUPPORT_WHITE_VALUE if len(self.port) == 4 else 0)
)
def get_rgbw(self): def get_rgbw(self):
if not self.is_on: if not self.is_on:
@@ -225,7 +180,9 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
if not self.customize.get(CONF_WHITE_SEP): if not self.customize.get(CONF_WHITE_SEP):
white = white * (self.brightness / 255) white = white * (self.brightness / 255)
rgb.append(white / 255) rgb.append(white / 255)
rgb = [round(x * self.max_values[i]) for i, x in enumerate(rgb)] rgb = [
round(x * self.max_values[i]) for i, x in enumerate(rgb)
]
if self.is_ws: if self.is_ws:
# восстанавливаем мэпинг # восстанавливаем мэпинг
rgb = map_reorder_rgb(rgb, RGB, self._color_order) rgb = map_reorder_rgb(rgb, RGB, self._color_order)
@@ -235,7 +192,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
return return
self._last_called = time.time() self._last_called = time.time()
self.lg.debug(f"turn on %s with kwargs %s", self.entity_id, kwargs) self.lg.debug(f'turn on %s with kwargs %s', self.entity_id, kwargs)
if self._restore is not None: if self._restore is not None:
self._restore.update(kwargs) self._restore.update(kwargs)
kwargs = self._restore kwargs = self._restore
@@ -251,9 +208,9 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
return return
self._last_called = time.time() self._last_called = time.time()
self._restore = { self._restore = {
"hs_color": self.hs_color, 'hs_color': self.hs_color,
"brightness": self.brightness, 'brightness': self.brightness,
"white_value": self.white_value, 'white_value': self.white_value,
} }
_before = self.get_rgbw() _before = self.get_rgbw()
self._is_on = False self._is_on = False
@@ -262,16 +219,11 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
self._task = asyncio.create_task(self.set_color(_before, **kwargs)) self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def set_color(self, _before, **kwargs): async def set_color(self, _before, **kwargs):
transition = kwargs.get("transition") transition = kwargs.get('transition')
update_state = transition is not None and transition > 3 update_state = transition is not None and transition > 3
_after = None
for item, value in kwargs.items(): for item, value in kwargs.items():
setattr(self, f"_{item}", value) setattr(self, f'_{item}', value)
if item == "rgb_color": _after = self.get_rgbw()
_after = map_reorder_rgb(value, RGB, self._color_order)
self._hs_color = colorsys.rgb_to_hsv(*value)
_after = _after or self.get_rgbw()
self._rgb_color = map_reorder_rgb(tuple(_after[:3]), self._color_order, RGB)
if transition is None: if transition is None:
transition = self.smooth.total_seconds() transition = self.smooth.total_seconds()
ratio = self.calc_speed_ratio(_before, _after) ratio = self.calc_speed_ratio(_before, _after)
@@ -293,7 +245,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
except asyncio.CancelledError: except asyncio.CancelledError:
return return
except: except:
self.lg.exception("while dimming") self.lg.exception('while dimming')
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
@@ -308,10 +260,10 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
w = None w = None
rgb = rgbw rgb = rgbw
if self.is_ws: if self.is_ws:
rgb = map_reorder_rgb(rgb, self._color_order, RGB) rgb = map_reorder_rgb(
h, s, v = colorsys.rgb_to_hsv( rgb, self._color_order, RGB
*[x / self.max_values[i] for i, x in enumerate(rgb)]
) )
h, s, v = colorsys.rgb_to_hsv(*[x/self.max_values[i] for i, x in enumerate(rgb)])
h *= 360 h *= 360
s *= 100 s *= 100
v *= 255 v *= 255
@@ -345,7 +297,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
return return
data = data.get(x, None) data = data.get(x, None)
if isinstance(data, dict): if isinstance(data, dict):
data = data.get("value") data = data.get('value')
data = safe_int(data) data = safe_int(data)
if data is None: if data is None:
return return

View File

@@ -12,9 +12,8 @@
"homekit": {}, "homekit": {},
"after_dependencies": ["mqtt"], "after_dependencies": ["mqtt"],
"codeowners": [ "codeowners": [
"@andvikt", "@andvikt"
"@den-dmitriev"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues", "issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v1.1.8b14" "version": "v1.1.0b0"
} }

View File

@@ -5,8 +5,8 @@ import struct
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA, PLATFORM_SCHEMA as SENSOR_SCHEMA,
SensorEntity, DEVICE_CLASS_TEMPERATURE,
SensorDeviceClass, DEVICE_CLASS_HUMIDITY
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -14,46 +14,34 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
) )
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 ( from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT, DOMAIN, CONF_CUSTOM, CONF_SKIP
CONF_KEY,
TEMP,
HUM,
W1,
W1BUS,
CONF_CONV_TEMPLATE,
CONF_HEX_TO_FLOAT,
DOMAIN,
CONF_CUSTOM,
CONF_SKIP,
CONF_FILTER_VALUES,
CONF_FILTER_SCALE,
CONF_FILTER_LOW,
CONF_FILTER_HIGH,
CONF_FILL_NA,
)
from .hub import MegaD from .hub import MegaD
import re import re
from .tools import int_ignore from .tools import int_ignore
lg = logging.getLogger(__name__) lg = logging.getLogger(__name__)
TEMP_PATT = re.compile(r"temp:([01234567890\.]+)") TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
HUM_PATT = re.compile(r"hum:([01234567890\.]+)") HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
PATTERNS = { PATTERNS = {
TEMP: TEMP_PATT, TEMP: TEMP_PATT,
HUM: HUM_PATT, HUM: HUM_PATT,
} }
UNITS = {TEMP: "°C", HUM: "%"} UNITS = {
CLASSES = {TEMP: SensorDeviceClass.TEMPERATURE, HUM: SensorDeviceClass.HUMIDITY} TEMP: '°C',
HUM: '%'
}
CLASSES = {
TEMP: DEVICE_CLASS_TEMPERATURE,
HUM: DEVICE_CLASS_HUMIDITY
}
# Validation of the user's configuration # Validation of the user's configuration
_ITEM = { _ITEM = {
vol.Required(CONF_PORT): int, vol.Required(CONF_PORT): int,
@@ -63,18 +51,18 @@ _ITEM = {
W1, W1,
W1BUS, W1BUS,
), ),
vol.Optional(CONF_KEY, default=""): str, vol.Optional(CONF_KEY, default=''): str,
} }
PLATFORM_SCHEMA = SENSOR_SCHEMA.extend( PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
{vol.Optional(str, description="mega id"): [_ITEM]}, {
vol.Optional(str, description="mega id"): [_ITEM]
},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
lg.warning( lg.warning('mega integration does not support yaml for sensors, please use UI configuration')
"mega integration does not support yaml for sensors, please use UI configuration"
)
return True return True
@@ -85,23 +73,19 @@ def _make_entity(config_entry, mid: str, port: int, conf: dict):
mega_id=mid, mega_id=mid,
port=port, port=port,
patt=PATTERNS.get(key), patt=PATTERNS.get(key),
unit_of_measurement=UNITS.get( unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
key, UNITS[TEMP]
), # TODO: make other units, make options in config flow
device_class=CLASSES.get(key, CLASSES[TEMP]), device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key, id_suffix=key,
config_entry=config_entry, config_entry=config_entry
) )
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices
):
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, {}).get(mid, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
for tp in ["sensor", "i2c"]: for tp in ['sensor', 'i2c']:
for port, cfg in config_entry.data.get(tp, {}).items(): for port, cfg in config_entry.data.get(tp, {}).items():
port = int_ignore(port) port = int_ignore(port)
c = customize.get(port, {}) c = customize.get(port, {})
@@ -109,96 +93,29 @@ async def async_setup_entry(
hub.skip_ports |= {port} hub.skip_ports |= {port}
continue continue
for data in cfg: for data in cfg:
hub.lg.debug( hub.lg.debug(f'add sensor on port %s with data %s', port, data)
f"add sensor on port %s with data %s, constructor: %s",
port,
data,
_constructors[tp],
)
sensor = _constructors[tp]( sensor = _constructors[tp](
mega=hub, mega=hub,
port=port, port=port,
config_entry=config_entry, config_entry=config_entry,
**data, **data,
) )
if "<" in sensor.name: if '<' in sensor.name:
continue continue
devices.append(sensor) devices.append(sensor)
async_add_devices(devices) async_add_devices(devices)
class FilterBadValues(MegaPushEntity, SensorEntity): class MegaI2C(MegaPushEntity):
def __init__(self, *args, **kwargs):
self._prev_value = None
super().__init__(*args, **kwargs)
def filter_value(self, value):
if value is None:
return
try:
if (
value in self.filter_values
or (self.filter_low is not None and value < self.filter_low)
or (self.filter_high is not None and value > self.filter_high)
or (
self._prev_value is not None
and self.filter_scale is not None
and abs(value)
> 2 # при переходе через 0 каждое небольшое изменение будет иметь слишком большой эффект
and (
abs((value - self._prev_value) / self._prev_value)
> self.filter_scale
)
)
):
if self.fill_na == "last":
value = self._prev_value
else:
value = None
self._prev_value = value
return value
except Exception as exc:
lg.exception(f"while parsing value")
return None
@property
def filter_values(self):
return self.customize.get(
CONF_FILTER_VALUES, self.mega.customize.get(CONF_FILTER_VALUES, [])
)
@property
def filter_scale(self):
return self.customize.get(
CONF_FILTER_SCALE, self.mega.customize.get(CONF_FILTER_SCALE, None)
)
@property
def filter_low(self):
return self.customize.get(
CONF_FILTER_LOW, self.mega.customize.get(CONF_FILTER_LOW, None)
)
@property
def filter_high(self):
return self.customize.get(
CONF_FILTER_HIGH, self.mega.customize.get(CONF_FILTER_HIGH, None)
)
@property
def fill_na(self):
return self.customize.get(CONF_FILL_NA, "last")
class MegaI2C(FilterBadValues):
def __init__( def __init__(
self, self,
*args, *args,
device_class: str, device_class: str,
params: dict, params: dict,
unit_of_measurement: str = None, unit_of_measurement: str = None,
**kwargs, **kwargs
): ):
self._device_class = device_class self._device_class = device_class
self._params = tuple(params.items()) self._params = tuple(params.items())
@@ -207,65 +124,49 @@ class MegaI2C(FilterBadValues):
@property @property
def customize(self): def customize(self):
ret = super().customize return super().customize.get(self.id_suffix, {}) or {}
_old = ret.get(self.id_suffix)
if _old is not None:
ret = ret.copy()
ret.update(_old)
return ret
@property
def extra_state_attributes(self):
attrs = super().extra_state_attributes or {}
attrs.update(
{
"i2c_id": self.id_suffix,
}
)
return attrs
@property @property
def device_class(self): def device_class(self):
return self._device_class return self._device_class
@property @property
def native_unit_of_measurement(self): def unit_of_measurement(self):
return self._unit_of_measurement return self._unit_of_measurement
@property @property
def native_value(self): def state(self):
try: # self.lg.debug(f'get % all states: %', self._params, self.mega.values)
ret = self.mega.values.get(self._params) ret = self.mega.values.get(self._params)
if self.customize.get(CONF_HEX_TO_FLOAT): if self.customize.get(CONF_HEX_TO_FLOAT):
try: try:
ret = struct.unpack("!f", bytes.fromhex(ret))[0] ret = struct.unpack('!f', bytes.fromhex('41973333'))[0]
except: except:
self.lg.warning(f"could not convert {ret} form hex to float") self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get( tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)
)
try: try:
ret = float(ret) 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})
except: except:
ret = ret ret = ret
ret = self.filter_value(ret)
if ret is not None:
return str(ret) return str(ret)
except Exception:
lg.exception("while getting value")
return None
@property @property
def device_class(self): def device_class(self):
return self._device_class return self._device_class
class Mega1WSensor(FilterBadValues): class Mega1WSensor(MegaPushEntity):
def __init__( def __init__(
self, unit_of_measurement=None, device_class=None, key=None, *args, **kwargs self,
unit_of_measurement,
device_class,
key=None,
*args,
**kwargs
): ):
""" """
1-wire sensor entity 1-wire sensor entity
@@ -274,15 +175,14 @@ class Mega1WSensor(FilterBadValues):
:param patt: pattern to extract value, must have at least one group that will contain parsed value :param patt: pattern to extract value, must have at least one group that will contain parsed value
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.key = key
self._value = None self._value = None
self.key = key
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self) self.mega.sensors.append(self)
self.prev_value = None
@property @property
def native_unit_of_measurement(self): def unit_of_measurement(self):
_u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None) _u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None)
if _u is None: if _u is None:
return self._unit_of_measurement return self._unit_of_measurement
@@ -296,9 +196,9 @@ class Mega1WSensor(FilterBadValues):
@property @property
def unique_id(self): def unique_id(self):
if self.key: if self.key:
return super().unique_id + f"_{self.key}" return super().unique_id + f'_{self.key}'
else: else:
return super().unique_id return super(Mega1WSensor, self).unique_id
@property @property
def device_class(self): def device_class(self):
@@ -313,73 +213,52 @@ class Mega1WSensor(FilterBadValues):
return self._device_class return self._device_class
@property @property
def native_value(self): def state(self):
try:
ret = None ret = None
if not hasattr(self, "key"):
return None
if self.key: if self.key:
try: try:
ret = self.mega.values.get(self.port, {}) ret = self.mega.values.get(self.port, {})
if isinstance(ret, dict): if isinstance(ret, dict):
ret = ret.get("value", {}) ret = ret.get('value', {})
if isinstance(ret, dict): if isinstance(ret, dict):
ret = ret.get(self.key) ret = ret.get(self.key)
except: except:
self.lg.error(self.mega.values.get(self.port, {}).get("value", {})) self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
return return
else: else:
ret = self.mega.values.get(self.port, {}).get("value") ret = self.mega.values.get(self.port, {}).get('value')
if ( if ret is None and self._state is not None:
ret is None
and self.fill_na == "fill_na"
and self.prev_value is not None
):
ret = self.prev_value
elif ret is None and self.fill_na == "fill_na" and self._state is not None:
ret = self._state.state ret = self._state.state
try: try:
ret = float(ret) ret = float(ret)
ret = str(ret) ret = str(ret)
except: except:
self.lg.debug(f'could not convert to float "{ret}"') ret = None
ret = self.prev_value
if self.customize.get(CONF_HEX_TO_FLOAT): if self.customize.get(CONF_HEX_TO_FLOAT):
try: try:
ret = struct.unpack("!f", bytes.fromhex(ret))[0] ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except: except:
self.lg.warning(f"could not convert {ret} form hex to float") self.lg.warning(f'could not convert {ret} form hex to float')
tmpl: Template = self.customize.get( tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)
)
try: try:
ret = float(ret) 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})
except: except:
pass pass
ret = self.filter_value(ret)
self.prev_value = ret
if ret is not None:
return str(ret) return str(ret)
except Exception:
lg.exception("while parsing state")
return None
@property @property
def name(self): def name(self):
n = super().name n = super().name
c = self.customize.get(CONF_NAME, {}) c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict): if isinstance(c, dict):
try:
c = c.get(self.key) c = c.get(self.key)
except:
pass
return c or n return c or n
_constructors = { _constructors = {
"sensor": Mega1WSensor, 'sensor': Mega1WSensor,
"i2c": MegaI2C, 'i2c': MegaI2C,
} }

View File

@@ -50,10 +50,6 @@ class PriorityLock(asyncio.Lock):
finally: finally:
self.release() self.release()
@property
def _loop(self):
return asyncio.get_event_loop()
async def acquire(self, priority=0) -> bool: async def acquire(self, priority=0) -> bool:
"""Acquire a lock. """Acquire a lock.

View File

@@ -57,9 +57,7 @@
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию` нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
## Беты {: #beta } Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
Иногда я буду выпускать бета-версии, в стабильности которых пока не уверен и не готов раскатывать на всех, поэтому в целях тестирвоания
нужно принудительно включать поддержку бет, для этого зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии" там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
## Зависимости {: #deps } ## Зависимости {: #deps }

View File

@@ -1,25 +1,6 @@
С помощью yaml-конфигурации можно кастомизировать ваши устройства. С помощью yaml-конфигурации можно кастомизировать ваши устройства.
## Основное ## Основное
!!! note "Альтернативная адресация"
Начиная с v1.1.0 большинство параметров объектов можно записывать в более простой и понятной форме:
```yaml
mega: # название интеграции
entities:
sensor.some_sensor: #entity_id как в интерфейсе HA
filter_low: 20
filter_high: 40
```
Рекомендуется пользоваться именно этим способом, тк он более логичный и простой.
Некоторые параметры по своей логике (влияют на entity_id) не могут быть записаны таким образом, среди них:
- domain
- skip
- name
Остальные параметры можно записывать используя новый entities
Конфиг записывается стандартным образом в файл `configuration.yaml`, начинаем с Конфиг записывается стандартным образом в файл `configuration.yaml`, начинаем с
указания названия интеграции: указания названия интеграции:
```yaml hl_lines="1" ```yaml hl_lines="1"
@@ -81,11 +62,8 @@ mega:
!!! note "" !!! note ""
- **smooth** (float, 0): программное плавное диммирование. Это поле отвечает за кол-во секунд, за которое яркость - **smooth** (float, 0): программное плавное диммирование. Это поле отвечает за кол-во секунд, за которое яркость
диммера набирает от 0 до 100% диммера набирает от 0 до 100%
- **range** (list[int, int], [0, 255]), *начиная с версии 1.1.0*: границы диммирования в абсолютных единицах 0..255. При диммировании 1% - **limits** (list[int, int], [0, 255]), *начиная с версии 1.1.0*: границы диммирования в абсолютных единицах контроллера. При диммировании 1%
будет равен левой границе, 100% - правой. будет равен левой границе, 100% - правой.
```yaml
range: [20, 200]
```
[Подробнее про плавное диммирование](smooth.md) [Подробнее про плавное диммирование](smooth.md)
### MegaD-16R-XT, MegaD-16PWM ### MegaD-16R-XT, MegaD-16PWM
Порты расширителей MegaD-16R-XT, MegaD-16PWM конфигурируются аналогично обычным реле и диммерам, но адресация порта Порты расширителей MegaD-16R-XT, MegaD-16PWM конфигурируются аналогично обычным реле и диммерам, но адресация порта
@@ -181,14 +159,6 @@ some_led2:
- **value_template** (str): шаблон для конвертации, например `{{(value|float)/100}}` - **value_template** (str): шаблон для конвертации, например `{{(value|float)/100}}`
- **device_class** (str): класс устройства, [список доступных](https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes) - **device_class** (str): класс устройства, [список доступных](https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes)
- **hex_to_float** (bool, false): если ваш датчик возвращает float запакованный в HEX, интеграция его распакует (перед применением темплейта) - **hex_to_float** (bool, false): если ваш датчик возвращает float запакованный в HEX, интеграция его распакует (перед применением темплейта)
- **filter_high** (float, none): верхняя граница значений, выше нее значения будут считаться ошибочными и отбрасываться. [Доступно так же глобальное значение](#filter_high)
- **filter_low** (float, none): нижняя граница значений, ниже нее значения будут считаться ошибочными и отбрасываться. [Доступно так же глобальное значение](#filter_low)
- **filter_values** ([float], none): список значений, которые считаются ошибочными. [Доступно так же глобальное значение](#filter_values)
- **filter_scale** (float, none): значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если
установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться.
[Доступно так же глобальное значение](#filter_scale)
- **fill_na** (str, last): чем заполнять пропуски, по-умолчанию last, что означает последнее значение, можно так же поставить none-тогда будут пропуски (разрывы на графике).
При этом есть так же особенности адресации, так для сенсора на одном порте с одним значением: При этом есть так же особенности адресации, так для сенсора на одном порте с одним значением:
```yaml ```yaml
36: 36:
@@ -213,10 +183,10 @@ some_led2:
# конфиг # конфиг
``` ```
#### i2c {: #i2c} #### i2c {: #i2c}
Для сенсоров i2c нужно так же указать id сенсора, который можно посмотреть в атрибутах объекта на [странице разработчика](https://my.home-assistant.io/redirect/developer_states/). Для сенсоров i2c адресация конфига такая:
```yaml ```yaml
36: 36:
htu21d_humidity: # i2c_id htu21d_humidity: # entity_id сенсора без приставки sensor., видно в интерфейсе HA
# конфиг # конфиг
``` ```
[Подробнее про i2c](i2c.md) [Подробнее про i2c](i2c.md)
@@ -234,6 +204,33 @@ mega:
{% if m in [0, 1] %}d{% endif %} {% if m in [0, 1] %}d{% endif %}
``` ```
### encoders
Настройки енкодеров. Енкодер состоит из нескольких портов, поэтому энкодеры настраиваются в отдельном блоке. Настроенный
енкодер создает в вашем интерфейсе несколько объектов:
- **light.<encoder_id>**: это rgb-свет, отвечает за светодиод енкодера
- **sensor.<encoder_id>**: это текущее значение енкодера [0..255]
Пример:
```yaml
mega:
megaid1:
encoders:
some_encoder_id: # придумывайте любой
int: 6
scl: 30
sync_to: light.some_light
```
!!! note ""
- **int** (integer): номер порта-прерывания. Если не указать, сервер не будет реагировать на изменения.
- **scl** (integer): номер scl-порта, используется для управления подсветкой и синхронизации состояния
- **sync_to** (str): entity_id, с которым синхронизирвоать состояние. Синхронизация работает только в
одну сторону: а именно, когда вы меняете яркость объекта в интерфейсе, соответсвующая настройка "пробрасывается" на
контроллер. Обычно этого достаточно, тк управление самим светом лучше реализовать штатными средствами контроллера,
но в случае, если вы хотите так же управлять яркостью света с помощью енкодера+HA, то нужно использовать автоматизацию,
для вашего удобства есть шаблон, который можно установить по кнопке ниже:
## Параметры интеграции ## Параметры интеграции
### allow_hosts {: #allow_hosts } ### allow_hosts {: #allow_hosts }
Отвечает за список хостов, с которых интеграция "слушает" сообщения. По умолчанию, в этот список Отвечает за список хостов, с которых интеграция "слушает" сообщения. По умолчанию, в этот список
@@ -245,25 +242,3 @@ mega:
allow_hosts: allow_hosts:
- 192.168.1.20 - 192.168.1.20
``` ```
### filter_high {: #filter_high}
Верхняя граница значений датчиков по-умолчанию, выше нее значения будут считаться ошибочными и отбрасываться
### filter_low {: #filter_low}
Нижняя граница значений датчиков по-умолчанию, ниже нее значения будут считаться ошибочными и отбрасываться
### filter_values {: #filter_values }
Список значений, которые считаются ошибочными, настройка по-умолчанию для всех датчиков. Удобно, если у вас много
однотипных датчиков
```yaml
mega:
filter_values: [-82, - 150]
```
### filter_scale {: #filter_scale }
Значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если
установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться.
```yaml
mega:
filter_scale: 1 # 100%
```

View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
git clone https://github.com/den-dmitriev/mega_hacs.git git clone https://github.com/andvikt/mega_hacs.git
mkdir custom_components mkdir custom_components
cp mega_hacs/custom_components/mega custom_components/mega cp mega_hacs/custom_components/mega custom_components/mega
rm -fR mega_hacs rm -fR mega_hacs

View File

@@ -22,7 +22,7 @@ nav:
- Конфигурация: - Конфигурация:
- В интерфейсе: settings.md - В интерфейсе: settings.md
- Настройка обратной связи: http.md - Настройка обратной связи: http.md
- Кастомизация: yaml.md - YAML-конфиг: yaml.md
- i2c: i2c.md - i2c: i2c.md
- Плавные переходы: smooth.md - Плавные переходы: smooth.md
- Автоматизация: - Автоматизация: