Skip to content

Commit

Permalink
Merge pull request #32 from jyavenard/HighPrecisionEnergy
Browse files Browse the repository at this point in the history
High precision energy sensors ; resync and improve
  • Loading branch information
gtdiehl authored Sep 5, 2021
2 parents 0dbe610 + f8593e1 commit 395e4e7
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 78 deletions.
15 changes: 3 additions & 12 deletions custom_components/iotawatt/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,15 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
update_interval=timedelta(seconds=30),
)

self._last_run = None
self._refresh_requested = False
self._last_run: datetime | None = None

def update_last_run(self, last_run: datetime):
def update_last_run(self, last_run: datetime) -> None:
"""Notify coordinator of a sensor last update time."""
# We want to fetch the data from the iotawatt since HA was last shutdown.
# We retrieve from the sensor last updated.
# This method is called from each sensor upon their state being restored.
if self._last_run is None or last_run > self._last_run:
self._last_run = last_run # type: ignore

async def request_refresh(self):
"""Request a refresh of the iotawatt sensors."""
if self._refresh_requested:
return
self._refresh_requested = True
await self.async_request_refresh()
self._last_run = last_run

async def _async_update_data(self):
"""Fetch sensors from IoTaWatt device."""
Expand All @@ -72,5 +64,4 @@ async def _async_update_data(self):

await self.api.update(lastUpdate=self._last_run)
self._last_run = None
self._refresh_requested = False
return self.api.getSensors()
5 changes: 3 additions & 2 deletions custom_components/iotawatt/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"iotawattpy==0.1.0"
],
"codeowners": [
"@gtdiehl"
"@gtdiehl",
"@jyavenard"
],
"version": "0.1.1",
"version": "0.2.1",
"iot_class": "local_polling"
}
157 changes: 93 additions & 64 deletions custom_components/iotawatt/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
def _create_entity(key: str) -> IotaWattSensor:
"""Create a sensor entity."""
created.add(key)
data = coordinator.data["sensors"][key]
description = ENTITY_DESCRIPTION_KEY_MAP.get(
data.getUnit(), IotaWattSensorEntityDescription("base_sensor")
)
if data.getUnit() == "WattHours" and not data.getFromStart():
return IotaWattAccumulatingSensor(
coordinator=coordinator, key=key, entity_description=description
)

return IotaWattSensor(
coordinator=coordinator,
key=key,
entity_description=ENTITY_DESCRIPTION_KEY_MAP.get(
coordinator.data["sensors"][key].getUnit(),
IotaWattSensorEntityDescription("base_sensor"),
),
entity_description=description,
)

async_add_entities(_create_entity(key) for key in coordinator.data["sensors"])
Expand All @@ -153,45 +159,40 @@ def new_data_received():
coordinator.async_add_listener(new_data_received)


class IotaWattSensor(update_coordinator.CoordinatorEntity, RestoreEntity, SensorEntity):
class IotaWattSensor(update_coordinator.CoordinatorEntity, SensorEntity):
"""Defines a IoTaWatt Energy Sensor."""

entity_description: IotaWattSensorEntityDescription
_attr_force_update = True
coordinator: IotawattUpdater

def __init__(
self,
coordinator,
key,
coordinator: IotawattUpdater,
key: str,
entity_description: IotaWattSensorEntityDescription,
):
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator=coordinator)

self._key = key
data = self._sensor_data
self._accumulating = data.getUnit() == "WattHours" and not data.getFromStart()
self._accumulated_value = None
if data.getType() == "Input":
unit = data.getUnit() + self._name_suffix
self._attr_unique_id = (
f"{data.hub_mac_address}-input-{data.getChannel()}-{unit}"
f"{data.hub_mac_address}-input-{data.getChannel()}-{data.getUnit()}"
)
elif data.getType() == "Output":
self._attr_unique_id = f"{data.hub_mac_address}-output-{data.getSourceName()}"
self.entity_description = entity_description

@property
def _sensor_data(self) -> Sensor:
"""Return sensor data."""
return self.coordinator.data["sensors"][self._key]

@property
def _name_suffix(self) -> str:
return ".accumulated" if self._accumulating else ""

@property
def name(self) -> str | None:
"""Return name of the entity."""
return self._sensor_data.getSourceName() + self._name_suffix
return self._sensor_data.getName()

@property
def device_info(self) -> entity.DeviceInfo | None:
Expand All @@ -213,68 +214,96 @@ def _handle_coordinator_update(self) -> None:
else:
self.hass.async_create_task(self.async_remove())
return

if self._accumulating:
assert (
self._accumulated_value is not None
), "async_added_to_hass must have been called first"
self._accumulated_value += float(self._sensor_data.getValue())

super()._handle_coordinator_update()

@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, str]:
"""Return the extra state attributes of the entity."""
data = self._sensor_data
attrs = {"type": data.getType()}
if attrs["type"] == "Input":
attrs["channel"] = data.getChannel()
if self._accumulating:
attrs[
ATTR_LAST_UPDATE
] = self.coordinator.api.getLastUpdateTime().isoformat()

return attrs

@property
def last_reset(self):
"""Return the time when the sensor was last reset, if any."""
if self.state_class == STATE_CLASS_MEASUREMENT:
return None

