Compare commits

...

18 Commits

Author SHA1 Message Date
Викторов Андрей Германович
9ddca7c84f Bump version: 1.1.8b7 → 1.1.8b8 2023-10-15 21:12:13 +03:00
Викторов Андрей Германович
37b29c6369 i2c 2023-10-15 21:12:07 +03:00
Викторов Андрей Германович
802eed20a7 Bump version: 1.1.8b6 → 1.1.8b7 2023-10-15 21:10:51 +03:00
Викторов Андрей Германович
39696b054f - add many i2c sensors 2023-10-15 21:09:46 +03:00
Викторов Андрей Германович
bd98fa216d fix dimmer attr 2023-10-15 19:23:46 +03:00
Викторов Андрей Германович
39c23a62cf - fix http error 2023-10-15 19:19:51 +03:00
Викторов Андрей Германович
adf5cfe2f8 Bump version: 1.1.8b5 → 1.1.8b6 2023-10-15 14:10:59 +03:00
Викторов Андрей Германович
6bb897a26f - fix init of conf_http 2023-10-15 14:01:39 +03:00
Викторов Андрей Германович
4edc0dcb4f - исправлена логика filter_value 2023-10-15 13:56:44 +03:00
Викторов Андрей Германович
89d261bd9c - исправление логики filter_scale 2023-10-15 13:53:11 +03:00
Викторов Андрей Германович
58288de1dd Bump version: 1.1.8b4 → 1.1.8b5 2023-10-15 13:37:31 +03:00
Викторов Андрей Германович
ec5aeab559 fix update extender 2023-10-15 13:37:24 +03:00
Викторов Андрей Германович
d0659b84dc Bump version: 1.1.8b3 → 1.1.8b4 2023-10-14 18:58:17 +03:00
Викторов Андрей Германович
3cd38a57e0 fix updater service 2023-10-14 18:57:47 +03:00
Викторов Андрей Германович
261c628909 Bump version: 1.1.8b2 → 1.1.8b3 2023-10-11 14:01:30 +03:00
Викторов Андрей Германович
2af9cfbeaf fix rgb brightness memory 2023-10-11 14:01:23 +03:00
Викторов Андрей Германович
60fad1f20f Bump version: 1.1.8b1 → 1.1.8b2 2023-10-11 11:26:57 +03:00
Викторов Андрей Германович
5c18e395da fix color order 2023-10-11 11:26:15 +03:00
10 changed files with 696 additions and 441 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.1.8b1 current_version = 1.1.8b8
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

View File

