From 4650ff4c587a05e6788166de1982f100a15f87a8 Mon Sep 17 00:00:00 2001 From: Lewis Juggins <873275+lwis@users.noreply.github.com> Date: Thu, 26 Dec 2024 19:58:04 +0000 Subject: [PATCH] Add lifetime sensors --- .../octopus_energy/api_client/__init__.py | 32 +++--- .../octopus_energy/api_client/heat_pump.py | 4 +- .../heat_pump/sensor_lifetime_energy_input.py | 108 ++++++++++++++++++ .../heat_pump/sensor_lifetime_heat_output.py | 108 ++++++++++++++++++ .../heat_pump/sensor_lifetime_scop.py | 93 +++++++++++++++ .../heat_pump/sensor_live_cop.py | 6 +- custom_components/octopus_energy/sensor.py | 24 ++++ 7 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 custom_components/octopus_energy/heat_pump/sensor_lifetime_energy_input.py create mode 100644 custom_components/octopus_energy/heat_pump/sensor_lifetime_heat_output.py create mode 100644 custom_components/octopus_energy/heat_pump/sensor_lifetime_scop.py diff --git a/custom_components/octopus_energy/api_client/__init__.py b/custom_components/octopus_energy/api_client/__init__.py index d6d384af..d8ba505c 100644 --- a/custom_components/octopus_energy/api_client/__init__.py +++ b/custom_components/octopus_energy/api_client/__init__.py @@ -532,6 +532,18 @@ }} readAt }} + octoHeatPumpLifetimePerformance(euid: "{euid}") {{ + seasonalCoefficientOfPerformance + heatOutput {{ + value + unit + }} + energyInput {{ + value + unit + }} + readAt + }} }} ''' @@ -551,23 +563,6 @@ }} ''' -heat_pump_lifetime_performance = ''' -query {{ - octoHeatPumpLifetimePerformance(euid: "{euid}") {{ - seasonalCoefficientOfPerformance - heatOutput {{ - value - unit - }} - energyInput {{ - value - unit - }} - }} - readAt -}} -''' - user_agent_value = "bottlecapdave-ha-octopus-energy" @@ -875,7 +870,8 @@ async def async_get_heat_pump_configuration_and_status(self, account_id: str, eu and "data" in response and "octoHeatPumpControllerConfiguration" in response["data"] and "octoHeatPumpControllerStatus" in response["data"] - and "octoHeatPumpLivePerformance" in response["data"]): + and "octoHeatPumpLivePerformance" in response["data"] + and "octoHeatPumpLifetimePerformance" in response["data"]): return HeatPumpResponse.parse_obj(response["data"]) return None diff --git a/custom_components/octopus_energy/api_client/heat_pump.py b/custom_components/octopus_energy/api_client/heat_pump.py index 0cdcf796..b5a4e0c5 100644 --- a/custom_components/octopus_energy/api_client/heat_pump.py +++ b/custom_components/octopus_energy/api_client/heat_pump.py @@ -136,6 +136,7 @@ class OctoHeatPumpLifetimePerformance(BaseModel): energyInput: ValueAndUnit readAt: str + class OctoHeatPumpTimeRangedPerformance(BaseModel): coefficientOfPerformance: str energyOutput: ValueAndUnit @@ -145,4 +146,5 @@ class OctoHeatPumpTimeRangedPerformance(BaseModel): class HeatPumpResponse(BaseModel): octoHeatPumpControllerStatus: OctoHeatPumpControllerStatus octoHeatPumpControllerConfiguration: OctoHeatPumpControllerConfiguration - octoHeatPumpLivePerformance: OctoHeatPumpLivePerformance \ No newline at end of file + octoHeatPumpLivePerformance: OctoHeatPumpLivePerformance + octoHeatPumpLifetimePerformance: OctoHeatPumpLifetimePerformance \ No newline at end of file diff --git a/custom_components/octopus_energy/heat_pump/sensor_lifetime_energy_input.py b/custom_components/octopus_energy/heat_pump/sensor_lifetime_energy_input.py new file mode 100644 index 00000000..9a3fa057 --- /dev/null +++ b/custom_components/octopus_energy/heat_pump/sensor_lifetime_energy_input.py @@ -0,0 +1,108 @@ +from datetime import datetime +import logging +from typing import List + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfEnergy +) +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 (BaseOctopusEnergyHeatPumpSensor) +from ..utils.attributes import dict_to_typed_dict +from ..api_client.heat_pump import HeatPump +from ..coordinators.heat_pump_configuration_and_status import HeatPumpCoordinatorResult + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyHeatPumpSensorLifetimeEnergyInput(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensor, RestoreSensor): + """Sensor for displaying the lifetime energy input of a heat pump.""" + + def __init__(self, hass: HomeAssistant, coordinator, heat_pump_id: str, heat_pump: HeatPump): + """Init sensor.""" + # Pass coordinator to base class + CoordinatorEntity.__init__(self, coordinator) + BaseOctopusEnergyHeatPumpSensor.__init__(self, hass, heat_pump_id, heat_pump) + + 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}_lifetime_energy_input" + + @property + def name(self): + """Name of the sensor.""" + return f"Lifetime Energy Input ({self._heat_pump_id})" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL_INCREASING + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:flash" + + @property + def native_unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return UnitOfEnergy.KILO_WATT_HOUR + + @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 live CoP for the heat pump.""" + 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.octoHeatPumpLifetimePerformance is not None): + _LOGGER.debug(f"Updating OctopusEnergyHeatPumpSensorLifetimeEnergyInput for '{self._heat_pump_id}'") + + self._state = float(result.data.octoHeatPumpLifetimePerformance.energyInput.value) + self._attributes["read_at"] = datetime.fromisoformat(result.data.octoHeatPumpLifetimePerformance.readAt) + 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, []) + + _LOGGER.debug(f'Restored OctopusEnergyHeatPumpSensorLifetimeEnergyInput state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/heat_pump/sensor_lifetime_heat_output.py b/custom_components/octopus_energy/heat_pump/sensor_lifetime_heat_output.py new file mode 100644 index 00000000..8e99d2e3 --- /dev/null +++ b/custom_components/octopus_energy/heat_pump/sensor_lifetime_heat_output.py @@ -0,0 +1,108 @@ +from datetime import datetime +import logging +from typing import List + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfEnergy +) +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 (BaseOctopusEnergyHeatPumpSensor) +from ..utils.attributes import dict_to_typed_dict +from ..api_client.heat_pump import HeatPump +from ..coordinators.heat_pump_configuration_and_status import HeatPumpCoordinatorResult + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyHeatPumpSensorLifetimeHeatOutput(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensor, RestoreSensor): + """Sensor for displaying the lifetime heat output of a heat pump.""" + + def __init__(self, hass: HomeAssistant, coordinator, heat_pump_id: str, heat_pump: HeatPump): + """Init sensor.""" + # Pass coordinator to base class + CoordinatorEntity.__init__(self, coordinator) + BaseOctopusEnergyHeatPumpSensor.__init__(self, hass, heat_pump_id, heat_pump) + + 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}_lifetime_heat_output" + + @property + def name(self): + """Name of the sensor.""" + return f"Lifetime Heat Output ({self._heat_pump_id})" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL_INCREASING + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:flash" + + @property + def native_unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return UnitOfEnergy.KILO_WATT_HOUR + + @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 lifeime heat output for the heat pump.""" + 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.octoHeatPumpLifetimePerformance is not None): + _LOGGER.debug(f"Updating OctopusEnergyHeatPumpSensorLifetimeHeatOutput for '{self._heat_pump_id}'") + + self._state = float(result.data.octoHeatPumpLifetimePerformance.heatOutput.value) + self._attributes["read_at"] = datetime.fromisoformat(result.data.octoHeatPumpLifetimePerformance.readAt) + 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, []) + + _LOGGER.debug(f'Restored OctopusEnergyHeatPumpSensorLifetimeHeatOutput state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/heat_pump/sensor_lifetime_scop.py b/custom_components/octopus_energy/heat_pump/sensor_lifetime_scop.py new file mode 100644 index 00000000..9a316d28 --- /dev/null +++ b/custom_components/octopus_energy/heat_pump/sensor_lifetime_scop.py @@ -0,0 +1,93 @@ +from datetime import datetime +import logging +from typing import List + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfPower +) +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 (BaseOctopusEnergyHeatPumpSensor) +from ..utils.attributes import dict_to_typed_dict +from ..api_client.heat_pump import HeatPump +from ..coordinators.heat_pump_configuration_and_status import HeatPumpCoordinatorResult + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyHeatPumpSensorLifetimeSCoP(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensor, RestoreSensor): + """Sensor for displaying the lifetime SCoP of a heat pump.""" + + def __init__(self, hass: HomeAssistant, coordinator, heat_pump_id: str, heat_pump: HeatPump): + """Init sensor.""" + # Pass coordinator to base class + CoordinatorEntity.__init__(self, coordinator) + BaseOctopusEnergyHeatPumpSensor.__init__(self, hass, heat_pump_id, heat_pump) + + 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}_lifetime_scop" + + @property + def name(self): + """Name of the sensor.""" + return f"Lifetime SCoP ({self._heat_pump_id})" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.MEASUREMENT + + @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 lifetime SCoP for the heat pump.""" + 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.octoHeatPumpLifetimePerformance is not None): + _LOGGER.debug(f"Updating OctopusEnergyHeatPumpSensorLifetimeSCoP for '{self._heat_pump_id}'") + + self._state = float(result.data.octoHeatPumpLifetimePerformance.seasonalCoefficientOfPerformance) + self._attributes["read_at"] = datetime.fromisoformat(result.data.octoHeatPumpLifetimePerformance.readAt) + 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, []) + + _LOGGER.debug(f'Restored OctopusEnergyHeatPumpSensorLifetimeSCoP state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/heat_pump/sensor_live_cop.py b/custom_components/octopus_energy/heat_pump/sensor_live_cop.py index 0987382d..0aa811bc 100644 --- a/custom_components/octopus_energy/heat_pump/sensor_live_cop.py +++ b/custom_components/octopus_energy/heat_pump/sensor_live_cop.py @@ -73,7 +73,11 @@ def _handle_coordinator_update(self) -> None: and result.data.octoHeatPumpLivePerformance is not None): _LOGGER.debug(f"Updating OctopusEnergyHeatPumpSensorLiveCoP for '{self._heat_pump_id}'") - self._state = float(result.data.octoHeatPumpLivePerformance.coefficientOfPerformance) + self._state = 0 + # Only update the CoP if active + if float(result.data.octoHeatPumpLivePerformance.powerInput.value) != 0: + self._state = float(result.data.octoHeatPumpLivePerformance.coefficientOfPerformance) + self._attributes["read_at"] = datetime.fromisoformat(result.data.octoHeatPumpLivePerformance.readAt) self._last_updated = current diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index 4aeb399f..79337890 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -63,6 +63,9 @@ from .heat_pump.sensor_live_heat_output import OctopusEnergyHeatPumpSensorLiveHeatOutput from .heat_pump.sensor_live_cop import OctopusEnergyHeatPumpSensorLiveCoP from .heat_pump.sensor_live_outdoor_temperature import OctopusEnergyHeatPumpSensorLiveOutdoorTemperature +from .heat_pump.sensor_lifetime_scop import OctopusEnergyHeatPumpSensorLifetimeSCoP +from .heat_pump.sensor_lifetime_heat_output import OctopusEnergyHeatPumpSensorLifetimeHeatOutput +from .heat_pump.sensor_lifetime_energy_input import OctopusEnergyHeatPumpSensorLifetimeEnergyInput from .api_client.intelligent_device import IntelligentDevice from .utils.debug_overrides import async_get_account_debug_override, async_get_meter_debug_override @@ -619,6 +622,27 @@ def setup_heat_pump_sensors(hass: HomeAssistant, heat_pump_id: str, heat_pump_re heat_pump_response.octoHeatPumpControllerConfiguration.heatPump )) + entities.append(OctopusEnergyHeatPumpSensorLifetimeEnergyInput( + hass, + coordinator, + heat_pump_id, + heat_pump_response.octoHeatPumpControllerConfiguration.heatPump + )) + + entities.append(OctopusEnergyHeatPumpSensorLifetimeHeatOutput( + hass, + coordinator, + heat_pump_id, + heat_pump_response.octoHeatPumpControllerConfiguration.heatPump + )) + + entities.append(OctopusEnergyHeatPumpSensorLifetimeSCoP( + hass, + coordinator, + heat_pump_id, + heat_pump_response.octoHeatPumpControllerConfiguration.heatPump + )) + return entities async def async_setup_cost_sensors(hass: HomeAssistant, entry, config, async_add_entities):