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`. 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" ], diff --git a/custom_components/wemportal/number.py b/custom_components/wemportal/number.py index 49adabe..b52f279 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_value_and_uom, uom_to_device_class) async def async_setup_platform( @@ -68,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 @@ -77,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 = 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"] @@ -86,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( @@ -127,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) @@ -137,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 8aaa2dc..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,6 +12,7 @@ from .const import _LOGGER, DOMAIN from . import get_wemportal_unique_id +from .utils import (fix_value_and_uom, uom_to_device_class, uom_to_state_class) async def async_setup_platform( @@ -72,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 @@ -81,10 +81,12 @@ 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_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.""" @@ -113,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) @@ -125,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 a7547a5..03e5231 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_value_and_uom) async def async_setup_platform( @@ -73,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 @@ -83,13 +87,15 @@ def __init__( self._parameter_id = entity_data["ParameterID"] self._attr_icon = entity_data["icon"] - self._attr_unit = 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.""" @@ -155,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 new file mode 100644 index 0000000..bad7b30 --- /dev/null +++ b/custom_components/wemportal/utils.py @@ -0,0 +1,79 @@ +from homeassistant.components.sensor import (SensorDeviceClass, SensorStateClass) +from homeassistant.const import (UnitOfEnergy, UnitOfPower, UnitOfVolumeFlowRate, UnitOfTemperature, UnitOfTime, UnitOfFrequency) + +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 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"]] = {}