diff --git a/custom_components/homewizard_energy/__init__.py b/custom_components/homewizard_energy/__init__.py index 800ebfd..de4970c 100644 --- a/custom_components/homewizard_energy/__init__.py +++ b/custom_components/homewizard_energy/__init__.py @@ -1,137 +1,24 @@ """The Homewizard Energy integration.""" import asyncio import logging -import re -from datetime import timedelta -from enum import unique import aiohwenergy import async_timeout -import voluptuous as vol + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_VERSION, CONF_ID, CONF_STATE -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import slugify - -from .const import ( - ATTR_BRIGHTNESS, - ATTR_POWER_ON, - ATTR_SWITCHLOCK, - CONF_API, - CONF_DATA, - CONF_MODEL, - CONF_NAME, - CONF_SW_VERSION, - CONF_UNLOAD_CB, - COORDINATOR, - DOMAIN, - MODEL_KWH_1, - MODEL_KWH_3, - MODEL_P1, - MODEL_SOCKET, - PLATFORMS, -) - -Logger = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +from .const import CONF_API, COORDINATOR, DOMAIN, PLATFORMS +from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Homewizard Energy component.""" - Logger.debug("__init__ async_setup") - hass.data[DOMAIN] = {} - - return True - +_LOGGER = logging.getLogger(__name__) -async def migrate_old_configuration(hass: HomeAssistant, entry: ConfigEntry): - """ - Migrates 0.4.x configuration, where unique_id was based on IP to - >0.5.0 configuration, where unique_id is based on the serial ID - """ - Logger.info("Migrating old integration to new one") - host_ip = entry.unique_id - api = aiohwenergy.HomeWizardEnergy(host_ip) - try: - with async_timeout.timeout(5): - await api.initialize() - except (asyncio.TimeoutError, aiohwenergy.RequestError): - Logger.error( - "(-1) Error connecting to the Energy device at %s", - host_ip, - ) - return False - except Exception: # pylint: disable=broad-except - Logger.error( - "(-2) Error connecting to the Energy device at %s", - host_ip, - ) - return False - finally: - await api.close() - - if api.device == None: - Logger.error( - "Device (%s) API disabled, enable API and restart integration" % host_ip - ) - return False - - # Update unique_id information - unique_id = "%s_%s" % ( - api.device.product_type, - api.device.serial, - ) - - # Update entities - er = await entity_registry.async_get_registry(hass) - entities = entity_registry.async_entries_for_config_entry(er, entry.entry_id) - old_unique_id_prefix = "p1_meter_%s_" % slugify(host_ip) - - for entity in entities: - new_unique_id_type = entity.unique_id.replace(old_unique_id_prefix, "") - new_unique_id = "%s_%s" % (unique_id, new_unique_id_type) - Logger.debug("Changing %s to %s" % (entity.unique_id, new_unique_id)) - er.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) - - # Update device information - data = entry.data.copy() - data["host"] = host_ip - data["name"] = api.device.product_name - data["custom_name"] = api.device.product_name - data["unique_id"] = unique_id - data.pop("ip_address") - - hass.config_entries.async_update_entry(entry, data=data, unique_id=unique_id) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homewizard Energy from a config entry.""" - Logger.debug("__init__ async_setup_entry") - - hass.data[DOMAIN][entry.data["unique_id"]] = {} - - # Migrate manual config to zeroconf (<0.5.0 to 0.5.x) - # Check if unique_id == ipv4 or ipv6 - if re.match("(?:[0-9]{1,3}\.){3}[0-9]{1,3}", entry.unique_id) or re.match( - "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))", - entry.unique_id, - ): - result = await migrate_old_configuration(hass, entry) - if not result: - return False - - # Add listener for config updates - hass.data[DOMAIN][entry.data["unique_id"]][ - CONF_UNLOAD_CB - ] = entry.add_update_listener(async_entry_updated) + _LOGGER.debug("__init__ async_setup_entry") # Get api and do a initialization energy_api = aiohwenergy.HomeWizardEnergy(entry.data.get("host")) @@ -143,35 +30,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await energy_api.initialize() initialized = True - except (asyncio.TimeoutError, aiohwenergy.RequestError): - Logger.error( + except (asyncio.TimeoutError, aiohwenergy.RequestError) as ex: + _LOGGER.error( "Error connecting to the Energy device at %s", - energy_api._host, + energy_api.host, ) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex + + except aiohwenergy.DisabledError as ex: + _LOGGER.error("API disabled, API must be enabled in the app") + raise ConfigEntryNotReady from ex - except aiohwenergy.AioHwEnergyException: - Logger.exception("Unknown Energy API error occurred") - raise ConfigEntryNotReady + except aiohwenergy.AiohwenergyException as ex: + _LOGGER.error("Unknown Energy API error occurred") + raise ConfigEntryNotReady from ex - except Exception: # pylint: disable=broad-except - Logger.exception( + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( "Unknown error connecting with Energy Device at %s", - energy_api._host["host"], + energy_api.host, ) - return False + raise ConfigEntryNotReady from ex finally: if not initialized: await energy_api.close() # Create coordinator - coordinator = hass.data[DOMAIN][entry.data["unique_id"]][ - COORDINATOR - ] = HWEnergyDeviceUpdateCoordinator(hass, energy_api) + coordinator = Coordinator(hass, energy_api) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.data["unique_id"]][CONF_API] = energy_api + # Finalize + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.data["unique_id"]] = { + COORDINATOR: coordinator, + CONF_API: energy_api, + } + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -180,22 +75,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_entry_updated(hass, config_entry): - """Handle entry updates.""" - Logger.info("Configuration changed, reloading...") - await hass.config_entries.async_reload(config_entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - Logger.debug("__init__ async_unload_entry") + _LOGGER.debug("__init__ async_unload_entry") unload_ok = all( await asyncio.gather( - *[ + *( hass.config_entries.async_forward_entry_unload(entry, component) for component in PLATFORMS - ] + ) ) ) @@ -205,85 +94,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): energy_api = config_data[CONF_API] await energy_api.close() - if CONF_UNLOAD_CB in config_data: - unload_cb = config_data[CONF_UNLOAD_CB] - unload_cb() - return unload_ok - - -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - Logger.debug("__init async_remove_entry") - pass - - -class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator): - """Gather data for the energy device""" - - def __init__( - self, - hass: HomeAssistant, - api: aiohwenergy.HomeWizardEnergy, - ) -> None: - self.api = api - - update_interval = self.get_update_interval() - super().__init__(hass, Logger, name="", update_interval=update_interval) - - def get_update_interval(self) -> timedelta: - - try: - product_type = self.api.device.product_type - except AttributeError: - product_type = "Unknown" - - if product_type == MODEL_P1: - try: - smr_version = self.api.data.smr_version - if smr_version == 50: - return timedelta(seconds=1) - else: - return timedelta(seconds=5) - except AttributeError: - pass - - elif product_type in [MODEL_KWH_1, MODEL_KWH_3, MODEL_SOCKET]: - return timedelta(seconds=5) - - return timedelta(seconds=10) - - async def _async_update_data(self) -> dict: - """Fetch all device and sensor data from api.""" - try: - async with async_timeout.timeout(10): - # Update all properties - status = await self.api.update() - - if not status: - raise Exception("Failed to fetch data") - - data = { - CONF_NAME: self.api.device.product_name, - CONF_MODEL: self.api.device.product_type, - CONF_ID: self.api.device.serial, - CONF_SW_VERSION: self.api.device.firmware_version, - CONF_API_VERSION: self.api.device.api_version, - CONF_DATA: {}, - CONF_STATE: None, - } - - for datapoint in self.api.data.available_datapoints: - data[CONF_DATA][datapoint] = getattr(self.api.data, datapoint) - - if self.api.state is not None: - data[CONF_STATE] = { - ATTR_POWER_ON: self.api.state.power_on, - ATTR_SWITCHLOCK: self.api.state.switch_lock, - ATTR_BRIGHTNESS: self.api.state.brightness, - } - - except Exception as ex: - raise UpdateFailed(ex) from ex - - self.name = data[CONF_NAME] - return data diff --git a/custom_components/homewizard_energy/config_flow.py b/custom_components/homewizard_energy/config_flow.py index 5119f3a..f895349 100644 --- a/custom_components/homewizard_energy/config_flow.py +++ b/custom_components/homewizard_energy/config_flow.py @@ -1,180 +1,148 @@ """Config flow for Homewizard Energy.""" -import asyncio +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any import aiohwenergy -import async_timeout -import voluptuous as vol from aiohwenergy.hwenergy import SUPPORTED_DEVICES -from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.helpers import config_entry_flow -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType +import async_timeout from voluptuous import All, Length, Required, Schema from voluptuous.util import Lower -from .const import CONF_IP_ADDRESS, DOMAIN +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_PORT +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN -Logger = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for P1 meter.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Set up the instance.""" - Logger.debug("config_flow __init__") + _LOGGER.debug("config_flow __init__") async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" - if user_input is None: - return self._show_setup_form() - - # Check if data is IP (Volup?) - - # Make connection with device - energy_api = aiohwenergy.HomeWizardEnergy(user_input[CONF_IP_ADDRESS]) - initialized = False - try: - with async_timeout.timeout(10): - await energy_api.initialize() - if energy_api.device != None: - initialized = True - except (asyncio.TimeoutError, aiohwenergy.RequestError): - Logger.error( - "Error connecting to the Energy device at %s", - energy_api._host, - ) - return self.async_abort(reason="manual_config_request_error") - - except aiohwenergy.AioHwEnergyException: - Logger.exception("Unknown Energy API error occurred") - return self.async_abort(reason="manual_config_unknown_error") + _LOGGER.debug("config_flow async_step_user") - except Exception: # pylint: disable=broad-except - Logger.exception( - "Unknown error connecting with Energy Device at %s", - energy_api._host["host"], + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=Schema( + { + Required(CONF_IP_ADDRESS): str, + } + ), + errors=None, ) - return self.async_abort(reason="manual_config_unknown_error") - finally: - await energy_api.close() - - if not initialized: - return self.async_abort(reason="manual_config_unknown_error") - - # Validate metadata - if energy_api.device.api_version != "v1": - return self.async_abort(reason="manual_config_unsupported_api_version") - - # Configure device entry_info = { - "host": user_input[CONF_IP_ADDRESS], - "port": 80, - "api_enabled": "1", - "path": "/api/v1", - "product_name": energy_api.device.product_name, - "product_type": energy_api.device.product_type, - "serial": energy_api.device.serial, + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PORT: 80, } - Logger.debug(entry_info) - return await self.async_step_check(entry_info) - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_IP_ADDRESS): str, - } - ), - errors=errors or {}, - ) - async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" - Logger.debug("config_flow async_step_zeroconf") + _LOGGER.debug("config_flow async_step_zeroconf") + + # Validate doscovery entry + if ( + "host" not in discovery_info + or "api_enabled" not in discovery_info["properties"] + or "path" not in discovery_info["properties"] + or "product_name" not in discovery_info["properties"] + or "product_type" not in discovery_info["properties"] + or "serial" not in discovery_info["properties"] + ): + return self.async_abort(reason="invalid_discovery_parameters") + + if (discovery_info["properties"]["path"]) != "/api/v1": + return self.async_abort(reason="unsupported_api_version") + + if (discovery_info["properties"]["api_enabled"]) != "1": + return self.async_abort(reason="api_not_enabled") + # Pass parameters entry_info = { - "host": discovery_info["host"], - "port": discovery_info["port"], - "api_enabled": discovery_info["properties"]["api_enabled"] - if "api_enabled" in discovery_info["properties"] - else None, - "path": discovery_info["properties"]["path"] - if "path" in discovery_info["properties"] - else None, - "product_name": discovery_info["properties"]["product_name"] - if "product_name" in discovery_info["properties"] - else None, - "product_type": discovery_info["properties"]["product_type"] - if "product_type" in discovery_info["properties"] - else None, - "serial": discovery_info["properties"]["serial"] - if "serial" in discovery_info["properties"] - else None, + CONF_IP_ADDRESS: discovery_info["host"], + CONF_PORT: discovery_info["port"], } return await self.async_step_check(entry_info) async def async_step_check(self, entry_info): - """Perform some checks and create entry if OK.""" + """Validate API connection and fetch metadata.""" - Logger.debug("config_flow async_step_check") + _LOGGER.debug("config_flow async_step_check") - if entry_info["product_type"] not in SUPPORTED_DEVICES: - Logger.warning( - "Device (%s) not supported by integration" % entry_info["product_type"] - ) - # return self.async_abort(reason="device_not_supported") + # Make connection with device + energy_api = aiohwenergy.HomeWizardEnergy(entry_info[CONF_IP_ADDRESS]) + + initialized = False + try: + with async_timeout.timeout(10): + await energy_api.initialize() + if energy_api.device is not None: + initialized = True - if entry_info["api_enabled"] != "1": - Logger.warning("API not enabled, please enable API in app") + except aiohwenergy.DisabledError: + _LOGGER.error("API disabled, API must be enabled in the app") return self.async_abort(reason="api_not_enabled") - Logger.debug(f"entry_info: {entry_info}") + except Exception: # pylint: disable=broad-except + _LOGGER.error( + "Error connecting with Energy Device at %s", + entry_info[CONF_IP_ADDRESS], + ) + return self.async_abort(reason="unknown_error") - if ( - ("host" not in entry_info) - or ("port" not in entry_info) - or ("serial" not in entry_info) - or ("product_name" not in entry_info) - or ("product_type" not in entry_info) - or ("path" not in entry_info) - or ("api_enabled" not in entry_info) - ): - Logger.warning(f"Invalid discovery parameters") - return self.async_abort(reason="invalid_discovery_parameters") + finally: + await energy_api.close() - self.context["host"] = entry_info["host"] - self.context["unique_id"] = "%s_%s" % ( - entry_info["product_type"], - entry_info["serial"], - ) - self.context["serial"] = entry_info["serial"] - self.context["port"] = entry_info["port"] - self.context["path"] = entry_info["path"] + if not initialized: + _LOGGER.error("Initialization failed") + return self.async_abort(reason="unknown_error") + + # Validate metadata + if energy_api.device.api_version != "v1": + return self.async_abort(reason="unsupported_api_version") + + if energy_api.device.product_type not in SUPPORTED_DEVICES: + _LOGGER.error( + "Device (%s) not supported by integration", + energy_api.device.product_type, + ) + return self.async_abort(reason="device_not_supported") + + # Configure device + entry_info["product_name"] = energy_api.device.product_name + entry_info["product_type"] = energy_api.device.product_type + entry_info["serial"] = energy_api.device.serial + + self.context[CONF_HOST] = entry_info[CONF_IP_ADDRESS] + self.context[CONF_PORT] = entry_info[CONF_PORT] self.context["product_name"] = entry_info["product_name"] self.context["product_type"] = entry_info["product_type"] - self.context["api_enabled"] = entry_info["api_enabled"] - - self.context["name"] = "%s (%s)" % ( - self.context["product_name"], - self.context["serial"][-6:], - ) + self.context["serial"] = entry_info["serial"] + self.context[ + "unique_id" + ] = f"{entry_info['product_type']}_{entry_info['serial']}" + self.context[ + "name" + ] = f"{self.context['product_name']} ({self.context['serial'][-6:]})" await self.async_set_unique_id(self.context["unique_id"]) self._abort_if_unique_id_configured(updates=entry_info) @@ -184,51 +152,36 @@ async def async_step_check(self, entry_info): "unique_id": self.context["unique_id"], } - # TODO Check if device is already configured (but maybe moved to new IP) + return await self.async_step_confirm() - return await self.async_step_discovery_confirm() + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of node.""" - async def async_step_discovery_confirm(self, user_input=None): - """Handle user-confirmation of discovered node.""" - - Logger.debug("config_flow async_step_discovery_confirm") - - errors = {} - - schema = Schema( - { - Required("name", default=self.context["product_name"]): All( - str, Length(min=1) - ) - } - ) + _LOGGER.debug("config_flow async_step_confirm") if user_input is None: return self.async_show_form( - step_id="discovery_confirm", + step_id="confirm", description_placeholders={"name": self.context["product_name"]}, - data_schema=schema, - errors=errors, + data_schema=Schema( + { + Required("name", default=self.context["product_name"]): All( + str, Length(min=1) + ) + } + ), + errors=None, ) - else: - if self.context["api_enabled"] != "1": - Logger.warning("API not enabled") - return self.async_abort(reason="api_not_enabled") + # Format name + self.context["custom_name"] = user_input["name"] + if Lower(self.context["product_name"]) != Lower(user_input["name"]): + title = f"{self.context['product_name']} ({self.context['custom_name']})" + else: + title = self.context["custom_name"] - Logger.debug("async_step_discovery_confirm _create_entry") - self.context["custom_name"] = ( - user_input["name"] if user_input["name"] != "" else self.context["name"] - ) - if Lower(self.context["product_name"]) != Lower(user_input["name"]): - title = "%s (%s)" % ( - self.context["product_name"], - self.context["custom_name"], - ) - else: - title = self.context["custom_name"] - - return self.async_create_entry( - title=title, - data=self.context, - ) + # Finish up + return self.async_create_entry( + title=title, + data=self.context, + ) diff --git a/custom_components/homewizard_energy/const.py b/custom_components/homewizard_energy/const.py index da85da6..35cb8ca 100644 --- a/custom_components/homewizard_energy/const.py +++ b/custom_components/homewizard_energy/const.py @@ -1,7 +1,5 @@ """Constants for the Homewizard Energy integration.""" -from homeassistant import const - # Set up. DOMAIN = "homewizard_energy" COORDINATOR = "coordinator" @@ -9,34 +7,27 @@ PLATFORMS = ["sensor", "switch"] # Platform config. -CONF_ENTITY_ID = const.CONF_ENTITY_ID -CONF_IP_ADDRESS = const.CONF_IP_ADDRESS -CONF_NAME = const.CONF_NAME - +CONF_SERIAL = "serial" CONF_API = "api" -CONF_UNLOAD_CB = "unload_unsub" CONF_MODEL = "model" CONF_SW_VERSION = "sw_ver" CONF_DATA = "data" -# Services. -SERVICE_UPDATE = "update" - # Service attributes. -ATTR_SMR_VERSION = "smr_version" -ATTR_METER_MODEL = "meter_model" -ATTR_WIFI_SSID = "wifi_ssid" -ATTR_WIFI_STRENGTH = "wifi_strength" -ATTR_TOTAL_ENERGY_IMPORT_T1_KWH = "total_power_import_t1_kwh" -ATTR_TOTAL_ENERGY_IMPORT_T2_KWH = "total_power_import_t2_kwh" -ATTR_TOTAL_ENERGY_EXPORT_T1_KWH = "total_power_export_t1_kwh" -ATTR_TOTAL_ENERGY_EXPORT_T2_KWH = "total_power_export_t2_kwh" -ATTR_ACTIVE_POWER_W = "active_power_w" ATTR_ACTIVE_POWER_L1_W = "active_power_l1_w" ATTR_ACTIVE_POWER_L2_W = "active_power_l2_w" ATTR_ACTIVE_POWER_L3_W = "active_power_l3_w" -ATTR_TOTAL_GAS_M3 = "total_gas_m3" +ATTR_ACTIVE_POWER_W = "active_power_w" ATTR_GAS_TIMESTAMP = "gas_timestamp" +ATTR_METER_MODEL = "meter_model" +ATTR_SMR_VERSION = "smr_version" +ATTR_TOTAL_ENERGY_EXPORT_T1_KWH = "total_power_export_t1_kwh" +ATTR_TOTAL_ENERGY_EXPORT_T2_KWH = "total_power_export_t2_kwh" +ATTR_TOTAL_ENERGY_IMPORT_T1_KWH = "total_power_import_t1_kwh" +ATTR_TOTAL_ENERGY_IMPORT_T2_KWH = "total_power_import_t2_kwh" +ATTR_TOTAL_GAS_M3 = "total_gas_m3" +ATTR_WIFI_SSID = "wifi_ssid" +ATTR_WIFI_STRENGTH = "wifi_strength" # State attributes ATTR_POWER_ON = "power_on" diff --git a/custom_components/homewizard_energy/coordinator.py b/custom_components/homewizard_energy/coordinator.py new file mode 100644 index 0000000..0e5fa2b --- /dev/null +++ b/custom_components/homewizard_energy/coordinator.py @@ -0,0 +1,93 @@ +"""Update coordinator for HomeWizard Energy.""" + +from datetime import timedelta +import logging + +import aiohwenergy +import async_timeout + +from homeassistant.const import CONF_API_VERSION, CONF_ID, CONF_NAME, CONF_STATE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_BRIGHTNESS, + ATTR_POWER_ON, + ATTR_SWITCHLOCK, + CONF_DATA, + CONF_MODEL, + CONF_SW_VERSION, + MODEL_P1, +) + +_LOGGER = logging.getLogger(__name__) + + +class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator): + """Gather data for the energy device.""" + + def __init__( + self, + hass: HomeAssistant, + api: aiohwenergy.HomeWizardEnergy, + ) -> None: + """Initialize Update Coordinator.""" + + self.api = api + + update_interval = self.get_update_interval() + super().__init__(hass, _LOGGER, name="", update_interval=update_interval) + + def get_update_interval(self) -> timedelta: + """Return best interval for product type.""" + try: + product_type = self.api.device.product_type + except AttributeError: + product_type = "Unknown" + + if product_type == MODEL_P1: + try: + smr_version = self.api.data.smr_version + if smr_version == 50: + return timedelta(seconds=1) + + except AttributeError: + pass + + return timedelta(seconds=5) + + async def _async_update_data(self) -> dict: + """Fetch all device and sensor data from api.""" + try: + async with async_timeout.timeout(10): + # Update all properties + status = await self.api.update() + + if not status: + raise Exception("Failed to fetch data") + + data = { + CONF_NAME: self.api.device.product_name, + CONF_MODEL: self.api.device.product_type, + CONF_ID: self.api.device.serial, + CONF_SW_VERSION: self.api.device.firmware_version, + CONF_API_VERSION: self.api.device.api_version, + CONF_DATA: {}, + CONF_STATE: None, + } + + for datapoint in self.api.data.available_datapoints: + data[CONF_DATA][datapoint] = getattr(self.api.data, datapoint) + + if self.api.state is not None: + data[CONF_STATE] = { + ATTR_POWER_ON: self.api.state.power_on, + ATTR_SWITCHLOCK: self.api.state.switch_lock, + ATTR_BRIGHTNESS: self.api.state.brightness, + } + + except Exception as ex: + raise UpdateFailed(ex) from ex + + self.name = data[CONF_NAME] + return data diff --git a/custom_components/homewizard_energy/manifest.json b/custom_components/homewizard_energy/manifest.json index 5fca0e8..fdb8a7a 100644 --- a/custom_components/homewizard_energy/manifest.json +++ b/custom_components/homewizard_energy/manifest.json @@ -11,7 +11,7 @@ "zeroconf" ], "requirements": [ - "aiohwenergy==0.3.2" + "aiohwenergy==0.4.0" ], "zeroconf": [ "_hwenergy._tcp.local." diff --git a/custom_components/homewizard_energy/sensor.py b/custom_components/homewizard_energy/sensor.py index 20ac420..726301a 100644 --- a/custom_components/homewizard_energy/sensor.py +++ b/custom_components/homewizard_energy/sensor.py @@ -2,9 +2,8 @@ from __future__ import annotations import logging -from typing import Any, Final +from typing import Final -import aiohwenergy from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -16,7 +15,6 @@ DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, - DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -34,11 +32,11 @@ ATTR_GAS_TIMESTAMP, ATTR_METER_MODEL, ATTR_SMR_VERSION, - ATTR_TOTAL_GAS_M3, ATTR_TOTAL_ENERGY_EXPORT_T1_KWH, ATTR_TOTAL_ENERGY_EXPORT_T2_KWH, ATTR_TOTAL_ENERGY_IMPORT_T1_KWH, ATTR_TOTAL_ENERGY_IMPORT_T2_KWH, + ATTR_TOTAL_GAS_M3, ATTR_WIFI_SSID, ATTR_WIFI_STRENGTH, CONF_API, @@ -49,7 +47,7 @@ DOMAIN, ) -Logger = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( @@ -72,37 +70,32 @@ name="Wifi Strength", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_TOTAL_ENERGY_IMPORT_T1_KWH, - name="Total Energy Import T1", - icon="mdi:home-import-outline", + name="Total Power Import T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key=ATTR_TOTAL_ENERGY_IMPORT_T2_KWH, - name="Total Energy Import T2", - icon="mdi:home-import-outline", + name="Total Power Import T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key=ATTR_TOTAL_ENERGY_EXPORT_T1_KWH, - name="Total Energy Export T1", - icon="mdi:home-export-outline", + name="Total Power Export T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key=ATTR_TOTAL_ENERGY_EXPORT_T2_KWH, - name="Total Energy Export T2", - icon="mdi:home-export-outline", + name="Total Power Export T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -110,7 +103,6 @@ SensorEntityDescription( key=ATTR_ACTIVE_POWER_W, name="Active Power", - icon="mdi:transmission-tower", native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -118,7 +110,6 @@ SensorEntityDescription( key=ATTR_ACTIVE_POWER_L1_W, name="Active Power L1", - icon="mdi:transmission-tower", native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -126,7 +117,6 @@ SensorEntityDescription( key=ATTR_ACTIVE_POWER_L2_W, name="Active Power L2", - icon="mdi:transmission-tower", native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -134,7 +124,6 @@ SensorEntityDescription( key=ATTR_ACTIVE_POWER_L3_W, name="Active Power L3", - icon="mdi:transmission-tower", native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -142,7 +131,6 @@ SensorEntityDescription( key=ATTR_TOTAL_GAS_M3, name="Total Gas", - icon="mdi:fire", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -150,7 +138,6 @@ SensorEntityDescription( key=ATTR_GAS_TIMESTAMP, name="Gas Timestamp", - icon="mdi:timeline-clock", device_class=DEVICE_CLASS_TIMESTAMP, ), ) @@ -158,15 +145,10 @@ async def async_setup_entry(hass, entry, async_add_entities): """Config entry example.""" - Logger.info("Setting up sensor for HomeWizard Energy.") - energy_api = hass.data[DOMAIN][entry.data["unique_id"]][CONF_API] coordinator = hass.data[DOMAIN][entry.data["unique_id"]][COORDINATOR] - # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() - - if energy_api.data != None: + if energy_api.data is not None: entities = [] for description in SENSORS: if description.key in energy_api.data.available_datapoints: @@ -174,19 +156,18 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities, update_before_add=True) return True - else: - return False + + return False class HWEnergySensor(CoordinatorEntity, SensorEntity): - """Representation of a HomeWizard Energy Sensor""" + """Representation of a HomeWizard Energy Sensor.""" - name = None - entry_data = None unique_id = None + name = None def __init__(self, coordinator, entry_data, description): - """Initializes the sensor.""" + """Initialize Sensor Domain.""" super().__init__(coordinator) self.entity_description = description @@ -194,9 +175,9 @@ def __init__(self, coordinator, entry_data, description): self.entry_data = entry_data # Config attributes. - self.name = "%s %s" % (entry_data["custom_name"], description.name) + self.name = "{} {}".format(entry_data["custom_name"], description.name) self.data_type = description.key - self.unique_id = "%s_%s" % (entry_data["unique_id"], description.key) + self.unique_id = "{}_{}".format(entry_data["unique_id"], description.key) # Some values are given, but set to NULL (eg. gas_timestamp when no gas meter is connected) if self.data[CONF_DATA][self.data_type] is None: @@ -213,6 +194,7 @@ def __init__(self, coordinator, entry_data, description): @property def device_info(self) -> DeviceInfo: + """Return device information.""" return { "name": self.entry_data["custom_name"], "manufacturer": "HomeWizard", @@ -222,8 +204,8 @@ def device_info(self) -> DeviceInfo: } @property - def data(self) -> dict[str:Any]: - """Return data from DataUpdateCoordinator""" + def data(self): + """Return data object from DataUpdateCoordinator.""" return self.coordinator.data @property @@ -233,21 +215,10 @@ def icon(self): @property def state(self): - """Returns state of meter.""" + """Return state of meter.""" return self.data[CONF_DATA][self.data_type] @property def available(self): - """Returns state of meter.""" + """Return availability of meter.""" return self.data_type in self.data[CONF_DATA] - - -async def async_get_aiohwenergy_from_entry_data(entry_data): - """Create a HomewizardEnergy object from entry data.""" - - Logger.debug( - "%s async_get_aiohwenergy_from_entry_data\nentry_data:\n%s" - % (__name__, str(entry_data)) - ) - - return aiohwenergy.HomeWizardEnergy(entry_data["host"]) diff --git a/custom_components/homewizard_energy/switch.py b/custom_components/homewizard_energy/switch.py index 76d62c9..a7b79cc 100644 --- a/custom_components/homewizard_energy/switch.py +++ b/custom_components/homewizard_energy/switch.py @@ -3,9 +3,7 @@ from typing import Any, Final -import aiohwenergy import logging -import homeassistant.helpers.device_registry as dr from homeassistant.components.switch import ( DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, @@ -14,12 +12,11 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_STATE -from homeassistant.core import DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, - DataUpdateCoordinator, ) from .const import ( @@ -47,7 +44,6 @@ ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -58,7 +54,7 @@ async def async_setup_entry( energy_api = hass.data[DOMAIN][entry.data["unique_id"]][CONF_API] coordinator = hass.data[DOMAIN][entry.data["unique_id"]][COORDINATOR] - if energy_api.state != None: + if energy_api.state is not None: entities = [] for description in SWITCHES: entities.append( @@ -69,7 +65,7 @@ async def async_setup_entry( class HWEnergySwitch(CoordinatorEntity, SwitchEntity): - """Representation of a HomeWizard Energy Switch""" + """Representation of a HomeWizard Energy Switch.""" unique_id = None name = None