Skip to content

Commit

Permalink
feat: Added temperature sensors from heat pumps (2.5 hours dev time)
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Dec 6, 2024
1 parent a836213 commit c79e5b8
Show file tree
Hide file tree
Showing 14 changed files with 441 additions and 50 deletions.
42 changes: 34 additions & 8 deletions custom_components/octopus_energy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@
from .statistics import get_statistic_ids_to_remove
from .intelligent import get_intelligent_features, is_intelligent_product, mock_intelligent_device
from .config.rolling_target_rates import async_migrate_rolling_target_config
from .coordinators.heatpump_configuration_and_status import HeatPumpCoordinatorResult, async_setup_heat_pump_coordinator

from .config.main import async_migrate_main_config
from .config.target_rates import async_migrate_target_config
from .config.cost_tracker import async_migrate_cost_tracker_config
from .utils import get_active_tariff
from .utils.debug_overrides import DebugOverride, async_get_debug_override
from .utils.debug_overrides import MeterDebugOverride, async_get_account_debug_override, async_get_meter_debug_override
from .utils.error import api_exception_to_string
from .storage.account import async_load_cached_account, async_save_cached_account
from .storage.intelligent_device import async_load_cached_intelligent_device, async_save_cached_intelligent_device


from .heat_pump import get_mock_heat_pump_id, mock_heat_pump_status_and_configuration
from .storage.heat_pump import async_load_cached_heat_pump, async_save_cached_heat_pump

