Skip to content

Commit

Permalink
feat: Added climate control for heat pump zone (3 hours dev time)
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Dec 7, 2024
1 parent b479c89 commit 5e72161
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 2 deletions.
2 changes: 1 addition & 1 deletion custom_components/octopus_energy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
REPAIR_UNKNOWN_INTELLIGENT_PROVIDER
)

ACCOUNT_PLATFORMS = ["sensor", "binary_sensor", "number", "switch", "text", "time", "event"]
ACCOUNT_PLATFORMS = ["sensor", "binary_sensor", "number", "switch", "text", "time", "event", "climate"]
TARGET_RATE_PLATFORMS = ["binary_sensor"]
COST_TRACKER_PLATFORMS = ["sensor"]
TARIFF_COMPARISON_PLATFORMS = ["sensor"]
Expand Down
79 changes: 79 additions & 0 deletions custom_components/octopus_energy/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import logging

from custom_components.octopus_energy.api_client import OctopusEnergyApiClient
from homeassistant.core import HomeAssistant

from .api_client.heat_pump import HeatPumpResponse
from .heat_pump import get_mock_heat_pump_id
from .heat_pump.zone import OctopusEnergyHeatPumpZone
from .utils.debug_overrides import async_get_account_debug_override

from .const import (
CONFIG_ACCOUNT_ID,
DATA_ACCOUNT,
DATA_CLIENT,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY,
DOMAIN,

CONFIG_MAIN_API_KEY
)

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass, entry, async_add_entities):
"""Setup sensors based on our entry"""

config = dict(entry.data)

if entry.options:
config.update(entry.options)

if CONFIG_MAIN_API_KEY in config:
await async_setup_default_sensors(hass, config, async_add_entities)

return True

async def async_setup_default_sensors(hass, config, async_add_entities):
_LOGGER.debug('Setting up default sensors')

entities = []

account_id = config[CONFIG_ACCOUNT_ID]
client = hass.data[DOMAIN][account_id][DATA_CLIENT]
account_debug_override = await async_get_account_debug_override(hass, account_id)
account_result = hass.data[DOMAIN][account_id][DATA_ACCOUNT]
account_info = account_result.account if account_result is not None else None

mock_heat_pump = account_debug_override.mock_heat_pump if account_debug_override is not None else False
if mock_heat_pump:
heat_pump_id = get_mock_heat_pump_id()
key = DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY.format(heat_pump_id)
coordinator = hass.data[DOMAIN][account_id][DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR.format(heat_pump_id)]
entities.extend(setup_heat_pump_sensors(hass, client, heat_pump_id, hass.data[DOMAIN][account_id][key].data, coordinator, mock_heat_pump))
elif "heat_pump_ids" in account_info:
for heat_pump_id in account_info["heat_pump_ids"]:
key = DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY.format(heat_pump_id)
coordinator = hass.data[DOMAIN][account_id][DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR.format(heat_pump_id)]
entities.extend(setup_heat_pump_sensors(hass, client, heat_pump_id, hass.data[DOMAIN][account_id][key].data, coordinator, mock_heat_pump))

async_add_entities(entities)

def setup_heat_pump_sensors(hass: HomeAssistant, client: OctopusEnergyApiClient, heat_pump_id: str, heat_pump_response: HeatPumpResponse, coordinator, mock_heat_pump: bool):

entities = []

if heat_pump_response is not None and heat_pump_response.octoHeatPumpControllerConfiguration is not None:
for zone in heat_pump_response.octoHeatPumpControllerConfiguration.zones:
if zone.configuration is not None:
entities.append(OctopusEnergyHeatPumpZone(
hass,
coordinator,
client,
heat_pump_id,
heat_pump_response.octoHeatPumpControllerConfiguration.heatPump,
zone,
mock_heat_pump
))

return entities
164 changes: 164 additions & 0 deletions custom_components/octopus_energy/heat_pump/zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from datetime import datetime
import logging
from typing import List

from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
PRECISION_TENTHS,
ATTR_TEMPERATURE
)
from homeassistant.core import HomeAssistant, callback

from homeassistant.util.dt import (now)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity
)
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
PRESET_NONE,
PRESET_BOOST,
)
from homeassistant.helpers.restore_state import RestoreEntity