@@ -1,25 +1,57 @@
"""The mega integration.""" """The mega integration."""
import asyncio import asyncio
import logging import logging
import typing
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_DOMAIN, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS, CONF_PORT CONF_DOMAIN,
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 DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \ from .const import (
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ DOMAIN,
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \ CONF_INVERT,
RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE, \ PLATFORMS,
CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH, CONF_FILL_NA, CONF_MEGA_ID, CONF_ADDR, \ CONF_PORTS,
CONF_1WBUS 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
@@ -28,58 +60,53 @@ _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, vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds, },
}, {
{ 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_NAME): str, vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
}, vol.Optional(CONF_NAME): str,
) },
} )
}
CUSTOMIZE_PORT = { CUSTOMIZE_PORT = {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(
vol.Optional(CONF_FILL_NA, default='last'): vol.Any( CONF_SKIP, description="исключить порт из сканирования", default=False
'last', ): bool,
'none' vol.Optional(CONF_FILL_NA, default="last"): vol.Any("last", "none"),
), vol.Optional(CONF_RANGE, description="диапазон диммирования"): [
vol.Optional(CONF_RANGE, description='диапазон диммирования'): [
vol.Range(0, 255), vol.Range(0, 255),
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.Optional(CONF_NAME): vol.Any(str, {vol.Required(str): str}),
vol.Required(str): str vol.Optional(CONF_DOMAIN): vol.Any("light", "switch"),
}), vol.Optional(
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'), CONF_UNIT_OF_MEASUREMENT,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'): description="единицы измерений, либо строка либо мепинг",
vol.Any(str, { ): 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(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,
@@ -96,43 +123,48 @@ 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)), OWBUS = vol.Schema(
vol.Required(CONF_MEGA_ID): vol.Coerce(str), {
vol.Required(CONF_ADDR): [str], 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("entities"): {
vol.Optional(str): vol.Any( vol.Optional(str): vol.Any(CUSTOMIZE_PORT, CUSTOMIZE_DS2413)
CUSTOMIZE_PORT, },
CUSTOMIZE_DS2413 vol.Optional(vol.Any(str, int), description="id меги из веб-интерфейса"): {
)},
vol.Optional(vol.Any(str, int), description='id меги из веб-интерфейса'): {
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
vol.Optional( vol.Optional(
CONF_DEF_RESPONSE, CONF_FORCE_D,
description='Ответ по умолчанию', description="Принудительно слать d после срабатывания входа",
default=None default=False,
): 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.Any(int, extender), description='номер порта'): vol.Any( vol.Optional(
vol.Any(int, extender), description="номер порта"
): vol.Any(
CUSTOMIZE_PORT, CUSTOMIZE_PORT,
CUSTOMIZE_DS2413, CUSTOMIZE_DS2413,
), ),
@@ -141,14 +173,14 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_FILTER_LOW): vol.Coerce(float), vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float), vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
}, },
vol.Optional(CONF_1WBUS): [OWBUS] 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 = {}
@@ -162,29 +194,40 @@ 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, 'save', partial(_save_service, hass), schema=vol.Schema({ DOMAIN,
vol.Optional('mega_id'): str "save",
}) partial(_save_service, hass),
schema=vol.Schema({vol.Optional("mega_id"): str}),
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({ DOMAIN,
vol.Optional('mega_id'): str, "get_port",
vol.Optional('port'): vol.Any(int, [int]), partial(_get_port, hass),
}) schema=vol.Schema(
{
vol.Optional("mega_id"): str,
vol.Optional("port"): vol.Any(int, [int]),
}
),
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({ DOMAIN,
vol.Optional('port'): int, "run_cmd",
vol.Required('cmd'): str, partial(_run_cmd, hass),
vol.Optional('mega_id'): str, schema=vol.Schema(
}) {
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)
@@ -194,7 +237,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
@@ -213,9 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
await hub.start() await hub.start()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup( hass.config_entries.async_forward_entry_setup(entry, platform)
entry, platform
)
) )
await hub.updater.async_refresh() await hub.updater.async_refresh()
return True return True
@@ -237,11 +278,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 True
_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)
@@ -255,19 +296,24 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
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("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION) _LOGGER.debug(
"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)
config_entry.data = new config_entry.data = new
config_entry.version = ConfigFlow.VERSION config_entry.version = ConfigFlow.VERSION
@@ -277,7 +323,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()
@@ -289,8 +335,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:
@@ -300,6 +346,7 @@ 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):
@@ -311,12 +358,13 @@ 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

@@ -8,9 +8,19 @@ 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 DOMAIN, CONF_RELOAD, \ from .const import (
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \ DOMAIN,
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME CONF_RELOAD,
CONF_NPORTS,
CONF_UPDATE_ALL,
CONF_POLL_OUTS,
CONF_FAKE_RESPONSE,
CONF_FORCE_D,
CONF_ALLOW_HOSTS,
CONF_PROTECTED,
CONF_RESTORE_ON_RESTART,
CONF_UPDATE_TIME,
)
from .hub import MegaD from .hub import MegaD
from . import exceptions from . import exceptions
@@ -18,7 +28,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,
@@ -31,7 +41,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,
}, },
) )
@@ -41,7 +51,7 @@ async def get_hub(hass: HomeAssistant, data):
# _mqtt = hass.data.get(mqtt.DOMAIN) # _mqtt = hass.data.get(mqtt.DOMAIN)
# if not isinstance(_mqtt, mqtt.MQTT): # if not isinstance(_mqtt, mqtt.MQTT):
# raise exceptions.MqttNotConfigured("mqtt must be configured first") # raise exceptions.MqttNotConfigured("mqtt must be configured first")
hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) #mqtt=_mqtt, hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) # mqtt=_mqtt,
hub.mqtt_id = await hub.get_mqtt_id() hub.mqtt_id = await hub.get_mqtt_id()
if not await hub.authenticate(): if not await hub.authenticate():
raise exceptions.InvalidAuth raise exceptions.InvalidAuth
@@ -54,7 +64,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
@@ -63,7 +73,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 = 26 VERSION = 27
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):
@@ -78,12 +88,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
hub = await validate_input(self.hass, user_input) hub = await validate_input(self.hass, user_input)
await hub.start() await hub.start()
hub.new_naming=True 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,
@@ -109,48 +119,66 @@ 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.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(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool, vol.Optional(
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)
# vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool, ): int,
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int, vol.Optional(
vol.Optional(CONF_RELOAD, default=False): bool, CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool, ): bool,
vol.Optional(CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)): bool, # vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
vol.Optional(CONF_FORCE_D, default=e.get(CONF_FORCE_D, False)): bool, # vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
vol.Optional(CONF_RESTORE_ON_RESTART, default=e.get(CONF_RESTORE_ON_RESTART, False)): bool, vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
vol.Optional(CONF_PROTECTED, default=e.get(CONF_PROTECTED, True)): bool, vol.Optional(CONF_RELOAD, default=False): 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_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)
# vol.Optional(CONF_INVERT, default=''): str, ): bool,
}), vol.Optional(
CONF_FAKE_RESPONSE, default=e.get(CONF_FAKE_RESPONSE, True)
): bool,
vol.Optional(
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,
}
),
) )
return ret return ret

