From ed4928011b245b5366e307a7ee3210c8d5c157a3 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 15 Jan 2021 16:05:10 +0300 Subject: [PATCH] add refresh devices --- custom_components/mega/__init__.py | 80 ++++++++++++++++++------- custom_components/mega/binary_sensor.py | 14 ++--- custom_components/mega/config_flow.py | 53 ++++++++++------ custom_components/mega/const.py | 7 ++- custom_components/mega/hub.py | 47 +++++++++++++++ custom_components/mega/light.py | 14 ++--- custom_components/mega/sensor.py | 38 ++++-------- 7 files changed, 168 insertions(+), 85 deletions(-) diff --git a/custom_components/mega/__init__.py b/custom_components/mega/__init__.py index 8ea1927..c100bd5 100644 --- a/custom_components/mega/__init__.py +++ b/custom_components/mega/__init__.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.service import bind_hass from homeassistant.components import mqtt from homeassistant.config_entries import ConfigEntry -from .const import DOMAIN, CONF_INVERT, CONF_RELOAD +from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS from .hub import MegaD @@ -33,11 +33,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [ - "light", - "binary_sensor", - "sensor", -] + ALIVE_STATE = 'alive' DEF_ID = 'def' _POLL_TASKS = {} @@ -80,12 +76,22 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def _add_mega(hass: HomeAssistant, id, data: dict): +async def get_hub(hass, entry): + id = entry.data.get('id', entry.entry_id) + data = dict(entry.data) + data.update(entry.options or {}) data.update(id=id) _mqtt = hass.data.get(mqtt.DOMAIN) if _mqtt is None: raise Exception('mqtt not configured, please configure mqtt first') - hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER) + hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER) + return hub + + +async def _add_mega(hass: HomeAssistant, entry: ConfigEntry): + id = entry.data.get('id', entry.entry_id) + hub = await get_hub(hass, entry) + hass.data[DOMAIN][id] = hub if not await hub.authenticate(): raise Exception("not authentificated") mid = await hub.get_mqtt_id() @@ -94,12 +100,9 @@ async def _add_mega(hass: HomeAssistant, id, data: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - id = entry.data.get('id', entry.entry_id) - data = dict(entry.data) - data.update(entry.options or {}) - hub = await _add_mega(hass, id, data) + hub = await _add_mega(hass, entry) _hubs[entry.entry_id] = hub - _subs[entry.entry_id] = entry.add_update_listener(update) + _subs[entry.entry_id] = entry.add_update_listener(updater) for platform in PLATFORMS: hass.async_create_task( @@ -111,26 +114,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def update(hass: HomeAssistant, entry: ConfigEntry): +async def updater(hass: HomeAssistant, entry: ConfigEntry): + """ + Обновляется конфигурация + :param hass: + :param entry: + :return: + """ hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]] hub.poll_interval = entry.options[CONF_SCAN_INTERVAL] - hub.port_to_scan = entry.options[CONF_PORT_TO_SCAN] - if entry.options[CONF_RELOAD]: - await async_remove_entry(hass, entry) - await async_setup_entry(hass, entry) + hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0) + entry.data = entry.options + await async_remove_entry(hass, entry) + await async_setup_entry(hass, entry) return True async def async_remove_entry(hass, entry) -> None: """Handle removal of an entry.""" id = entry.data.get('id', entry.entry_id) - hass.data[DOMAIN][id].unsubscribe_all() - task: asyncio.Task = _POLL_TASKS.pop(id) - task.cancel() + hub = hass.data[DOMAIN] + if hub is None: + return + _LOGGER.debug(f'remove {id}') _hubs.pop(entry.entry_id) + task: asyncio.Task = _POLL_TASKS.pop(id, None) + if task is None: + return + task.cancel() + if hub is None: + return + hub.unsubscribe_all() unsub = _subs.pop(entry.entry_id) - unsub() - hass.data[DOMAIN].pop(id) + if unsub: + unsub() + + +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s to version 2", config_entry.version) + hub = await get_hub(hass, config_entry) + new = dict(config_entry.data) + if config_entry.version == 1: + cfg = await hub.get_config() + new.update(cfg) + _LOGGER.debug(f'new config: %s', new) + config_entry.data = new + config_entry.version = 2 + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True async def _save_service(hass: HomeAssistant, call: ServiceCall): diff --git a/custom_components/mega/binary_sensor.py b/custom_components/mega/binary_sensor.py index 28b77df..03b2c03 100644 --- a/custom_components/mega/binary_sensor.py +++ b/custom_components/mega/binary_sensor.py @@ -62,15 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn hub: MegaD = hass.data['mega'][mid] devices = [] - async def scan(): - async for port, pty, m in hub.scan_ports(): - if pty == "0": - sensor = MegaBinarySensor(mega_id=mid, port=port, config_entry=config_entry) - devices.append(sensor) - - async_add_devices(devices) - - asyncio.create_task(scan()) + for port, cfg in config_entry.data.get('binary_sensor', {}).items(): + hub.lg.debug(f'add binary_sensor on port %s', port) + sensor = MegaBinarySensor(mega_id=mid, port=port, config_entry=config_entry) + devices.append(sensor) + async_add_devices(devices) class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity): diff --git a/custom_components/mega/config_flow.py b/custom_components/mega/config_flow.py index f3c50ed..581469f 100644 --- a/custom_components/mega/config_flow.py +++ b/custom_components/mega/config_flow.py @@ -8,8 +8,8 @@ from homeassistant import config_entries, core from homeassistant.components import mqtt from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL -from homeassistant.core import callback -from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD # pylint:disable=unused-import +from homeassistant.core import callback, HomeAssistant +from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS # pylint:disable=unused-import from .hub import MegaD from . import exceptions @@ -26,6 +26,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +async def get_hub(hass: HomeAssistant, data): + _mqtt = hass.data.get(mqtt.DOMAIN) + if not isinstance(_mqtt, mqtt.MQTT): + raise exceptions.MqttNotConfigured("mqtt must be configured first") + hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt) + if not await hub.authenticate(): + raise exceptions.InvalidAuth + return hub + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. @@ -33,12 +43,7 @@ async def validate_input(hass: core.HomeAssistant, data): """ if data[CONF_ID] in hass.data.get(DOMAIN, []): raise exceptions.DuplicateId('duplicate_id') - _mqtt = hass.data.get(mqtt.DOMAIN) - if not isinstance(_mqtt, mqtt.MQTT): - raise exceptions.MqttNotConfigured("mqtt must be configured first") - hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt) - if not await hub.authenticate(): - raise exceptions.InvalidAuth + hub = await get_hub(hass, data) return hub @@ -46,7 +51,7 @@ async def validate_input(hass: core.HomeAssistant, data): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for mega.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED async def async_step_user(self, user_input=None): @@ -59,7 +64,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - await validate_input(self.hass, user_input) + hub = await validate_input(self.hass, user_input) + config = await hub.get_config() + hub.lg.debug(f'config loaded: %s', config) + config.update(user_input) + return self.async_create_entry( + title=user_input.get(CONF_ID, user_input[CONF_HOST]), + data=config, + ) except exceptions.CannotConnect: errors["base"] = "cannot_connect" except exceptions.InvalidAuth: @@ -69,11 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception as exc: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors[CONF_ID] = str(exc) - else: - return self.async_create_entry( - title=user_input.get(CONF_ID, user_input[CONF_HOST]), - data=user_input, - ) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors @@ -92,10 +99,22 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" + if user_input is not None: + reload = user_input.pop(CONF_RELOAD) + cfg = dict(self.config_entry.data) + cfg.update(user_input) + hub = await get_hub(self.hass, self.config_entry.data) + if reload: + new = await hub.get_config() + _LOGGER.debug(f'new config: %s', new) + cfg = dict(self.config_entry.data) + for x in PLATFORMS: + cfg.pop(x, None) + cfg.update(new) return self.async_create_entry( title='', - data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}}, + data=cfg, ) e = self.config_entry.data ret = self.async_show_form( @@ -103,7 +122,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema({ vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): int, vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int, - # vol.Optional(CONF_RELOAD, default=False): bool, + vol.Optional(CONF_RELOAD, default=False): bool, # vol.Optional(CONF_INVERT, default=''): str, }), ) diff --git a/custom_components/mega/const.py b/custom_components/mega/const.py index e446ace..f8c3b09 100644 --- a/custom_components/mega/const.py +++ b/custom_components/mega/const.py @@ -11,4 +11,9 @@ W1 = 'w1' W1BUS = 'w1bus' CONF_PORT_TO_SCAN = 'port_to_scan' CONF_RELOAD = 'reload' -CONF_INVERT = 'invert' \ No newline at end of file +CONF_INVERT = 'invert' +PLATFORMS = [ + "light", + "binary_sensor", + "sensor", +] \ No newline at end of file diff --git a/custom_components/mega/hub.py b/custom_components/mega/hub.py index 51cce24..b1212b3 100644 --- a/custom_components/mega/hub.py +++ b/custom_components/mega/hub.py @@ -1,6 +1,7 @@ import asyncio import json import logging +from collections import defaultdict from datetime import datetime from functools import wraps @@ -9,10 +10,27 @@ import typing from bs4 import BeautifulSoup from homeassistant.components import mqtt +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from .const import TEMP, HUM from .exceptions import CannotConnect +import re +TEMP_PATT = re.compile(r'temp:([01234567890\.]+)') +HUM_PATT = re.compile(r'hum:([01234567890\.]+)') +PATTERNS = { + TEMP: TEMP_PATT, + HUM: HUM_PATT, +} +UNITS = { + TEMP: '°C', + HUM: '%' +} +CLASSES = { + TEMP: DEVICE_CLASS_TEMPERATURE, + HUM: DEVICE_CLASS_HUMIDITY +} class MegaD: """MegaD Hub""" @@ -275,3 +293,32 @@ class MegaD: ret = await self.scan_port(x) if ret: yield [x, *ret] + + async def get_config(self): + ret = defaultdict(lambda: defaultdict(list)) + async for port, pty, m in self.scan_ports(): + if pty == "0": + ret['binary_sensor'][port].append({}) + elif pty == "1" and m in ['0', '1']: + ret['light'][port].append({'dimmer': m == '1'}) + elif pty == '3': + values = await self.get_port(port) + self.lg.debug(f'values: %s', values) + if values is None: + self.lg.warning(f'port {port} is of type sensor but did not respond, skipping it') + continue + if isinstance(values, str) and TEMP_PATT.search(values): + values = {TEMP: values} + elif not isinstance(values, dict): + values = {None: values} + for key in values: + self.lg.debug(f'add sensor {key}') + ret['sensor'][port].append(dict( + key=key, + patt=PATTERNS.get(key), + unit_of_measurement=UNITS.get(key, UNITS[TEMP]), + # TODO: make other units, make options in config flow + device_class=CLASSES.get(key, CLASSES[TEMP]), + id_suffix=key, + )) + return ret diff --git a/custom_components/mega/light.py b/custom_components/mega/light.py index 42d7421..aa4f841 100644 --- a/custom_components/mega/light.py +++ b/custom_components/mega/light.py @@ -78,14 +78,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn hub: MegaD = hass.data['mega'][mid] devices = [] - async def scan_ports(): - async for port, pty, m in hub.scan_ports(): - if pty == "1" and m in ['0', '1']: - light = MegaLight(mega_id=mid, port=port, dimmer=m == '1', config_entry=config_entry) - devices.append(light) - async_add_devices(devices) - - asyncio.create_task(scan_ports()) + for port, cfg in config_entry.data.get('light', {}).items(): + for data in cfg: + hub.lg.debug(f'add light on port %s with data %s', port, data) + light = MegaLight(mega_id=mid, port=port, config_entry=config_entry, **data) + devices.append(light) + async_add_devices(devices) class MegaLight(LightEntity, BaseMegaEntity): diff --git a/custom_components/mega/sensor.py b/custom_components/mega/sensor.py index 1728664..ac55058 100644 --- a/custom_components/mega/sensor.py +++ b/custom_components/mega/sensor.py @@ -88,34 +88,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn hub: MegaD = hass.data['mega'][mid] devices = [] - async def scan(): - async for port, pty, m in hub.scan_ports(): - if pty == "3": - values = await hub.get_port(port) - lg.debug(f'values: %s', values) - if values is None: - continue - if isinstance(values, str) and TEMP_PATT.search(values): - values = {TEMP: values} - elif not isinstance(values, dict): - values = {None: values} - for key in values: - hub.lg.debug(f'add sensor {W1}:{key}') - sensor = _make_entity( - mid=mid, - port=port, - conf={ - CONF_TYPE: W1, - CONF_KEY: key, - }, - config_entry=config_entry, - ) - devices.append(sensor) - hub.sensors.append(sensor) + for port, cfg in config_entry.data.get('sensor', {}).items(): + for data in cfg: + hub.lg.debug(f'add sensor on port %s with data %s', port, data) + sensor = Mega1WSensor( + mega_id=mid, + port=port, + config_entry=config_entry, + **data, + ) + devices.append(sensor) - async_add_devices(devices) - - asyncio.create_task(scan()) + async_add_devices(devices) class Mega1WSensor(BaseMegaEntity):