diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index b5bfea07..cba5b2da 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -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"] diff --git a/custom_components/octopus_energy/climate.py b/custom_components/octopus_energy/climate.py new file mode 100644 index 00000000..cf36e34a --- /dev/null +++ b/custom_components/octopus_energy/climate.py @@ -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 \ No newline at end of file diff --git a/custom_components/octopus_energy/heat_pump/zone.py b/custom_components/octopus_energy/heat_pump/zone.py new file mode 100644 index 00000000..fd11ad2e --- /dev/null +++ b/custom_components/octopus_energy/heat_pump/zone.py @@ -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() \ No newline at end of file diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index 2218822b..28b7dbb6 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -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):