View File

@@ -12,8 +12,21 @@ 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 DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \ from .const import (
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY, CONF_SMOOTH, CONF_RANGE DOMAIN,
CONF_CUSTOM,
CONF_INVERT,
EVENT_BINARY_SENSOR,
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__)
@@ -22,7 +35,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
@@ -31,6 +44,7 @@ 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
@@ -40,20 +54,21 @@ 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,
): ):
self._smooth = smooth self._smooth = smooth
self.http_cmd = http_cmd self.http_cmd = http_cmd
@@ -65,11 +80,19 @@ 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 )
self._name = name or f"{mega.id}_{_pt}" + \ _pt = (
(f"_{id_suffix}" if id_suffix else "") port
if not mega.new_naming
else f"{port:02}"
if isinstance(port, int)
else port
)
self._name = name or f"{mega.id}_{_pt}" + (
f"_{id_suffix}" if id_suffix else ""
)
self._customize: dict = None self._customize: dict = None
else: else:
assert id_suffix is not None assert id_suffix is not None
@@ -83,7 +106,7 @@ 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) super().__init__(coordinator=mega.updater)
@@ -92,12 +115,12 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
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
@@ -118,7 +141,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
@@ -130,12 +153,17 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
if self.hass is None or self.entity_id is None: if self.hass is None or self.entity_id 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_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) c.update(c_entity_id)
self._customize = c self._customize = c
@@ -147,18 +175,24 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
if isinstance(self.port, list): if isinstance(self.port, list):
pt_idx = self.id_suffix pt_idx = self.id_suffix
else: else:
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port _pt = (
if isinstance(_pt, str) and 'e' in _pt: self.port
pt_idx, _ = _pt.split('e') if not self.mega.new_naming
else f"{self.port:02}"
if isinstance(self.port, int)
else self.port
)
if isinstance(_pt, str) and "e" in _pt:
pt_idx, _ = _pt.split("e")
else: else:
pt_idx = _pt pt_idx = _pt
return DeviceInfo( return DeviceInfo(
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, name=self.name,
manufacturer='ab-log.ru', manufacturer="ab-log.ru",
sw_version=self.mega.fw, sw_version=self.mega.fw,
via_device=(DOMAIN, self._mega_id), via_device=(DOMAIN, self._mega_id),
) )
@@ -176,7 +210,13 @@ 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 = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port _pt = (
self.port
if not self.mega.new_naming
else f"{self.port:02}"
if isinstance(self.port, int)
else self.port
)
c = self._name or f"{self.mega.id}_p{_pt}" c = self._name or f"{self.mega.id}_p{_pt}"
else: else:
c = self.id_suffix c = self.id_suffix
@@ -192,7 +232,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()
@@ -212,51 +252,42 @@ 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={ event_data={"entity_id": self.entity_id, "type": SINGLE_CLICK},
'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={ event_data={"entity_id": self.entity_id, "type": DOUBLE_CLICK},
'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={ event_data={"entity_id": self.entity_id, "type": LONG},
'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
@@ -266,21 +297,13 @@ class MegaPushEntity(BaseMegaEntity):
class MegaOutPort(MegaPushEntity): class MegaOutPort(MegaPushEntity):
def __init__(self, dimmer=False, dimmer_scale=1, *args, **kwargs):
def __init__( super().__init__(*args, **kwargs)
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
@@ -313,9 +336,14 @@ class MegaOutPort(MegaPushEntity):
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(self._state.attributes.get("brightness"), def_on=self.max_dim, def_off=0, def_val=0) ret = safe_int(
self._state.attributes.get("brightness"),
def_on=self.max_dim,
def_off=0,
def_val=0,
)
ret = self._calc_brightness(ret) ret = self._calc_brightness(ret)
elif isinstance(self.port, str) and 'e' in self.port: 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, def_on=self.max_dim, def_off=0, def_val=0)
else: else:
@@ -340,46 +368,68 @@ class MegaOutPort(MegaPushEntity):
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 self.dimmer: if hasattr(self, "dimmer") and 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 not isinstance(val, str) and self.index is not None and self.addr is not None: if (
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(self.addr, val.get(self.addr.lower(), val.get(self.addr.upper()))) _val = val.get(
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(f'{self.entity_id}: can not get {self.addr} from {val}, recieved {_val}') self.mega.lg.warning(
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('%s parsed values: %s[%s]="%s"', self.entity_id, _val, self.index, _val) self.mega.lg.debug(
'%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 val == 'ON' or str(val) == '1' or (safe_int(val) is not None and safe_int(val) > 0) return (
val == "ON"
or str(val) == "1"
or (safe_int(val) is not None and safe_int(val) > 0)
)
else: else:
return val == 'OFF' or str(val) == '0' or (safe_int(val) is not None and safe_int(val) == 0) return (
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
@@ -400,9 +450,7 @@ 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] = { self.mega.values[self.port] = {"value": value[0]}
'value': value[0]
}
if update_state: if update_state:
self.async_write_ha_state() self.async_write_ha_state()
@@ -412,13 +460,15 @@ 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.mega.smooth_dim( self.task = asyncio.create_task(
(self.cmd_port, from_, to_), self.mega.smooth_dim(
time=tm, (self.cmd_port, from_, to_),
can_smooth_hardware=self.can_smooth_hardware, time=tm,
max_values=[255 if self.dimmer_scale == 1 else 4095], can_smooth_hardware=self.can_smooth_hardware,
updater=partial(self.update_from_smooth, update_state=update_state), max_values=[255 if self.dimmer_scale == 1 else 4095],
)) updater=partial(self.update_from_smooth, update_state=update_state),
)
)
def _calc_brightness(self, brightness): def _calc_brightness(self, brightness):
if brightness is None: if brightness is None:
@@ -454,9 +504,9 @@ class MegaOutPort(MegaPushEntity):
brightness = self._calc_brightness(brightness) 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 self.dimmer and brightness == 0: if hasattr(self, "dimmer") and self.dimmer and brightness == 0:
cmd = self.max_dim cmd = self.max_dim
elif self.dimmer: elif hasattr(self, "dimmer") and 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)
@@ -471,7 +521,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:
@@ -480,29 +530,31 @@ 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(safe_int(self._brightness)) self._restore_brightness = self._cal_reverse_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:
@@ -517,12 +569,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:
@@ -530,13 +582,19 @@ class MegaOutPort(MegaPushEntity):
self.task.cancel() self.task.cancel()
def safe_int(v, def_on=1, def_off=0, def_val=None): def safe_int(v, def_on=1, def_off=0, def_val=None):
if v == 'ON': if v == "ON":
return def_on return def_on
elif v == 'OFF': elif v == "OFF":
return def_off return def_off
try: try:
return int(v) return int(v)
except (ValueError, TypeError): except (ValueError, TypeError):
return def_val return def_val
def safe_float(v):
try:
return float(v)
except:
return None

View File

@@ -13,40 +13,42 @@ from homeassistant.core import HomeAssistant
from .const import EVENT_BINARY_SENSOR, DOMAIN, 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"
url = '/mega' name = "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]] = {
mid: { mid: {
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
if isinstance(cfg[mid], dict)
} }
_LOGGER.debug('templates: %s', self.templates) _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:
@@ -54,33 +56,39 @@ class MegaView(HomeAssistantView):
auth = True auth = True
break break
if not auth: if not auth:
msg = f"Non-authorised request from {request.remote} to `/mega`. "\ msg = (
f"If you want to accept requests from this host "\ f"Non-authorised request from {request.remote} to `/mega`. "
f"please add it to allowed hosts in `mega` UI-configuration" f"If you want to accept requests from this host "
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(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}') _LOGGER.warning(
if hub is None and request.remote in ['::1', '127.0.0.1']: 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"]:
try: try:
hub = list(self.hubs.values())[0] hub = list(self.hubs.values())[0]
except IndexError: except IndexError:
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}') _LOGGER.warning(
f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}'
)
return Response(status=400) return Response(status=400)
elif hub is None: elif hub is None:
return Response(status=400) return Response(status=400)
@@ -91,17 +99,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 = '' # пока ответ всегда пустой, неясно какая будет реакция на непустой ответ
@@ -110,49 +118,70 @@ 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(f'can not find extender for int port {port}, ' hub.lg.warning(
f'have ext_int: {hub.ext_in}, ext: {hub.extenders}') f"can not find extender for int port {port}, "
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 = f'{pt_orig}e{idx}' if not hub.new_naming else f'{int(pt_orig):02d}e{int(idx):02d}' pt = (
_data['pt_orig'] = pt_orig f"{pt_orig}e{idx}"
_data['value'] = 'ON' if v == '1' else 'OFF' if not hub.new_naming
_data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично else f"{int(pt_orig):02d}e{int(idx):02d}"
)
_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(f'act on port {pt}: {act}, all acts are: {hub.ext_act}') hub.lg.debug(
template: Template = self.templates.get(hub.id, {}).get(port, hub.def_response) f"act on port {pt}: {act}, all acts are: {hub.ext_act}"
)
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: # 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(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)
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 hub.fake_response and 'value' not in data and 'pt' in data and port in hub.binary_sensors: if (
if 'd' in ret: hub.fake_response
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):
@@ -169,5 +198,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

@@ -32,7 +32,7 @@ from .const import (
CONF_FORCE_I2C_SCAN, CONF_FORCE_I2C_SCAN,
REMOVE_CONFIG, REMOVE_CONFIG,
) )
from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int, safe_float
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
@@ -164,10 +164,13 @@ class MegaD:
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.binary_sensors = []
self.sht31inited = (
set()
) # список портов sht31 которые уже успешно проинициализированы были
async def start(self): async def start(self):
pass pass
@@ -477,7 +480,7 @@ class MegaD:
return return
ret = {} ret = {}
for i, x in enumerate(values.split(";")): for i, x in enumerate(values.split(";")):
ret[f"{port}e{i}"] = x ret[f"{port}e{i}" if not self.new_naming else f"{port:02d}e{i:02d}"] = x
return ret return ret
async def _update_i2c(self, params): async def _update_i2c(self, params):
@@ -487,16 +490,39 @@ class MegaD:
:return: :return:
""" """
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 not None: if pt is None:
pass return
_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(i2c_dev=i2c_dev, **__params)
await asyncio.sleep(0.1)
self.sht31inited |= pt
delay = None delay = None
idx: int = params.pop("idx", None)
pt: int = params.get("pt", None)
if "delay" in params: if "delay" in params:
delay = params.pop("delay") delay = params.pop("delay")
try: try:
ret = {_params: await self.request(**params)} if idx is None or idx == 0:
v: str = await self.request(**params)
# scd4x фактически отдает сразу 3 датчика на одном запросе, не ложится
# в общую архитектуру, поэтому используется такой костыль с кешем
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:
v = safe_float(v.split("/")[idx])
ret = {_params: v}
except Exception:
self.lg.exception(f"while getting i2c {params=}")
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
self.lg.debug("i2c response: %s", ret) self.lg.debug("i2c response: %s", ret)

