Skip to content

Commit

Permalink
Merge pull request #107 from erikkastelec/master
Browse files Browse the repository at this point in the history
  • Loading branch information
erikkastelec authored Nov 5, 2024
2 parents b22fd65 + 4aeb07b commit b2d218a
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 39 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.
Go to `Settings > Devices&Services `, find WEM Portal and click on `three dots` at the bottom of the card. Click on `Enable debug logging`.
2 changes: 1 addition & 1 deletion custom_components/wemportal/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
28 changes: 23 additions & 5 deletions custom_components/wemportal/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -77,15 +81,17 @@ 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"]
self._attr_should_poll = False
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(
Expand Down Expand Up @@ -127,16 +133,28 @@ 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)
_LOGGER.debug("Sensor data %s", self.coordinator.data)

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."""
Expand Down
51 changes: 23 additions & 28 deletions custom_components/wemportal/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
13 changes: 11 additions & 2 deletions custom_components/wemportal/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
79 changes: 79 additions & 0 deletions custom_components/wemportal/utils.py
Original file line number Diff line number Diff line change
@@ -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: <https://github.com/home-assistant/core/blob/dev/homeassistant/const.py>)
* 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: <https://github.com/home-assistant/core/blob/dev/homeassistant/const.py>
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: <https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes>
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: <https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes>
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
2 changes: 1 addition & 1 deletion custom_components/wemportal/wemportalapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]] = {}
Expand Down

0 comments on commit b2d218a

Please sign in to comment.