if self._accumulating:
return datetime.min # an accumulating sensor never reset.
last_reset = self._sensor_data.getBegin()
if last_reset is None:
return None
return dt.parse_datetime(last_reset)

async def async_added_to_hass(self):
"""Load the last known state value of the entity if the accumulated type."""
await super().async_added_to_hass()
if self._accumulating:
state = await self.async_get_last_state()
self._accumulated_value = 0.0
if state:
try:
self._accumulated_value = float(state.state)
if ATTR_LAST_UPDATE in state.attributes:
self.coordinator.update_last_run(
dt.parse_datetime(state.attributes.get(ATTR_LAST_UPDATE))
)
except (ValueError) as err:
_LOGGER.warning("Could not restore last state: %s", err)
# Force a second update from the iotawatt to ensure that sensors are up to date.
await self.coordinator.request_refresh()

@property
def native_value(self) -> entity.StateType:
"""Return the state of the sensor."""
if func := self.entity_description.value:
return func(self._sensor_data.getValue())

if not self._accumulating:
return self._sensor_data.getValue()
return self._sensor_data.getValue()


class IotaWattAccumulatingSensor(IotaWattSensor, RestoreEntity):
"""Defines a IoTaWatt Accumulative Energy (High Accuracy) Sensor."""

def __init__(
self,
coordinator: IotawattUpdater,
key: str,
entity_description: IotaWattSensorEntityDescription,
) -> None:
"""Initialize the sensor."""

super().__init__(coordinator, key, entity_description)

self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
if self._attr_unique_id is not None:
self._attr_unique_id += ".accumulated"

self._accumulated_value: float | None = None

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
assert (
self._accumulated_value is not None
), "async_added_to_hass must have been called first"
self._accumulated_value += float(self._sensor_data.getValue())

super()._handle_coordinator_update()

@property
def native_value(self) -> entity.StateType:
"""Return the state of the sensor."""
if self._accumulated_value is None:
return None
return round(self._accumulated_value, 1)

async def async_added_to_hass(self) -> None:
"""Load the last known state value of the entity if the accumulated type."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
self._accumulated_value = 0.0
if state:
try:
# Previous value could be `unknown` if the connection didn't originally
# complete.
self._accumulated_value = float(state.state)
except (ValueError) as err:
_LOGGER.warning("Could not restore last state: %s", err)
else:
if ATTR_LAST_UPDATE in state.attributes:
last_run = dt.parse_datetime(state.attributes[ATTR_LAST_UPDATE])
if last_run is not None:
self.coordinator.update_last_run(last_run)
# Force a second update from the iotawatt to ensure that sensors are up to date.
await self.coordinator.async_request_refresh()

@property
def name(self) -> str | None:
"""Return name of the entity."""
return f"{self._sensor_data.getSourceName()} Accumulated"

@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the extra state attributes of the entity."""
attrs = super().extra_state_attributes

assert (
self.coordinator.api is not None
and self.coordinator.api.getLastUpdateTime() is not None
)
attrs[ATTR_LAST_UPDATE] = self.coordinator.api.getLastUpdateTime().isoformat()

return attrs
16 changes: 16 additions & 0 deletions custom_components/iotawatt/translations/el.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"error": {
"cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2",
"unknown": "\u0391\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
},
"step": {
"auth": {
"data": {
"password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2",
"username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7"
}
}
}
}
}
22 changes: 22 additions & 0 deletions custom_components/iotawatt/translations/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"config": {
"error": {
"cannot_connect": "La conexi\u00f3n ha fallado",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"step": {
"auth": {
"data": {
"password": "Contrase\u00f1a",
"username": "Nombre de usuario"
}
},
"user": {
"data": {
"host": "Anfitri\u00f3n"
}
}
}
}
}
22 changes: 22 additions & 0 deletions custom_components/iotawatt/translations/he.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"config": {
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"step": {
"auth": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
},
"user": {
"data": {
"host": "\u05de\u05d0\u05e8\u05d7"
}
}
}
}
}
23 changes: 23 additions & 0 deletions custom_components/iotawatt/translations/hu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {
"error": {
"cannot_connect": "Nem siker\u00fclt csatlakozni",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"unknown": "V\u00e1ratlan hiba"
},
"step": {
"auth": {
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
},
"description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd kattintson a K\u00fcld\u00e9s gombra."
},
"user": {
"data": {
"host": "Gazdag\u00e9p"
}
}
}
}
}
23 changes: 23 additions & 0 deletions custom_components/iotawatt/translations/zh-Hans.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {
"error": {
"cannot_connect": "\u8fde\u63a5\u5931\u8d25",
"invalid_auth": "\u65e0\u6548\u51ed\u8bc1",
"unknown": "\u672a\u77e5\u9519\u8bef"
},
"step": {
"auth": {
"data": {
"password": "\u5bc6\u7801",
"username": "\u7528\u6237\u540d"
},
"description": "IoTawatt \u8bbe\u5907\u9700\u8981\u8eab\u4efd\u9a8c\u8bc1\u3002\u8bf7\u8f93\u5165\u7528\u6237\u540d\u548c\u5bc6\u7801\uff0c\u7136\u540e\u5355\u51fb\u63d0\u4ea4\u6309\u94ae\u3002"
},
"user": {
"data": {
"host": "\u4e3b\u673a\u5730\u5740"
}
}
}
}
}

0 comments on commit 395e4e7

Please sign in to comment.