From 58848f96232abfc837a61563221e59641f318661 Mon Sep 17 00:00:00 2001 From: Georg Henzler Date: Wed, 18 Sep 2024 12:24:34 +0200 Subject: [PATCH 1/6] Update readme.md for config flow --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea83a75..e69c863 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Expert view) and makes it available in [Home Assistant](https://home-assistant.i ### HACS (preferred method) - In [HACS](https://github.com/hacs/default) Store search for erikkastelec/hass-WEM-Portal and install it -- Activate the component by adding configuration into your `configuration.yaml` file. +- Activate the component by configuring it via UI as described in [Configuration](#configuration) section below. ### Manual install @@ -58,4 +58,4 @@ Configuration variables: ## Troubleshooting Please set your logging for the custom_component to debug: -Go to `Settings > Devices&Services `, find WEM Portal and click on `three dots` at the bottom of the card. Click on `Enable debug logging`. \ No newline at end of file +Go to `Settings > Devices&Services `, find WEM Portal and click on `three dots` at the bottom of the card. Click on `Enable debug logging`. From b53b7ed89aaa6b92806eea38f5f4e6a0b6f9856e Mon Sep 17 00:00:00 2001 From: Dirk Date: Sun, 6 Oct 2024 19:43:49 +0200 Subject: [PATCH 2/6] #96 fixed api call for mobile api --- custom_components/wemportal/wemportalapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/wemportal/wemportalapi.py b/custom_components/wemportal/wemportalapi.py index fc4415d..d533f95 100644 --- a/custom_components/wemportal/wemportalapi.py +++ b/custom_components/wemportal/wemportalapi.py @@ -276,7 +276,7 @@ def make_api_call( def get_devices(self): _LOGGER.debug("Fetching api device data") self.modules = {} - data = self.make_api_call("https://www.wemportal.com/app/device/Read").json() + data = self.make_api_call("https://www.wemportal.com/app/Device/Read").json() for device in data["Devices"]: self.data[device["ID"]] = {} From e8640bb7daa00bd114bbc8bd2e5856c3327030c0 Mon Sep 17 00:00:00 2001 From: Dirk Date: Mon, 7 Oct 2024 11:03:42 +0200 Subject: [PATCH 3/6] #100 version number change to match the current released version --- custom_components/wemportal/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/wemportal/manifest.json b/custom_components/wemportal/manifest.json index 38552fe..45a8f03 100644 --- a/custom_components/wemportal/manifest.json +++ b/custom_components/wemportal/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://github.com/erikkastelec/hass-WEM-Portal", "issue_tracker": "https://github.com/erikkastelec/hass-WEM-Portal/issues", "dependencies": [], - "version": "1.5.9", + "version": "1.5.10", "codeowners": [ "@erikkastelec" ], From cfd8a914b95280eca923a80304d94af86507b6d3 Mon Sep 17 00:00:00 2001 From: Konstantin Baumann Date: Thu, 24 Oct 2024 09:14:29 +0200 Subject: [PATCH 4/6] attempt to fix #76 WEM seems to use `kW (W)` for `W` and `kW (W)h` for `Wh`. This fix checks the `unit of measurement` value for the above 2 patterns and fixes them to the correct values. --- custom_components/wemportal/number.py | 3 ++- custom_components/wemportal/sensor.py | 3 ++- custom_components/wemportal/switch.py | 3 ++- custom_components/wemportal/utils.py | 7 +++++++ 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 custom_components/wemportal/utils.py diff --git a/custom_components/wemportal/number.py b/custom_components/wemportal/number.py index 49adabe..94cdccd 100644 --- a/custom_components/wemportal/number.py +++ b/custom_components/wemportal/number.py @@ -10,6 +10,7 @@ from . import get_wemportal_unique_id from .const import _LOGGER, DOMAIN from homeassistant.helpers.entity import DeviceInfo +from .utils import fix_unit_of_measurement async def async_setup_platform( @@ -77,7 +78,7 @@ def __init__( self._last_updated = None self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_native_unit_of_measurement = entity_data["unit"] + self._attr_native_unit_of_measurement = fix_unit_of_measurement(entity_data["unit"]) self._attr_native_value = entity_data["value"] self._attr_native_min_value = entity_data["min_value"] self._attr_native_max_value = entity_data["max_value"] diff --git a/custom_components/wemportal/sensor.py b/custom_components/wemportal/sensor.py index 8aaa2dc..fa96969 100644 --- a/custom_components/wemportal/sensor.py +++ b/custom_components/wemportal/sensor.py @@ -16,6 +16,7 @@ from .const import _LOGGER, DOMAIN from . import get_wemportal_unique_id +from .utils import fix_unit_of_measurement async def async_setup_platform( @@ -81,7 +82,7 @@ def __init__( ) self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_native_unit_of_measurement = entity_data["unit"] + self._attr_native_unit_of_measurement = fix_unit_of_measurement(entity_data["unit"]) self._attr_native_value = entity_data["value"] self._attr_should_poll = False diff --git a/custom_components/wemportal/switch.py b/custom_components/wemportal/switch.py index a7547a5..79d841c 100644 --- a/custom_components/wemportal/switch.py +++ b/custom_components/wemportal/switch.py @@ -11,6 +11,7 @@ from .const import _LOGGER, DOMAIN from . import get_wemportal_unique_id +from .utils import fix_unit_of_measurement async def async_setup_platform( @@ -83,7 +84,7 @@ def __init__( self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_unit = entity_data["unit"] + self._attr_unit = fix_unit_of_measurement(entity_data["unit"]) self._attr_state = entity_data["value"] self._attr_should_poll = False self._attr_device_class = "switch" # type: ignore diff --git a/custom_components/wemportal/utils.py b/custom_components/wemportal/utils.py new file mode 100644 index 0000000..9cc84b1 --- /dev/null +++ b/custom_components/wemportal/utils.py @@ -0,0 +1,7 @@ +def fix_unit_of_measurement(uom: str) -> str: + """Fix the unit of measurement. WEM Portal uses "kW (W)" for "W" and "kW (W)h" for "Wh".""" + + return { + "kW (W)": "W", + "kW (W)h": "Wh", + }.get(uom, uom) From 9f74a2a9eda4d91d29ad8ec265ef62baf24c8ae9 Mon Sep 17 00:00:00 2001 From: Konstantin Baumann Date: Thu, 31 Oct 2024 08:12:27 +0100 Subject: [PATCH 5/6] fix also the casing of some units: `KW` -> `kW`, `KWh` -> `kWh` --- custom_components/wemportal/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/custom_components/wemportal/utils.py b/custom_components/wemportal/utils.py index 9cc84b1..8a805a9 100644 --- a/custom_components/wemportal/utils.py +++ b/custom_components/wemportal/utils.py @@ -1,7 +1,10 @@ def fix_unit_of_measurement(uom: str) -> str: - """Fix the unit of measurement. WEM Portal uses "kW (W)" for "W" and "kW (W)h" for "Wh".""" + """Fix the unit of measurement. WEM Portal uses "kW (W)" for "W" and "kW (W)h" for "Wh". Also fix the casing of some units.""" return { - "kW (W)": "W", - "kW (W)h": "Wh", - }.get(uom, uom) + "w": "W", + "kw": "kW", + "kwh": "kWh", + "kw (w)": "W", + "kw (w)h": "Wh", + }.get(uom.lower(), uom) From 981646cdb011191eb96e70ab5f2094638c9f2e88 Mon Sep 17 00:00:00 2001 From: Konstantin Baumann Date: Thu, 31 Oct 2024 08:55:07 +0100 Subject: [PATCH 6/6] improve/fix UoM (unit of measurement) handling --- custom_components/wemportal/number.py | 29 +++++++-- custom_components/wemportal/sensor.py | 52 ++++++++-------- custom_components/wemportal/switch.py | 14 ++++- custom_components/wemportal/utils.py | 85 ++++++++++++++++++++++++--- 4 files changed, 134 insertions(+), 46 deletions(-) diff --git a/custom_components/wemportal/number.py b/custom_components/wemportal/number.py index 94cdccd..b52f279 100644 --- a/custom_components/wemportal/number.py +++ b/custom_components/wemportal/number.py @@ -10,7 +10,7 @@ from . import get_wemportal_unique_id from .const import _LOGGER, DOMAIN from homeassistant.helpers.entity import DeviceInfo -from .utils import fix_unit_of_measurement +from .utils import (fix_value_and_uom, uom_to_device_class) async def async_setup_platform( @@ -69,6 +69,9 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + + val, uom = fix_value_and_uom(entity_data["value"], entity_data["unit"]) + self._config_entry = config_entry self._device_id = device_id self._attr_name = _unique_id @@ -78,8 +81,8 @@ def __init__( self._last_updated = None self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_native_unit_of_measurement = fix_unit_of_measurement(entity_data["unit"]) - self._attr_native_value = entity_data["value"] + self._attr_native_unit_of_measurement = uom + self._attr_native_value = val self._attr_native_min_value = entity_data["min_value"] self._attr_native_max_value = entity_data["max_value"] self._attr_native_step = entity_data["step"] @@ -87,6 +90,8 @@ def __init__( self._module_index = entity_data["ModuleIndex"] self._module_type = entity_data["ModuleType"] + _LOGGER.debug(f'Init number: {self._attr_name}: "{self._attr_native_value}" [{self._attr_native_unit_of_measurement}]') + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await self.hass.async_add_executor_job( @@ -128,9 +133,17 @@ def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" try: - self._attr_native_value = self.coordinator.data[self._device_id][ - self._attr_name - ]["value"] + entity_data = self.coordinator.data[self._device_id][self._attr_name] + val, uom = fix_value_and_uom(entity_data["value"], entity_data["unit"]) + + self._attr_native_value = val + + # set uom if it references a valid non-trivial unit of measurement + if not uom in (None, ""): + self._attr_native_unit_of_measurement = uom + + _LOGGER.debug(f'Update number: {self._attr_name}: "{self._attr_native_value}" [{self._attr_native_unit_of_measurement}]') + except KeyError: self._attr_native_value = None _LOGGER.warning("Can't find %s", self._attr_unique_id) @@ -138,6 +151,10 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() + @property + def device_class(self): + return uom_to_device_class(self._attr_native_unit_of_measurement) + @property def extra_state_attributes(self): """Return the state attributes of this device.""" diff --git a/custom_components/wemportal/sensor.py b/custom_components/wemportal/sensor.py index fa96969..4ee6d57 100644 --- a/custom_components/wemportal/sensor.py +++ b/custom_components/wemportal/sensor.py @@ -2,11 +2,7 @@ Sensor platform for wemportal component """ -from homeassistant.components.sensor import ( - SensorEntity, - SensorDeviceClass, - SensorStateClass, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -16,7 +12,7 @@ from .const import _LOGGER, DOMAIN from . import get_wemportal_unique_id -from .utils import fix_unit_of_measurement +from .utils import (fix_value_and_uom, uom_to_device_class, uom_to_state_class) async def async_setup_platform( @@ -73,6 +69,9 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + + val, uom = fix_value_and_uom(entity_data["value"], entity_data["unit"]) + self._last_updated = None self._config_entry = config_entry self._device_id = device_id @@ -82,10 +81,12 @@ def __init__( ) self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_native_unit_of_measurement = fix_unit_of_measurement(entity_data["unit"]) - self._attr_native_value = entity_data["value"] + self._attr_native_unit_of_measurement = uom + self._attr_native_value = val self._attr_should_poll = False + _LOGGER.debug(f'Init sensor: {self._attr_name}: "{self._attr_native_value}" [{self._attr_native_unit_of_measurement}]') + @property def device_info(self) -> DeviceInfo: """Get device information.""" @@ -114,9 +115,18 @@ def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" try: - self._attr_native_value = self.coordinator.data[self._device_id][ - self._attr_name - ]["value"] + + entity_data = self.coordinator.data[self._device_id][self._attr_name] + val, uom = fix_value_and_uom(entity_data["value"], entity_data["unit"]) + + self._attr_native_value = val + + # set uom if it references a valid non-trivial unit of measurement + if not uom in (None, ""): + self._attr_native_unit_of_measurement = uom + + _LOGGER.debug(f'Update sensor: {self._attr_name}: "{self._attr_native_value}" [{self._attr_native_unit_of_measurement}]') + except KeyError: self._attr_native_value = None _LOGGER.warning("Can't find %s", self._attr_unique_id) @@ -126,27 +136,11 @@ def _handle_coordinator_update(self) -> None: @property def device_class(self): - """Return the device_class of this entity.""" - if self._attr_native_unit_of_measurement == "°C": - return SensorDeviceClass.TEMPERATURE - elif self._attr_native_unit_of_measurement in ("kWh", "Wh"): - return SensorDeviceClass.ENERGY - elif self._attr_native_unit_of_measurement in ("kW", "W"): - return SensorDeviceClass.POWER - elif self._attr_native_unit_of_measurement == "%": - return SensorDeviceClass.POWER_FACTOR - else: - return None + return uom_to_device_class(self._attr_native_unit_of_measurement) @property def state_class(self): - """Return the state class of this entity, if any.""" - if self._attr_native_unit_of_measurement in ("°C", "kW", "W", "%"): - return SensorStateClass.MEASUREMENT - elif self._attr_native_unit_of_measurement in ("kWh", "Wh"): - return SensorStateClass.TOTAL_INCREASING - else: - return None + return uom_to_state_class(self._attr_native_unit_of_measurement) @property def extra_state_attributes(self): diff --git a/custom_components/wemportal/switch.py b/custom_components/wemportal/switch.py index 79d841c..03e5231 100644 --- a/custom_components/wemportal/switch.py +++ b/custom_components/wemportal/switch.py @@ -11,7 +11,7 @@ from .const import _LOGGER, DOMAIN from . import get_wemportal_unique_id -from .utils import fix_unit_of_measurement +from .utils import (fix_value_and_uom) async def async_setup_platform( @@ -74,6 +74,9 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + + val, uom = fix_value_and_uom(entity_data["value"], entity_data["unit"]) + self._last_updated = None self._config_entry = config_entry self._device_id = device_id @@ -84,13 +87,15 @@ def __init__( self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_unit = fix_unit_of_measurement(entity_data["unit"]) - self._attr_state = entity_data["value"] + self._attr_unit = uom + self._attr_state = val self._attr_should_poll = False self._attr_device_class = "switch" # type: ignore self._module_index = entity_data["ModuleIndex"] self._module_type = entity_data["ModuleType"] + _LOGGER.debug(f'Init switch: {self._attr_name}: "{self._attr_state}" [{self._attr_unit}]') + @property def device_info(self) -> DeviceInfo: """Get device information.""" @@ -156,6 +161,9 @@ def _handle_coordinator_update(self) -> None: self._attr_state = "on" # type: ignore else: self._attr_state = "off" # type: ignore + + _LOGGER.debug(f'Update switch: {self._attr_name}: "{self._attr_state}" [{self._attr_unit}]') + except KeyError: self._attr_state = None _LOGGER.warning("Can't find %s", self._attr_unique_id) diff --git a/custom_components/wemportal/utils.py b/custom_components/wemportal/utils.py index 8a805a9..bad7b30 100644 --- a/custom_components/wemportal/utils.py +++ b/custom_components/wemportal/utils.py @@ -1,10 +1,79 @@ -def fix_unit_of_measurement(uom: str) -> str: - """Fix the unit of measurement. WEM Portal uses "kW (W)" for "W" and "kW (W)h" for "Wh". Also fix the casing of some units.""" +from homeassistant.components.sensor import (SensorDeviceClass, SensorStateClass) +from homeassistant.const import (UnitOfEnergy, UnitOfPower, UnitOfVolumeFlowRate, UnitOfTemperature, UnitOfTime, UnitOfFrequency) - return { - "w": "W", - "kw": "kW", - "kwh": "kWh", - "kw (w)": "W", - "kw (w)h": "Wh", +def fix_value_and_uom(val, uom): + """ + Translate WEM specific values and units of measurement to Home Assistent. + + This function returns: + * a valid Home Assistant UoM if it can be mapped (see: ) + * an empty string as UoM if the value is a number without any indication of its unit of measurement (e.g., a counter) + * None as UoM if the value is a string without any indication of its unit of measurement (e.g., a status text) + """ + + # special case: volume flow rate + if isinstance(val, str) and val.endswith("m3/h"): + return float(val.replace("m3/h", "")), UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR + + # special case: no unit of measurement + if uom is None: + return val, None + + # special case: empty string for unit of measurement for a number + if uom == "": + try: + # return the value as a float and an empty string as unit of measurement + return float(val), "" + except ValueError: + # if the conversion to a float fails, return the value as a string and no unit of measurement + return val, None + + # remap the unit of measurement from WEM to a Home Assistant UoM + # see: + uom = { + "": None, + "w": UnitOfPower.WATT, + "kw (w)": UnitOfPower.WATT, + "kw": UnitOfPower.KILO_WATT, + "kwh": UnitOfEnergy.KILO_WATT_HOUR, + "kw (w)h": UnitOfEnergy.WATT_HOUR, + "h": UnitOfTime.HOURS, + "hz": UnitOfFrequency.HERTZ, + "m3/h": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR }.get(uom.lower(), uom) + return val, uom + +def uom_to_device_class(uom): + """Return the device_class of this unit of measurement, if any.""" + + # see: + return { + "%": SensorDeviceClass.POWER_FACTOR, + UnitOfTemperature.CELSIUS: SensorDeviceClass.TEMPERATURE, + UnitOfTemperature.KELVIN: SensorDeviceClass.TEMPERATURE, + UnitOfEnergy.KILO_WATT_HOUR: SensorDeviceClass.ENERGY, + UnitOfEnergy.WATT_HOUR: SensorDeviceClass.ENERGY, + UnitOfPower.KILO_WATT: SensorDeviceClass.POWER, + UnitOfPower.WATT: SensorDeviceClass.POWER, + UnitOfTime.HOURS: SensorDeviceClass.DURATION, + UnitOfFrequency.HERTZ: SensorDeviceClass.FREQUENCY, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: SensorDeviceClass.VOLUME_FLOW_RATE, + }.get(uom, None) # return None if no device class is available + +def uom_to_state_class(uom): + """Return the state class of this unit of measurement, if any.""" + + # see: + return { + "": SensorStateClass.MEASUREMENT, + "%": SensorStateClass.MEASUREMENT, + UnitOfTemperature.CELSIUS: SensorStateClass.MEASUREMENT, + UnitOfTemperature.KELVIN: SensorStateClass.MEASUREMENT, + UnitOfEnergy.KILO_WATT_HOUR: SensorStateClass.TOTAL_INCREASING, + UnitOfEnergy.WATT_HOUR: SensorStateClass.TOTAL_INCREASING, + UnitOfPower.KILO_WATT: SensorStateClass.MEASUREMENT, + UnitOfPower.WATT: SensorStateClass.MEASUREMENT, + UnitOfTime.HOURS: SensorStateClass.TOTAL_INCREASING, + UnitOfFrequency.HERTZ: SensorStateClass.MEASUREMENT, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: SensorStateClass.MEASUREMENT, + }.get(uom, None) # return None if no state class is available