View File

@@ -16,27 +16,33 @@ from collections import namedtuple
# DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix') # DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
@dataclass @dataclass
class DeviceType: class DeviceType:
device_class: typing.Optional[str] = None device_class: typing.Optional[str] = None
unit_of_measurement: typing.Optional[str] = None unit_of_measurement: typing.Optional[str] = None
suffix: typing.Optional[str] = None suffix: typing.Optional[str] = None
delay: typing.Optional[float] = 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() _params = params.copy()
if c is Skip: if c is Skip:
@@ -46,31 +52,39 @@ def parse_scan_page(page: str):
continue continue
elif isinstance(c, Request): elif isinstance(c, Request):
if c.delay: if c.delay:
_params['delay'] = c.delay _params["delay"] = c.delay
req.append(_params) req.append(_params)
continue continue
elif isinstance(c, DeviceType): elif isinstance(c, DeviceType):
c, m, suffix, delay = astuple(c) c, m, suffix, delay, i2c_par, idx = astuple(c)
if delay is not None: if delay is not None:
_params['delay'] = delay _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: if i > 0 and i2c_par is None:
_params['i2c_par'] = i _params["i2c_par"] = i
# i2c_par может быть явно указан в DeviceType
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, {
'device_class': c, "id_suffix": _dev,
'params': _params, "device_class": c,
'unit_of_measurement': m, "params": _params,
}) "unit_of_measurement": m,
}
)
req.append(_params) req.append(_params)
return req, ret return req, ret
@@ -85,67 +99,80 @@ class Request:
i2c_classes = { i2c_classes = {
'htu21d': [ "htu21d": [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'sht31': [ "sht31": [
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=1.5), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=0.1),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'max44009': [ "max44009": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None) "bh1750": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
], "tsl2591": [DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)],
'bh1750': [ "bmp180": [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
],
'tsl2591': [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
],
'bmp180': [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'bmx280': [ "bmx280": [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None) DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
], ],
'dps368': [ "dps368": [
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mlx90614': [ "mlx90614": [
Skip, Skip,
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'temp'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "temp"),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'object'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "object"),
], ],
'ptsensor': [ "ptsensor": [
Skip, Skip,
Request(delay=3), # запрос на измерение Request(delay=3), # запрос на измерение
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mcp9600': [ "mcp9600": [
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему DeviceType(
SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None
), # сенсор встроенный в микросхему
], ],
't67xx': [ "t67xx": [DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None)],
DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None) "tmp117": [
],
'tmp117': [
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'ads1115': [ "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"),
], ],
'ads1015': [ "ads1015": [
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": [
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None),
],
"ina226": [
Skip,
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, TEMP_CELSIUS, None, i2c_par=0, idx=1),
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, i2c_par=0, idx=2),
], ],
} }