from .const import (
CONFIG_FAVOUR_DIRECT_DEBIT_RATES,
CONFIG_KIND,
Expand All @@ -44,6 +49,7 @@
CONFIG_MAIN_HOME_PRO_API_KEY,
CONFIG_MAIN_OLD_API_KEY,
CONFIG_VERSION,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY,
DATA_HOME_PRO_CLIENT,
DATA_INTELLIGENT_DEVICE,
DATA_INTELLIGENT_MPAN,
Expand Down Expand Up @@ -342,15 +348,13 @@ async def async_setup_dependencies(hass, config):
has_intelligent_tariff = False
intelligent_mpan = None
intelligent_serial_number = None
debug_override: DebugOverride | None = None
account_debug_override = await async_get_account_debug_override(hass, account_id)
for point in account_info["electricity_meter_points"]:
mpan = point["mpan"]
electricity_tariff = get_active_tariff(now, point["agreements"])

for meter in point["meters"]:
serial_number = meter["serial_number"]

debug_override = await async_get_debug_override(hass, mpan, serial_number)

if electricity_tariff is not None:
if meter["is_export"] == False:
Expand All @@ -364,7 +368,7 @@ async def async_setup_dependencies(hass, config):
if electricity_device is not None:
device_registry.async_remove_device(electricity_device.id)

should_mock_intelligent_data = debug_override.mock_intelligent_controls if debug_override is not None else False
should_mock_intelligent_data = account_debug_override.mock_intelligent_controls if account_debug_override is not None else False
if should_mock_intelligent_data:
# Pick the first meter if we're mocking our intelligent data
for point in account_info["electricity_meter_points"]:
Expand Down Expand Up @@ -425,16 +429,38 @@ async def async_setup_dependencies(hass, config):
serial_number = meter["serial_number"]
is_export_meter = meter["is_export"]
is_smart_meter = meter["is_smart_meter"]
override = await async_get_debug_override(hass, mpan, serial_number)
override = await async_get_meter_debug_override(hass, mpan, serial_number)
tariff_override = override.tariff if override is not None else None
planned_dispatches_supported = intelligent_features.planned_dispatches_supported if intelligent_features is not None else True
await async_setup_electricity_rates_coordinator(hass, account_id, mpan, serial_number, is_smart_meter, is_export_meter, planned_dispatches_supported, tariff_override)

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()
await async_setup_heat_pump_coordinator(hass, account_id, heat_pump_id, True)

key = DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY.format(heat_pump_id)
try:
hass.data[DOMAIN][account_id][key] = HeatPumpCoordinatorResult(now, 1, heat_pump_id, mock_heat_pump_status_and_configuration())
await async_save_cached_heat_pump(hass, account_id, heat_pump_id, hass.data[DOMAIN][account_id][key].data)
except:
hass.data[DOMAIN][account_id][key] = HeatPumpCoordinatorResult(now, 1, heat_pump_id, await async_load_cached_heat_pump(hass, account_id, heat_pump_id))
elif "heat_pump_ids" in account_info:
for heat_pump_id in account_info["heat_pump_ids"]:
await async_setup_heat_pump_coordinator(hass, account_id, heat_pump_id, False)

key = DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY.format(heat_pump_id)
try:
hass.data[DOMAIN][account_id][key] = HeatPumpCoordinatorResult(now, 1, heat_pump_id, await client.async_get_heatpump_configuration_and_status(account_id, heat_pump_id))
await async_save_cached_heat_pump(hass, account_id, heat_pump_id, hass.data[DOMAIN][account_id][key].data)
except:
hass.data[DOMAIN][account_id][key] = HeatPumpCoordinatorResult(now, 1, heat_pump_id, await async_load_cached_heat_pump(hass, account_id, heat_pump_id))

await async_setup_account_info_coordinator(hass, account_id)

await async_setup_intelligent_dispatches_coordinator(hass, account_id, debug_override.mock_intelligent_controls if debug_override is not None else False)
await async_setup_intelligent_dispatches_coordinator(hass, account_id, account_debug_override.mock_intelligent_controls if account_debug_override is not None else False)

await async_setup_intelligent_settings_coordinator(hass, account_id, intelligent_device.id if intelligent_device is not None else None, debug_override.mock_intelligent_controls if debug_override is not None else False)
await async_setup_intelligent_settings_coordinator(hass, account_id, intelligent_device.id if intelligent_device is not None else None, account_debug_override.mock_intelligent_controls if account_debug_override is not None else False)

await async_setup_saving_sessions_coordinators(hass, account_id)

Expand Down
5 changes: 5 additions & 0 deletions custom_components/octopus_energy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
REFRESH_RATE_IN_MINUTES_OCTOPLUS_POINTS = 60
REFRESH_RATE_IN_MINUTES_GREENNESS_FORECAST = 180
REFRESH_RATE_IN_MINUTES_HOME_PRO_CONSUMPTION = 0.17
REFRESH_RATE_IN_MINUTES_HEAT_PUMP = 1

CONFIG_VERSION = 4

Expand Down Expand Up @@ -140,12 +141,16 @@
DATA_FREE_ELECTRICITY_SESSIONS = "FREE_ELECTRICITY_SESSIONS"
DATA_FREE_ELECTRICITY_SESSIONS_COORDINATOR = "FREE_ELECTRICITY_SESSIONS_COORDINATOR"

DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY = "HEAT_PUMP_CONFIGURATION_AND_STATUS_{}"
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR = "HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR_{}"

DATA_SAVING_SESSIONS_FORCE_UPDATE = "SAVING_SESSIONS_FORCE_UPDATE"

STORAGE_COMPLETED_DISPATCHES_NAME = "octopus_energy.{}-completed-intelligent-dispatches.json"
STORAGE_ELECTRICITY_TARIFF_OVERRIDE_NAME = "octopus_energy.{}-{}-tariff-override.json"
STORAGE_TARIFF_CACHE_NAME = "octopus_energy.tariff-{}.json"
STORAGE_METER_DEBUG_OVERRIDE_NAME = "octopus_energy.{}-{}-override.json"
STORAGE_ACCOUNT_DEBUG_OVERRIDE_NAME = "octopus_energy.{}-override.json"

INTELLIGENT_SOURCE_SMART_CHARGE = "smart-charge"
INTELLIGENT_SOURCE_BUMP_CHARGE = "bump-charge"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logging
from datetime import datetime, timedelta

from homeassistant.util.dt import (utcnow)
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator
)

