diff --git a/.github/helpers/update_manifest.py b/.github/helpers/update_manifest.py new file mode 100644 index 0000000..abbe2f5 --- /dev/null +++ b/.github/helpers/update_manifest.py @@ -0,0 +1,27 @@ +"""Update the manifest file.""" +"""Idea from https://github.com/hacs/integration/blob/main/manage/update_manifest.py""" + +import sys +import json +import os + + +def update_manifest(): + """Update the manifest file.""" + version = "0.0.0" + for index, value in enumerate(sys.argv): + if value in ["--version", "-V"]: + version = sys.argv[index + 1] + + with open(f"{os.getcwd()}/custom_components/homewizard_energy/manifest.json") as manifestfile: + manifest = json.load(manifestfile) + + manifest["version"] = version + + with open( + f"{os.getcwd()}/custom_components/homewizard_energy/manifest.json", "w" + ) as manifestfile: + manifestfile.write(json.dumps(manifest, indent=4, sort_keys=True)) + + +update_manifest() diff --git a/custom_components/homewizard_energy/__init__.py b/custom_components/homewizard_energy/__init__.py index 9dc56cf..800ebfd 100644 --- a/custom_components/homewizard_energy/__init__.py +++ b/custom_components/homewizard_energy/__init__.py @@ -1,41 +1,44 @@ """The Homewizard Energy integration.""" import asyncio -from homeassistant.const import CONF_API_VERSION, CONF_ID, CONF_STATE -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import logging -from datetime import timedelta 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.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_NAME, - CONF_MODEL, CONF_DATA, + CONF_MODEL, + CONF_NAME, CONF_SW_VERSION, - CONF_OVERRIDE_POLL_INTERVAL, - CONF_POLL_INTERVAL_SECONDS, CONF_UNLOAD_CB, COORDINATOR, - DEFAULT_OVERRIDE_POLL_INTERVAL, - DEFAULT_POLL_INTERVAL_SECONDS, 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) -PLATFORMS = ["sensor"] - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Homewizard Energy component.""" @@ -132,7 +135,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Get api and do a initialization energy_api = aiohwenergy.HomeWizardEnergy(entry.data.get("host")) - await energy_api.initialize() + + # Validate connection + initialized = False + try: + with async_timeout.timeout(10): + await energy_api.initialize() + initialized = True + + except (asyncio.TimeoutError, aiohwenergy.RequestError): + Logger.error( + "Error connecting to the Energy device at %s", + energy_api._host, + ) + raise ConfigEntryNotReady + + except aiohwenergy.AioHwEnergyException: + Logger.exception("Unknown Energy API error occurred") + raise ConfigEntryNotReady + + except Exception: # pylint: disable=broad-except + Logger.exception( + "Unknown error connecting with Energy Device at %s", + energy_api._host["host"], + ) + return False + + finally: + if not initialized: + await energy_api.close() # Create coordinator coordinator = hass.data[DOMAIN][entry.data["unique_id"]][ @@ -196,9 +227,31 @@ def __init__( ) -> None: self.api = api - update_interval = timedelta(seconds=10) + 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: @@ -224,9 +277,9 @@ async def _async_update_data(self) -> dict: if self.api.state is not None: data[CONF_STATE] = { - "power_on": self.api.state.power_on, - "switch_lock": self.api.state.switch_lock, - "brightness": self.api.state.brightness, + 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: diff --git a/custom_components/homewizard_energy/config_flow.py b/custom_components/homewizard_energy/config_flow.py index 9e9676f..5119f3a 100644 --- a/custom_components/homewizard_energy/config_flow.py +++ b/custom_components/homewizard_energy/config_flow.py @@ -15,14 +15,7 @@ from voluptuous import All, Length, Required, Schema from voluptuous.util import Lower -from .const import ( - CONF_IP_ADDRESS, - CONF_OVERRIDE_POLL_INTERVAL, - CONF_POLL_INTERVAL_SECONDS, - DEFAULT_OVERRIDE_POLL_INTERVAL, - DEFAULT_POLL_INTERVAL_SECONDS, - DOMAIN, -) +from .const import CONF_IP_ADDRESS, DOMAIN Logger = logging.getLogger(__name__) @@ -33,12 +26,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return HWEnergyConfigFlowHandler(config_entry) - def __init__(self): """Set up the instance.""" Logger.debug("config_flow __init__") @@ -155,7 +142,7 @@ async def async_step_check(self, entry_info): # return self.async_abort(reason="device_not_supported") if entry_info["api_enabled"] != "1": - # Logger.warning("API not enabled, please enable API in app") + Logger.warning("API not enabled, please enable API in app") return self.async_abort(reason="api_not_enabled") Logger.debug(f"entry_info: {entry_info}") @@ -245,38 +232,3 @@ async def async_step_discovery_confirm(self, user_input=None): title=title, data=self.context, ) - - -class HWEnergyConfigFlowHandler(config_entries.OptionsFlow): - """Handle options.""" - - def __init__(self, config_entry): - """Initialize Hue options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: - """Manage Energy options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_OVERRIDE_POLL_INTERVAL, - default=self.config_entry.options.get( - CONF_OVERRIDE_POLL_INTERVAL, DEFAULT_OVERRIDE_POLL_INTERVAL - ), - ): bool, - vol.Required( - CONF_POLL_INTERVAL_SECONDS, - default=self.config_entry.options.get( - CONF_POLL_INTERVAL_SECONDS, DEFAULT_POLL_INTERVAL_SECONDS - ), - ): vol.All(cv.positive_int, vol.Range(min=1)), - } - ), - ) diff --git a/custom_components/homewizard_energy/const.py b/custom_components/homewizard_energy/const.py index 1dad5ac..a5a942a 100644 --- a/custom_components/homewizard_energy/const.py +++ b/custom_components/homewizard_energy/const.py @@ -6,7 +6,7 @@ DOMAIN = "homewizard_energy" COORDINATOR = "coordinator" MANUFACTURER_NAME = "HomeWizard" -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "switch"] # Platform config. CONF_ENTITY_ID = const.CONF_ENTITY_ID @@ -38,14 +38,17 @@ ATTR_TOTAL_GAS_M3 = "total_gas_m3" ATTR_GAS_TIMESTAMP = "gas_timestamp" +# State attributes +ATTR_POWER_ON = "power_on" +ATTR_SWITCHLOCK = "switch_lock" +ATTR_BRIGHTNESS = "brightness" + # Default values. DEFAULT_STR_VALUE = "undefined" DEVICE_DEFAULT_NAME = "P1 Meter" - -# Config -CONF_OVERRIDE_POLL_INTERVAL = "override_poll_interval" -DEFAULT_OVERRIDE_POLL_INTERVAL = False - -CONF_POLL_INTERVAL_SECONDS = "poll_interval_seconds" -DEFAULT_POLL_INTERVAL_SECONDS = 10 +# Device models +MODEL_P1 = "HWE-P1" +MODEL_KWH_1 = "SDM230-wifi" +MODEL_KWH_3 = "SDM630-wifi" +MODEL_SOCKET = "HWE-SKT" diff --git a/custom_components/homewizard_energy/manifest.json b/custom_components/homewizard_energy/manifest.json index 701dea5..2f12aef 100644 --- a/custom_components/homewizard_energy/manifest.json +++ b/custom_components/homewizard_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "homewizard_energy", "name": "HomeWizard Energy", - "version": "0.8.2", + "version": "0.0.0", "documentation": "https://github.com/DCSBL/ha-homewizard-energy", "issue_tracker": "https://github.com/DCSBL/ha-homewizard-energy/issues", "codeowners": [ @@ -11,7 +11,7 @@ "zeroconf" ], "requirements": [ - "aiohwenergy==0.1.1" + "aiohwenergy==0.2.3" ], "zeroconf": [ "_hwenergy._tcp.local." diff --git a/custom_components/homewizard_energy/sensor.py b/custom_components/homewizard_energy/sensor.py index ad7205a..ea53d5b 100644 --- a/custom_components/homewizard_energy/sensor.py +++ b/custom_components/homewizard_energy/sensor.py @@ -1,34 +1,29 @@ """Creates Homewizard Energy sensor entities.""" +from __future__ import annotations + import asyncio import logging -from datetime import timedelta from typing import Any, Final import aiohwenergy -import async_timeout from homeassistant.components.sensor import ( - ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( - CONF_STATE, + CONF_ID, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, VOLUME_CUBIC_METERS, - DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp from .const import ( @@ -50,17 +45,13 @@ CONF_DATA, CONF_MODEL, CONF_SW_VERSION, - CONF_OVERRIDE_POLL_INTERVAL, - CONF_POLL_INTERVAL_SECONDS, COORDINATOR, - DEFAULT_OVERRIDE_POLL_INTERVAL, - DEFAULT_POLL_INTERVAL_SECONDS, DOMAIN, ) Logger = logging.getLogger(__name__) -SENSORS: Final[list[SensorEntityDescription]] = [ +SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key=ATTR_SMR_VERSION, name="SMR version", @@ -162,35 +153,7 @@ icon="mdi:timeline-clock", device_class=DEVICE_CLASS_TIMESTAMP, ), -] - - -def get_update_interval(entry, energy_api): - - if entry.options.get(CONF_OVERRIDE_POLL_INTERVAL, DEFAULT_OVERRIDE_POLL_INTERVAL): - return entry.options.get( - CONF_POLL_INTERVAL_SECONDS, DEFAULT_POLL_INTERVAL_SECONDS - ) - - try: - product_type = energy_api.device.product_type - except AttributeError: - product_type = "Unknown" - - if product_type == "HWE-P1": - try: - smr_version = energy_api.data.smr_version - if smr_version == 50: - return 1 - else: - return 5 - except AttributeError: - pass - - elif product_type == "SDM230-wifi" or product_type == "SDM630-wifi": - return 1 - - return 10 +) async def async_setup_entry(hass, entry, async_add_entities): @@ -198,36 +161,6 @@ async def async_setup_entry(hass, entry, async_add_entities): Logger.info("Setting up sensor for HomeWizard Energy.") energy_api = hass.data[DOMAIN][entry.data["unique_id"]][CONF_API] - - # Validate connection - initialized = False - try: - with async_timeout.timeout(10): - await energy_api.initialize() - initialized = True - - except (asyncio.TimeoutError, aiohwenergy.RequestError): - Logger.error( - "Error connecting to the Energy device at %s", - energy_api._host, - ) - raise ConfigEntryNotReady - - except aiohwenergy.AioHwEnergyException: - Logger.exception("Unknown Energy API error occurred") - raise ConfigEntryNotReady - - except Exception: # pylint: disable=broad-except - Logger.exception( - "Unknown error connecting with Energy Device at %s", - energy_api._host["host"], - ) - return False - - finally: - if not initialized: - await energy_api.close() - coordinator = hass.data[DOMAIN][entry.data["unique_id"]][COORDINATOR] # Fetch initial data so we have data when entities subscribe @@ -242,14 +175,12 @@ async def async_setup_entry(hass, entry, async_add_entities): return True else: - await energy_api.close() return False class HWEnergySensor(CoordinatorEntity, SensorEntity): - """Representation of a HomeWizard Energy""" + """Representation of a HomeWizard Energy Sensor""" - host = None name = None entry_data = None unique_id = None @@ -264,18 +195,31 @@ def __init__(self, coordinator, entry_data, description): # Config attributes. self.name = "%s %s" % (entry_data["custom_name"], description.name) - self.host = entry_data["host"] self.data_type = description.key self.unique_id = "%s_%s" % (entry_data["unique_id"], description.key) self._attr_last_reset = utc_from_timestamp(0) + # 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: + self.entity_description.entity_registry_enabled_default = False + + # Special case for export, not everyone has solarpanels + # The change that 'export' is non-zero when you have solar panels is nil + if self.data_type in [ + ATTR_TOTAL_POWER_EXPORT_T1_KWH, + ATTR_TOTAL_POWER_EXPORT_T2_KWH, + ]: + if self.data[CONF_DATA][self.data_type] == 0: + self.entity_description.entity_registry_enabled_default = False + @property def device_info(self) -> DeviceInfo: return { - "name": self.name, + "name": self.entry_data["custom_name"], "manufacturer": "HomeWizard", "sw_version": self.data[CONF_SW_VERSION], "model": self.data[CONF_MODEL], + "identifiers": {(DOMAIN, self.data[CONF_ID])}, } @property diff --git a/custom_components/homewizard_energy/switch.py b/custom_components/homewizard_energy/switch.py new file mode 100644 index 0000000..012664e --- /dev/null +++ b/custom_components/homewizard_energy/switch.py @@ -0,0 +1,129 @@ +"""Creates Homewizard Energy switch entities.""" + +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, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID, CONF_STATE +from homeassistant.core import DOMAIN, 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 ( + ATTR_POWER_ON, + ATTR_SWITCHLOCK, + CONF_API, + CONF_MODEL, + CONF_SW_VERSION, + COORDINATOR, + DOMAIN, +) + +Logger = logging.getLogger(__name__) + +SWITCHES: Final[list[SwitchEntityDescription]] = [ + SwitchEntityDescription( + key=ATTR_POWER_ON, name="Switch", device_class=DEVICE_CLASS_OUTLET + ), + SwitchEntityDescription( + key=ATTR_SWITCHLOCK, + name="Switch lock", + device_class=DEVICE_CLASS_SWITCH, + icon="mdi:lock", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + + 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: + entities = [] + for description in SWITCHES: + entities.append( + HWEnergySwitch(coordinator, entry.data, description, energy_api) + ) + + async_add_entities(entities) + + +class HWEnergySwitch(CoordinatorEntity, SwitchEntity): + """Representation of a HomeWizard Energy Switch""" + + unique_id = None + name = None + + def __init__(self, coordinator, entry_data, description, api) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.entity_description = description + self.coordinator = coordinator + self.entry_data = entry_data + self.api = api + + # Config attributes + self.name = "%s %s" % (entry_data["custom_name"], description.name) + self.unique_id = "%s_%s" % (entry_data["unique_id"], description.key) + self.data_type = description.key + + @property + def data(self) -> dict[str, Any]: + """Return data from DataUpdateCoordinator.""" + return self.coordinator.data + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return { + "name": self.entry_data["custom_name"], + "manufacturer": "HomeWizard", + "sw_version": self.data[CONF_SW_VERSION], + "model": self.data[CONF_MODEL], + "identifiers": {(DOMAIN, self.data[CONF_ID])}, + } + + @property + def is_on(self): + """Return true if switch is on.""" + return self.data[CONF_STATE][self.data_type] + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + if self.data_type == ATTR_POWER_ON: + await self.api.state.set(power_on=True) + elif self.data_type == ATTR_SWITCHLOCK: + await self.api.state.set(switch_lock=True) + else: + Logger.error("Internal error, unknown action") + + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + if self.data_type == ATTR_POWER_ON: + await self.api.state.set(power_on=False) + elif self.data_type == ATTR_SWITCHLOCK: + await self.api.state.set(switch_lock=False) + else: + Logger.error("Internal error, unknown action") + + await self.coordinator.async_refresh() diff --git a/custom_components/homewizard_energy/translations/en.json b/custom_components/homewizard_energy/translations/en.json index dc2d49e..1cd1990 100644 --- a/custom_components/homewizard_energy/translations/en.json +++ b/custom_components/homewizard_energy/translations/en.json @@ -17,22 +17,12 @@ "data": { "ip_address": "IP Address" }, - "description": "Set up your HomeWizard Energy P1 meter to integrate with Home Assistant.", + "description": "Enter the IP address of your HomeWizard Energy device to integrate with Home Assistant.", "title": "Configure device" }, "discovery_confirm": { "description": "Please enter a name for your {name}:" } } - }, - "options": { - "step": { - "init": { - "data": { - "override_poll_interval": "Override default poll interval", - "poll_interval_seconds": "Poll interval (seconds)" - } - } - } } -} \ No newline at end of file +} diff --git a/custom_components/homewizard_energy/translations/nl.json b/custom_components/homewizard_energy/translations/nl.json index 78e620e..e10581f 100644 --- a/custom_components/homewizard_energy/translations/nl.json +++ b/custom_components/homewizard_energy/translations/nl.json @@ -11,20 +11,19 @@ "manual_config_unsupported_api_version": "API versie niet ondersteund", "invalid_discovery_parameters": "Fout: invalid_discovery_parameters" }, + "step": { + "user": + { + "data": { + "ip_address": "IP Address" + }, + "description": "Vul het IP adres in van je HomeWizard Energy apparaat", + "title": "Apparaat toevoegen" + }, "discovery_confirm": { "description": "Geef een naam aan je apparaat:" } } - }, - "options": { - "step": { - "init": { - "data": { - "override_poll_interval": "Gebruik eigen update frequentie", - "poll_interval_seconds": "Update frequentie (in seconden)" - } - } - } } -} \ No newline at end of file +} diff --git a/hacs.json b/hacs.json index bfbe9e3..14768ad 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,7 @@ { "name": "HomeWizard Energy", "render_readme": true, - "domains": ["sensor"], + "domains": ["sensor", "switch"], "homeassistant": "2021.8.0", "iot_class": "local_polling", "zip_release": true,