View File

@@ -263,8 +263,9 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
setattr(self, f"_{item}", value) setattr(self, f"_{item}", value)
if item == "rgb_color": if item == "rgb_color":
_after = map_reorder_rgb(value, RGB, self._color_order) _after = map_reorder_rgb(value, RGB, self._color_order)
self._hs_color = colorsys.rgb_to_hsv(*value)
_after = _after or self.get_rgbw() _after = _after or self.get_rgbw()
self._rgb_color = tuple(_after[:3]) 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)

View File

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

View File

@@ -4,7 +4,9 @@ import voluptuous as vol
import struct import struct
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_SCHEMA, SensorEntity, SensorDeviceClass PLATFORM_SCHEMA as SENSOR_SCHEMA,
SensorEntity,
SensorDeviceClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -12,35 +14,46 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_ID, CONF_ID,
CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_TYPE,
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 CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT, DOMAIN, CONF_CUSTOM, \ from .const import (
CONF_SKIP, CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH, CONF_FILL_NA 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 = { UNITS = {TEMP: "°C", HUM: "%"}
TEMP: '°C', CLASSES = {TEMP: SensorDeviceClass.TEMPERATURE, HUM: SensorDeviceClass.HUMIDITY}
HUM: '%'
}
CLASSES = {
TEMP: SensorDeviceClass.TEMPERATURE,
HUM: SensorDeviceClass.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,
@@ -50,18 +63,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('mega integration does not support yaml for sensors, please use UI configuration') lg.warning(
"mega integration does not support yaml for sensors, please use UI configuration"
)
return True return True
@@ -72,19 +85,23 @@ 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(key, UNITS[TEMP]), # TODO: make other units, make options in config flow unit_of_measurement=UNITS.get(
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(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices): async def async_setup_entry(
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, {})
@@ -92,14 +109,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
hub.skip_ports |= {port} hub.skip_ports |= {port}
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s, constructor: %s', port, data, _constructors[tp]) hub.lg.debug(
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)
@@ -107,64 +129,76 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
class FilterBadValues(MegaPushEntity, SensorEntity): class FilterBadValues(MegaPushEntity, SensorEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._prev_value = None self._prev_value = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def filter_value(self, value): def filter_value(self, value):
if value is None:
return
try: try:
if value \ if (
in self.filter_values \ value in self.filter_values
or (self.filter_low is not None and value < self.filter_low) \ 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.filter_high is not None and value > self.filter_high)
or ( or (
self._prev_value is not None self._prev_value is not None
and self.filter_scale is not None and self.filter_scale is not None
and abs(value)
> 2 # при переходе через 0 каждое небольшое изменение будет иметь слишком большой эффект
and ( and (
abs(value - self._prev_value) / self._prev_value > self.filter_scale abs((value - self._prev_value) / self._prev_value)
> self.filter_scale
) )
)
): ):
if self.fill_na == 'last': if self.fill_na == "last":
value = self._prev_value value = self._prev_value
else: else:
value = None value = None
self._prev_value = value self._prev_value = value
return value return value
except Exception as exc: except Exception as exc:
lg.exception(f'while parsing value') lg.exception(f"while parsing value")
return None return None
@property @property
def filter_values(self): def filter_values(self):
return self.customize.get(CONF_FILTER_VALUES, self.mega.customize.get(CONF_FILTER_VALUES, [])) return self.customize.get(
CONF_FILTER_VALUES, self.mega.customize.get(CONF_FILTER_VALUES, [])
)
@property @property
def filter_scale(self): def filter_scale(self):
return self.customize.get(CONF_FILTER_SCALE, self.mega.customize.get(CONF_FILTER_SCALE, None)) return self.customize.get(
CONF_FILTER_SCALE, self.mega.customize.get(CONF_FILTER_SCALE, None)
)
@property @property
def filter_low(self): def filter_low(self):
return self.customize.get(CONF_FILTER_LOW, self.mega.customize.get(CONF_FILTER_LOW, None)) return self.customize.get(
CONF_FILTER_LOW, self.mega.customize.get(CONF_FILTER_LOW, None)
)
@property @property
def filter_high(self): def filter_high(self):
return self.customize.get(CONF_FILTER_HIGH, self.mega.customize.get(CONF_FILTER_HIGH, None)) return self.customize.get(
CONF_FILTER_HIGH, self.mega.customize.get(CONF_FILTER_HIGH, None)
)
@property @property
def fill_na(self): def fill_na(self):
return self.customize.get(CONF_FILL_NA, 'last') return self.customize.get(CONF_FILL_NA, "last")
class MegaI2C(FilterBadValues): 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())
@@ -183,9 +217,11 @@ class MegaI2C(FilterBadValues):
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
attrs = super().extra_state_attributes or {} attrs = super().extra_state_attributes or {}
attrs.update({ attrs.update(
'i2c_id': self.id_suffix, {
}) "i2c_id": self.id_suffix,
}
)
return attrs return attrs
@property @property
@@ -202,22 +238,24 @@ class MegaI2C(FilterBadValues):
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(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(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) tmpl: Template = self.customize.get(
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) ret = self.filter_value(ret)
if ret is not None: if ret is not None:
return str(ret) return str(ret)
except Exception: except Exception:
lg.exception('while getting value') lg.exception("while getting value")
return None return None
@property @property
@@ -226,14 +264,8 @@ class MegaI2C(FilterBadValues):
class Mega1WSensor(FilterBadValues): class Mega1WSensor(FilterBadValues):
def __init__( def __init__(
self, self, unit_of_measurement=None, device_class=None, key=None, *args, **kwargs
unit_of_measurement=None,
device_class=None,
key=None,
*args,
**kwargs
): ):
""" """
1-wire sensor entity 1-wire sensor entity
@@ -264,7 +296,7 @@ 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().unique_id
@@ -284,23 +316,27 @@ class Mega1WSensor(FilterBadValues):
def native_value(self): def native_value(self):
try: try:
ret = None ret = None
if not hasattr(self, 'key'): if not hasattr(self, "key"):
return None 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 ret is None and self.fill_na == 'fill_na' and self.prev_value is not None: if (
ret is None
and self.fill_na == "fill_na"
and self.prev_value is not None
):
ret = self.prev_value ret = self.prev_value
elif ret is None and self.fill_na == 'fill_na' and self._state is not None: 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)
@@ -310,15 +346,17 @@ class Mega1WSensor(FilterBadValues):
ret = self.prev_value 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(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) tmpl: Template = self.customize.get(
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) ret = self.filter_value(ret)
@@ -326,7 +364,7 @@ class Mega1WSensor(FilterBadValues):
if ret is not None: if ret is not None:
return str(ret) return str(ret)
except Exception: except Exception:
lg.exception('while parsing state') lg.exception("while parsing state")
return None return None
@property @property
@@ -342,6 +380,6 @@ class Mega1WSensor(FilterBadValues):
_constructors = { _constructors = {
'sensor': Mega1WSensor, "sensor": Mega1WSensor,
'i2c': MegaI2C, "i2c": MegaI2C,
} }