from ..const import (
COORDINATOR_REFRESH_IN_SECONDS,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY,
DOMAIN,

DATA_CLIENT,
DATA_ACCOUNT,
DATA_ACCOUNT_COORDINATOR,
REFRESH_RATE_IN_MINUTES_HEAT_PUMP,
)

from ..api_client import ApiException, OctopusEnergyApiClient
from ..api_client.heat_pump import HeatPumpResponse
from . import BaseCoordinatorResult

from ..heat_pump import mock_heat_pump_status_and_configuration

_LOGGER = logging.getLogger(__name__)

class HeatPumpCoordinatorResult(BaseCoordinatorResult):
euid: str
data: HeatPumpResponse

def __init__(self, last_evaluated: datetime, request_attempts: int, euid: str, data: HeatPumpResponse, last_error: Exception | None = None):
super().__init__(last_evaluated, request_attempts, REFRESH_RATE_IN_MINUTES_HEAT_PUMP, None, last_error)
self.euid = euid
self.data = data

async def async_refresh_heat_pump_configuration_and_status(
current: datetime,
client: OctopusEnergyApiClient,
account_info,
euid: str,
existing_heat_pump_result: HeatPumpCoordinatorResult | None,
is_mocked: bool
):
if (account_info is not None):
account_id = account_info["id"]
if (existing_heat_pump_result is None or current >= existing_heat_pump_result.next_refresh):
status_and_configuration = None
raised_exception = None

if is_mocked:
status_and_configuration = mock_heat_pump_status_and_configuration()
elif euid is not None:
try:
status_and_configuration = await client.async_get_heatpump_configuration_and_status(account_id, euid)
_LOGGER.debug(f'Heat Pump config and status retrieved for account {account_id} and device {euid}')
except Exception as e:
if isinstance(e, ApiException) == False:
raise

raised_exception = e
_LOGGER.debug(f'Failed to retrieve heat pump configuration and status for account {account_id} and device {euid}')

if status_and_configuration is not None:
return HeatPumpCoordinatorResult(current, 1, euid, status_and_configuration)

result = None
if (existing_heat_pump_result is not None):
result = HeatPumpCoordinatorResult(
existing_heat_pump_result.last_evaluated,
existing_heat_pump_result.request_attempts + 1,
euid,
existing_heat_pump_result.data,
last_error=raised_exception
)

if (result.request_attempts == 2):
_LOGGER.warning(f"Failed to retrieve new heat pump configuration and status - using cached settings. See diagnostics sensor for more information.")
else:
# We want to force into our fallback mode
result = HeatPumpCoordinatorResult(current - timedelta(minutes=REFRESH_RATE_IN_MINUTES_HEAT_PUMP), 2, euid, None, last_error=raised_exception)
_LOGGER.warning(f"Failed to retrieve new heat pump configuration and status. See diagnostics sensor for more information.")

return result

return existing_heat_pump_result

async def async_setup_heat_pump_coordinator(hass, account_id: str, euid: str, mock_heat_pump_data: bool):
key = DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY.format(euid)
# Reset data as we might have new information
hass.data[DOMAIN][account_id][key] = None

async def async_update_heat_pump_data():
"""Fetch data from API endpoint."""
# Request our account data to be refreshed
account_coordinator = hass.data[DOMAIN][account_id][DATA_ACCOUNT_COORDINATOR]
if account_coordinator is not None:
await account_coordinator.async_request_refresh()

current = utcnow()
client: OctopusEnergyApiClient = hass.data[DOMAIN][account_id][DATA_CLIENT]
account_result = hass.data[DOMAIN][account_id][DATA_ACCOUNT]
account_info = account_result.account if account_result is not None else None

hass.data[DOMAIN][account_id][key] = await async_refresh_heat_pump_configuration_and_status(
current,
client,
account_info,
euid,
hass.data[DOMAIN][account_id][key] if key in hass.data[DOMAIN][account_id] else None,
mock_heat_pump_data
)