from .base import (BaseOctopusEnergyHeatPumpSensor)
from ..utils.attributes import dict_to_typed_dict
from ..api_client.heat_pump import ConfigurationZone, HeatPump, Sensor, Zone
from ..coordinators.heatpump_configuration_and_status import HeatPumpCoordinatorResult
from ..api_client import OctopusEnergyApiClient

_LOGGER = logging.getLogger(__name__)

class OctopusEnergyHeatPumpZone(CoordinatorEntity, BaseOctopusEnergyHeatPumpSensor, ClimateEntity):
"""Sensor for interacting with a heat pump zone."""

_attr_supported_features = (
ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
)

_attr_min_temp = 5
_attr_max_temp = 50
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO]
_attr_hvac_mode = None
_attr_preset_modes = [PRESET_NONE, PRESET_BOOST]
_attr_preset_mode = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = PRECISION_TENTHS

def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, heat_pump_id: str, heat_pump: HeatPump, zone: ConfigurationZone, is_mocked: bool):
"""Init sensor."""
self._zone = zone
self._client = client
self._is_mocked = is_mocked

# self._attributes = {
# "type": zone.configuration.zoneType,
# "calling_for_heat": zone.configuration.callForHeat,
# "is_enabled": zone.configuration.enabled
# }

# Pass coordinator to base class
CoordinatorEntity.__init__(self, coordinator)
BaseOctopusEnergyHeatPumpSensor.__init__(self, hass, heat_pump_id, heat_pump, "climate")

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._zone.configuration.code}"

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

@callback
def _handle_coordinator_update(self) -> None:
"""Retrieve the previous rate."""

# self._attributes = {
# "type": self._zone.configuration.zoneType,
# "calling_for_heat": self._zone.configuration.callForHeat,
# "is_enabled": self._zone.configuration.enabled
# }

# 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.zones):
_LOGGER.debug(f"Updating OctopusEnergyHeatPumpZone for '{self._heat_pump_id}/{self._zone.configuration.code}'")

zones: List[Zone] = result.data.octoHeatPumpControllerStatus.zones
for zone in zones:
if zone.zone == self._zone.configuration.code and zone.telemetry is not None:

if zone.telemetry.mode == "ON":
self._attr_hvac_mode = HVACMode.HEAT
self._attr_preset_mode = PRESET_NONE
elif zone.telemetry.mode == "OFF":
self._attr_hvac_mode = HVACMode.OFF
self._attr_preset_mode = PRESET_NONE
elif zone.telemetry.mode == "AUTO":
self._attr_hvac_mode = HVACMode.AUTO
self._attr_preset_mode = PRESET_NONE
elif zone.telemetry.mode == "ON":
self._attr_preset_mode = PRESET_BOOST
else:
raise Exception(f"Unexpected heat pump mode detected: {zone.telemetry.mode}")

self._attr_target_temperature = zone.telemetry.setpointInCelsius

if (result.data.octoHeatPumpControllerStatus.sensors and self._zone.configuration.primarySensor):
sensors: List[Sensor] = result.data.octoHeatPumpControllerStatus.sensors
for sensor in sensors:
if sensor.code == self._zone.configuration.primarySensor and sensor.telemetry is not None:
self._attr_current_temperature = sensor.telemetry.temperatureInCelsius

self._attributes["retrieved_at"] = datetime.strptime(zone.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_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
# await self._client.async_set_heat_pump_mode()
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()

async def async_turn_on(self):
"""Turn the entity on."""
# await self._client.async_set_heat_pump_mode()
self._attr_hvac_mode = HVACMode.HEAT
self.async_write_ha_state()

async def async_turn_off(self):
"""Turn the entity off."""
# await self._client.async_set_heat_pump_mode()
self._attr_hvac_mode = HVACMode.OFF
self.async_write_ha_state()

async def async_set_preset_mode(self, preset_mode):
"""Set new target preset mode."""
# await self._client.async_set_heat_pump_mode()
self._attr_preset_mode = preset_mode
self.async_write_ha_state()

async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
temperature = kwargs[ATTR_TEMPERATURE]
# await self._client.async_set_heat_pump_mode()
self._attr_target_temperature = temperature
self.async_write_ha_state()
1 change: 0 additions & 1 deletion custom_components/octopus_energy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,6 @@ def setup_heat_pump_sensors(hass: HomeAssistant, heat_pump_id: str, heat_pump_re
sensor
))


return entities

async def async_setup_cost_sensors(hass: HomeAssistant, entry, config, async_add_entities):
Expand Down

0 comments on commit 5e72161

Please sign in to comment.