Skip to content

Commit

Permalink
feat: Added heat pump humidity sensor (1 hour dev time)
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Dec 7, 2024
1 parent c79e5b8 commit b479c89
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 33 deletions.
43 changes: 24 additions & 19 deletions custom_components/octopus_energy/heat_pump/__init__.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,76 @@
from datetime import timedelta
import random

from homeassistant.util.dt import (utcnow)

from ..api_client.heat_pump import HeatPumpResponse

def get_mock_heat_pump_id():
return "ABC"

def mock_heat_pump_status_and_configuration():
now = utcnow()
data = {
"octoHeatPumpControllerStatus": {
"sensors": [
{
"code": "ADC1",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.952000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": 57 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "ADC2",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.952000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "ADC3",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "ADC4",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "SENSOR01",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": 18 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": 57 + (random.randrange(1, 20) * 0.1),
"retrievedAt": "2024-12-01T10:03:15.615000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
Expand All @@ -77,31 +82,31 @@ def mock_heat_pump_status_and_configuration():
"telemetry": {
"temperatureInCelsius": 22 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": 54 + (random.randrange(1, 20) * 0.1),
"retrievedAt": "2024-12-01T10:03:54.876000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "SENSOR03",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.956000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": 22 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": 60 + (random.randrange(1, 20) * 0.1),
"retrievedAt": "2024-12-01T10:04:27.571000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
"code": "SENSOR04",
"connectivity": {
"online": True,
"retrievedAt": "2024-12-01T10:04:54.957000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
},
"telemetry": {
"temperatureInCelsius": 22 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": 46 + (random.randrange(1, 20) * 0.1),
"retrievedAt": "2024-12-01T10:03:12.376000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
}
],
Expand All @@ -113,7 +118,7 @@ def mock_heat_pump_status_and_configuration():
"mode": "AUTO",
"relaySwitchedOn": False,
"heatDemand": False,
"retrievedAt": "2024-12-01T10:04:59.116000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
Expand All @@ -123,7 +128,7 @@ def mock_heat_pump_status_and_configuration():
"mode": "AUTO",
"relaySwitchedOn": False,
"heatDemand": False,
"retrievedAt": "2024-12-01T10:04:59.117000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
Expand All @@ -133,7 +138,7 @@ def mock_heat_pump_status_and_configuration():
"mode": "OFF",
"relaySwitchedOn": False,
"heatDemand": False,
"retrievedAt": "2024-12-01T10:04:59.118000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
},
{
Expand All @@ -143,7 +148,7 @@ def mock_heat_pump_status_and_configuration():
"mode": "OFF",
"relaySwitchedOn": False,
"heatDemand": False,
"retrievedAt": "2024-12-01T10:04:59.118000+00:00"
"retrievedAt": (now - timedelta(seconds=random.randrange(1, 120))).strftime("%Y-%m-%dT%H:%M:%S%z")
}
}
]
Expand Down
28 changes: 26 additions & 2 deletions custom_components/octopus_energy/heat_pump/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from ..const import (
DOMAIN,
)
from ..api_client.heat_pump import HeatPump
from ..api_client.heat_pump import HeatPump, SensorConfiguration

class OctopusEnergyHeatPumpSensor:
class BaseOctopusEnergyHeatPumpSensor:
_unrecorded_attributes = frozenset({"data_last_retrieved"})

def __init__(self, hass: HomeAssistant, heat_pump_id: str, heat_pump: HeatPump, entity_domain = "sensor"):
Expand All @@ -26,4 +26,28 @@ def __init__(self, hass: HomeAssistant, heat_pump_id: str, heat_pump: HeatPump,
connections=set(),
model=heat_pump.model,
hw_version=heat_pump.hardwareVersion
)

class BaseOctopusEnergyHeatPumpSensorSensor(BaseOctopusEnergyHeatPumpSensor):
_unrecorded_attributes = frozenset({"data_last_retrieved"})

def __init__(self, hass: HomeAssistant, heat_pump_id: str, heat_pump: HeatPump, sensor: SensorConfiguration, entity_domain = "sensor"):
"""Init sensor"""
self._heat_pump = heat_pump
self._heat_pump_id = heat_pump_id
self._sensor = sensor

self._attributes = {
"type": sensor.type,
"code": sensor.code
}

self.entity_id = generate_entity_id(entity_domain + ".{}", self.unique_id, hass=hass)

self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"heat_pump_sensor_{heat_pump.serialNumber}_{sensor.code}")},
name=f"Heat Pump Sensor ({sensor.code})",
connections=set(),
sw_version=sensor.firmwareVersion,
hw_version=sensor.type
)
116 changes: 116 additions & 0 deletions custom_components/octopus_energy/heat_pump/sensor_humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from datetime import datetime
import logging
from typing import List

from custom_components.octopus_energy.coordinators.heatpump_configuration_and_status import HeatPumpCoordinatorResult
from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN
)
from homeassistant.core import HomeAssistant, callback

from homeassistant.util.dt import (now)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity
)
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorStateClass,
)

from .base import (BaseOctopusEnergyHeatPumpSensorSensor)
from ..utils.attributes import dict_to_typed_dict
from ..api_client.heat_pump import HeatPump, Sensor, SensorConfiguration

_LOGGER = logging.getLogger(__name__)