return hass.data[DOMAIN][account_id][key]

hass.data[DOMAIN][account_id][DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR.format(euid)] = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"heat_pump_{account_id}",
update_method=async_update_heat_pump_data,
update_interval=timedelta(seconds=COORDINATOR_REFRESH_IN_SECONDS),
always_update=True
)
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def async_refresh_intelligent_dispatches(
raise

raised_exception=e
_LOGGER.debug('Failed to retrieve intelligent dispatches for account {account_id}')
_LOGGER.debug(f'Failed to retrieve intelligent dispatches for account {account_id}')

if is_data_mocked:
dispatches = mock_intelligent_dispatches()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def async_refresh_intelligent_settings(
raise

raised_exception = e
_LOGGER.debug('Failed to retrieve intelligent settings for account {account_id}')
_LOGGER.debug(f'Failed to retrieve intelligent settings for account {account_id}')

if is_settings_mocked:
settings = mock_intelligent_settings()
Expand Down
29 changes: 16 additions & 13 deletions custom_components/octopus_energy/heat_pump/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import random
from ..api_client.heat_pump import HeatPumpResponse

def get_mock_heat_pump_id():
return "ABC"

def mock_heat_pump_status_and_configuration():
data = {
Expand All @@ -12,7 +15,7 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.952000+00:00"
},
"telemetry": {
"temperatureInCelsius": 57.4,
"temperatureInCelsius": 57 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
}
Expand All @@ -24,7 +27,7 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.952000+00:00"
},
"telemetry": {
"temperatureInCelsius": -273.1,
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
}
Expand All @@ -36,7 +39,7 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
},
"telemetry": {
"temperatureInCelsius": -273.1,
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
}
Expand All @@ -48,7 +51,7 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
},
"telemetry": {
"temperatureInCelsius": -273.1,
"temperatureInCelsius": -273 + (random.randrange(1, 20) * 0.1),
"humidityPercentage": None,
"retrievedAt": "2024-12-01T10:04:51.588000+00:00"
}
Expand All @@ -60,8 +63,8 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.953000+00:00"
},
"telemetry": {
"temperatureInCelsius": 19.4,
"humidityPercentage": 57,
"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"
}
},
Expand All @@ -72,8 +75,8 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.955000+00:00"
},
"telemetry": {
"temperatureInCelsius": 22.4,
"humidityPercentage": 54,
"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"
}
},
Expand All @@ -84,8 +87,8 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.956000+00:00"
},
"telemetry": {
"temperatureInCelsius": 22.3,
"humidityPercentage": 60,
"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"
}
},
Expand All @@ -96,8 +99,8 @@ def mock_heat_pump_status_and_configuration():
"retrievedAt": "2024-12-01T10:04:54.957000+00:00"
},
"telemetry": {
"temperatureInCelsius": 22.7,
"humidityPercentage": 46,
"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"
}
}
Expand Down Expand Up @@ -162,7 +165,7 @@ def mock_heat_pump_status_and_configuration():
"minWaterSetpoint": 40,
"heatingFlowTemperature": {
"currentTemperature": {
"value": "70",
"value": "56",
"unit": "DEGREES_CELSIUS"
},
"allowableRange": {
Expand Down
29 changes: 29 additions & 0 deletions custom_components/octopus_energy/heat_pump/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from homeassistant.core import HomeAssistant

from homeassistant.helpers.entity import generate_entity_id, DeviceInfo

from ..const import (
DOMAIN,
)
from ..api_client.heat_pump import HeatPump

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

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

self._attributes = {
}

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

self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"heat_pump_{heat_pump.serialNumber}")},
name=f"Heat Pump ({heat_pump.serialNumber})",
connections=set(),
model=heat_pump.model,
hw_version=heat_pump.hardwareVersion
)
Loading

0 comments on commit c79e5b8

Please sign in to comment.