class OctopusEnergyHeatPumpSensorHumidity(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensorSensor, RestoreSensor):
"""Sensor for displaying the humidity of a heat pump sensor."""

def __init__(self, hass: HomeAssistant, coordinator, heat_pump_id: str, heat_pump: HeatPump, sensor: SensorConfiguration):
"""Init sensor."""
# Pass coordinator to base class
CoordinatorEntity.__init__(self, coordinator)
BaseOctopusEnergyHeatPumpSensorSensor.__init__(self, hass, heat_pump_id, heat_pump, sensor)

self._state = None
self._last_updated = None

@property
def unique_id(self):
"""The id of the sensor."""
return f"octopus_energy_heat_pump_{self._heat_pump_id}_{self._sensor.code}_humidity"

@property
def name(self):
"""Name of the sensor."""
return f"Humidity ({self._sensor.displayName}) Heat Pump ({self._heat_pump_id})"

@property
def state_class(self):
"""The state class of sensor"""
return SensorStateClass.MEASUREMENT

@property
def device_class(self):
"""The type of sensor"""
return SensorDeviceClass.HUMIDITY

@property
def icon(self):
"""Icon of the sensor."""
return "mdi:thermometer"

@property
def native_unit_of_measurement(self):
"""Unit of measurement of the sensor."""
return "%"

@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
return self._attributes

@property
def native_value(self):
return self._state

@callback
def _handle_coordinator_update(self) -> None:
"""Retrieve the previous rate."""
# Find the previous rate. We only need to do this every half an hour
current = now()
result: HeatPumpCoordinatorResult = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None
if (result is not None and
result.data is not None and
result.data.octoHeatPumpControllerStatus is not None and
result.data.octoHeatPumpControllerStatus.sensors):
_LOGGER.debug(f"Updating OctopusEnergyHeatPumpSensorHumidity for '{self._heat_pump_id}/{self._sensor.code}'")

self._state = None
sensors: List[Sensor] = result.data.octoHeatPumpControllerStatus.sensors
for sensor in sensors:
if sensor.code == self._sensor.code and sensor.telemetry is not None:
self._state = sensor.telemetry.humidityPercentage
self._attributes["retrieved_at"] = datetime.strptime(sensor.telemetry.retrievedAt, "%Y-%m-%dT%H:%M:%S%z")

self._last_updated = current

self._attributes = dict_to_typed_dict(self._attributes)
super()._handle_coordinator_update()

async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
# If not None, we got an initial value.
await super().async_added_to_hass()
state = await self.async_get_last_state()

if state is not None and self._state is None:
self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state
self._attributes = dict_to_typed_dict(state.attributes, [])

self._attributes["type"] = self._sensor.type
self._attributes["code"] = self._sensor.code

_LOGGER.debug(f'Restored OctopusEnergyHeatPumpSensorHumidity state: {self._state}')
17 changes: 8 additions & 9 deletions custom_components/octopus_energy/heat_pump/sensor_temperature.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
import logging
from typing import List

Expand All @@ -19,30 +20,24 @@
SensorStateClass,
)

from .base import (OctopusEnergyHeatPumpSensor)
from .base import (BaseOctopusEnergyHeatPumpSensorSensor)
from ..utils.attributes import dict_to_typed_dict
from ..api_client.heat_pump import HeatPump, Sensor, SensorConfiguration

_LOGGER = logging.getLogger(__name__)

class OctopusEnergyHeatPumpSensorTemperature(CoordinatorEntity, OctopusEnergyHeatPumpSensor, RestoreSensor):
class OctopusEnergyHeatPumpSensorTemperature(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensorSensor, RestoreSensor):
"""Sensor for displaying the temperature of a heat pump sensor."""

def __init__(self, hass: HomeAssistant, coordinator, heat_pump_id: str, heat_pump: HeatPump, sensor: SensorConfiguration):
"""Init sensor."""
# Pass coordinator to base class
CoordinatorEntity.__init__(self, coordinator)

self._sensor = sensor

OctopusEnergyHeatPumpSensor.__init__(self, hass, heat_pump_id, heat_pump)
BaseOctopusEnergyHeatPumpSensorSensor.__init__(self, hass, heat_pump_id, heat_pump, sensor)

self._state = None
self._last_updated = None

self._attributes = {
}

@property
def unique_id(self):
"""The id of the sensor."""
Expand Down Expand Up @@ -99,6 +94,7 @@ def _handle_coordinator_update(self) -> None:
for sensor in sensors:
if sensor.code == self._sensor.code and sensor.telemetry is not None:
self._state = sensor.telemetry.temperatureInCelsius
self._attributes["retrieved_at"] = datetime.strptime(sensor.telemetry.retrievedAt, "%Y-%m-%dT%H:%M:%S%z")

self._last_updated = current

Expand All @@ -114,5 +110,8 @@ async def async_added_to_hass(self):
if state is not None and self._state is None:
self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state
self._attributes = dict_to_typed_dict(state.attributes, [])

self._attributes["type"] = self._sensor.type
self._attributes["code"] = self._sensor.code

_LOGGER.debug(f'Restored OctopusEnergyHeatPumpSensorTemperature state: {self._state}')
Loading

0 comments on commit b479c89

Please sign in to comment.