diff --git a/_docs/setup_account.md b/_docs/setup_account.md index 8510e5f1..bd299d8a 100644 --- a/_docs/setup_account.md +++ b/_docs/setup_account.md @@ -12,6 +12,7 @@ - [Government Pricing Caps](#government-pricing-caps) - [Services](#services) - [Service octopus\_energy.purge\_invalid\_external\_statistic\_ids](#service-octopus_energypurge_invalid_external_statistic_ids) + - [Service octopus\_energy.refresh\_previous\_consumption\_data](#service-octopus_energyrefresh_previous_consumption_data) Setup is done entirely via the [integration UI](https://my.home-assistant.io/redirect/config_flow_start/?domain=octopus_energy). @@ -70,4 +71,13 @@ There has been inconsistencies across tariffs on whether government pricing caps ### Service octopus_energy.purge_invalid_external_statistic_ids -Service for removing all external statistics that are associated with meters that don't have an active tariff. This is useful if you've been using the integration and obtained new smart meters. \ No newline at end of file +Service for removing all external statistics that are associated with meters that don't have an active tariff. This is useful if you've been using the integration and obtained new smart meters. + +### Service octopus_energy.refresh_previous_consumption_data + +Service for refreshing the consumption/cost information for a given previous consumption entity. This is useful when you've just installed the integration and want old data brought in or a previous consumption sensor fails to import (e.g. data becomes available outside of the configured offset). The service will raise a notification when the refreshing starts and finishes. + +This service is only available for the following sensors + +- `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_accumulative_consumption` +- `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_accumulative_consumption` \ No newline at end of file diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py index 4c087897..dfa96fad 100644 --- a/custom_components/octopus_energy/binary_sensor.py +++ b/custom_components/octopus_energy/binary_sensor.py @@ -1,4 +1,3 @@ -from datetime import timedelta import logging import voluptuous as vol diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index 03de34b1..bca64c45 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -67,6 +67,7 @@ # However it looks like there are some tariffs that don't fit this mold REGEX_TARIFF_PARTS = "^((?P[A-Z])-(?P[0-9A-Z]+)-)?(?P[A-Z0-9-]+)-(?P[A-Z])$" REGEX_OFFSET_PARTS = "^(-)?([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$" +REGEX_DATE = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" DATA_SCHEMA_ACCOUNT = vol.Schema({ vol.Required(CONFIG_MAIN_API_KEY): str, diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py index 926c673c..97d8373b 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py @@ -1,9 +1,6 @@ import logging from datetime import datetime -from ..statistics.consumption import async_import_external_statistics_from_consumption - -from homeassistant.core import HomeAssistant -from homeassistant.util.dt import (utcnow) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -23,16 +20,21 @@ from .base import (OctopusEnergyElectricitySensor) +from ..statistics.consumption import async_import_external_statistics_from_consumption, get_electricity_consumption_statistic_unique_id +from ..statistics.refresh import async_refresh_previous_electricity_consumption_data +from ..api_client import OctopusEnergyApiClient + _LOGGER = logging.getLogger(__name__) class OctopusEnergyPreviousAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity reading.""" - def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, coordinator, tariff_code, meter, point): """Init sensor.""" CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + self._client = client self._state = None self._tariff_code = tariff_code self._last_reset = None @@ -120,7 +122,7 @@ async def async_update(self): await async_import_external_statistics_from_consumption( self._hass, - f"electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption", + get_electricity_consumption_statistic_unique_id(self._serial_number, self._mpan, self._is_export), self.name, consumption_and_cost["charges"], rate_data, @@ -160,4 +162,19 @@ async def async_added_to_hass(self): if x == "last_reset": self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") - _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') + + @callback + async def async_refresh_previous_consumption_data(self, start_date): + """Update sensors config""" + + await async_refresh_previous_electricity_consumption_data( + self._hass, + self._client, + start_date, + self._mpan, + self._serial_number, + self._tariff_code, + self._is_smart_meter, + self._is_export + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py index 677a801a..eebf6eb1 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py @@ -17,7 +17,7 @@ from .base import (OctopusEnergyElectricitySensor) -from ..statistics.cost import async_import_external_statistics_from_cost +from ..statistics.cost import async_import_external_statistics_from_cost, get_electricity_cost_statistic_unique_id _LOGGER = logging.getLogger(__name__) @@ -116,7 +116,7 @@ async def async_update(self): _LOGGER.debug(f"Calculated previous electricity consumption cost for '{self._mpan}/{self._serial_number}'...") await async_import_external_statistics_from_cost( self._hass, - f"electricity_{self._serial_number}_{self._mpan}_previous_accumulative_cost", + get_electricity_cost_statistic_unique_id(self._serial_number, self._mpan, self._is_export), self.name, consumption_and_cost["charges"], rate_data, diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py index 91825347..eb6f56c7 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py @@ -1,8 +1,7 @@ import logging from datetime import datetime -from ..statistics.consumption import async_import_external_statistics_from_consumption -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) @@ -21,17 +20,22 @@ from .base import (OctopusEnergyGasSensor) +from ..api_client import OctopusEnergyApiClient +from ..statistics.consumption import async_import_external_statistics_from_consumption, get_gas_consumption_statistic_unique_id +from ..statistics.refresh import async_refresh_previous_gas_consumption_data + _LOGGER = logging.getLogger(__name__) class OctopusEnergyPreviousAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous days accumulative gas reading.""" - def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass + self._client = client self._tariff_code = tariff_code self._native_consumption_units = meter["consumption_units"] self._state = None @@ -122,7 +126,7 @@ async def async_update(self): await async_import_external_statistics_from_consumption( self._hass, - f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption", + get_gas_consumption_statistic_unique_id(self._serial_number, self._mprn), self.name, consumption_and_cost["charges"], rate_data, @@ -165,4 +169,19 @@ async def async_added_to_hass(self): if x == "last_reset": self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") - _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') + + @callback + async def async_refresh_previous_consumption_data(self, start_date): + """Update sensors config""" + + await async_refresh_previous_gas_consumption_data( + self._hass, + self._client, + start_date, + self._mprn, + self._serial_number, + self._tariff_code, + self._native_consumption_units, + self._calorific_value, + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost.py b/custom_components/octopus_energy/gas/previous_accumulative_cost.py index d076c70c..95d0c071 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost.py @@ -17,7 +17,7 @@ from .base import (OctopusEnergyGasSensor) -from ..statistics.cost import async_import_external_statistics_from_cost +from ..statistics.cost import async_import_external_statistics_from_cost, get_gas_cost_statistic_unique_id _LOGGER = logging.getLogger(__name__) @@ -121,7 +121,7 @@ async def async_update(self): await async_import_external_statistics_from_cost( self._hass, - f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost", + get_gas_cost_statistic_unique_id(self._serial_number, self._mprn), self.name, consumption_and_cost["charges"], rate_data, diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index fb695bbf..9133f715 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,6 +1,9 @@ +import voluptuous as vol import logging + from homeassistant.util.dt import (utcnow) from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform from .electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption from .electricity.current_accumulative_consumption import OctopusEnergyCurrentAccumulativeElectricityConsumption @@ -73,6 +76,20 @@ async def async_setup_entry(hass, entry, async_add_entities): if CONFIG_MAIN_API_KEY in entry.data: await async_setup_default_sensors(hass, entry, async_add_entities) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "refresh_previous_consumption_data", + vol.All( + vol.Schema( + { + vol.Optional("start_time"): str, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + "async_refresh_previous_consumption_data", + ) + async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_entities): config = dict(entry.data) @@ -125,7 +142,7 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti meter["is_smart_meter"], previous_electricity_consumption_days_offset ) - entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(hass, client, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) @@ -192,7 +209,7 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti None, previous_gas_consumption_days_offset ) - entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(hass, client, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasConsumptionKwh(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasCost(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasCostOverride(hass, previous_consumption_coordinator, client, gas_tariff_code, meter, point, calorific_value)) diff --git a/custom_components/octopus_energy/services.yaml b/custom_components/octopus_energy/services.yaml index dcb1e817..55a90dc6 100644 --- a/custom_components/octopus_energy/services.yaml +++ b/custom_components/octopus_energy/services.yaml @@ -30,7 +30,20 @@ update_target_config: The optional offset to apply to the target rate when it starts selector: text: - purge_invalid_external_statistic_ids: name: Purge invalid external statistics - description: Removes external statistics for all meters that don't have an active tariff \ No newline at end of file + description: Removes external statistics for all meters that don't have an active tariff +refresh_previous_consumption_data: + name: Refresh previous consumption data + description: Refreshes the previous consumption data for a given entity from a given date. + target: + entity: + integration: octopus_energy + domain: sensor + fields: + start_date: + name: Date + description: The date the data should be loaded from. + required: true + selector: + date: \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/consumption.py b/custom_components/octopus_energy/statistics/consumption.py index f9089e10..e2689aaa 100644 --- a/custom_components/octopus_energy/statistics/consumption.py +++ b/custom_components/octopus_energy/statistics/consumption.py @@ -11,6 +11,18 @@ _LOGGER = logging.getLogger(__name__) +def get_electricity_consumption_statistic_unique_id(serial_number: str, mpan: str, is_export: bool): + return f"electricity_{serial_number}_{mpan}{'_export' if is_export == True else ''}_previous_accumulative_consumption" + +def get_electricity_consumption_statistic_name(serial_number: str, mpan: str, is_export: bool): + return f"Electricity {serial_number} {mpan}{' Export' if is_export == True else ''} Previous Accumulative Consumption" + +def get_gas_consumption_statistic_unique_id(serial_number: str, mpan: str): + return f"gas_{serial_number}_{mpan}_previous_accumulative_consumption" + +def get_gas_consumption_statistic_name(serial_number: str, mpan: str): + return f"Gas {serial_number} {mpan} Previous Accumulative Consumption" + async def async_import_external_statistics_from_consumption( hass: HomeAssistant, unique_id: str, diff --git a/custom_components/octopus_energy/statistics/cost.py b/custom_components/octopus_energy/statistics/cost.py index 61913162..6cf1e27c 100644 --- a/custom_components/octopus_energy/statistics/cost.py +++ b/custom_components/octopus_energy/statistics/cost.py @@ -11,6 +11,18 @@ _LOGGER = logging.getLogger(__name__) +def get_electricity_cost_statistic_unique_id(serial_number: str, mpan: str, is_export: bool): + return f"electricity_{serial_number}_{mpan}{'_export' if is_export == True else ''}_previous_accumulative_cost" + +def get_electricity_cost_statistic_name(serial_number: str, mpan: str, is_export: bool): + return f"Electricity {serial_number} {mpan}{' Export' if is_export == True else ''} Previous Accumulative Cost" + +def get_gas_cost_statistic_unique_id(serial_number: str, mpan: str): + return f"gas_{serial_number}_{mpan}_previous_accumulative_cost" + +def get_gas_cost_statistic_name(serial_number: str, mpan: str): + return f"Gas {serial_number} {mpan} Previous Accumulative Cost" + async def async_import_external_statistics_from_cost( hass: HomeAssistant, unique_id: str, diff --git a/custom_components/octopus_energy/statistics/refresh.py b/custom_components/octopus_energy/statistics/refresh.py new file mode 100644 index 00000000..9283cad8 --- /dev/null +++ b/custom_components/octopus_energy/statistics/refresh.py @@ -0,0 +1,159 @@ +from datetime import timedelta +import re +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components import persistent_notification +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + VOLUME_CUBIC_METERS +) + +from homeassistant.util.dt import (now, parse_datetime) + +from ..api_client import OctopusEnergyApiClient +from ..const import REGEX_DATE +from .consumption import async_import_external_statistics_from_consumption, get_electricity_consumption_statistic_name, get_electricity_consumption_statistic_unique_id, get_gas_consumption_statistic_name, get_gas_consumption_statistic_unique_id +from .cost import async_import_external_statistics_from_cost, get_electricity_cost_statistic_name, get_electricity_cost_statistic_unique_id, get_gas_cost_statistic_name, get_gas_cost_statistic_unique_id +from ..electricity import calculate_electricity_consumption_and_cost +from ..gas import calculate_gas_consumption_and_cost + +async def async_refresh_previous_electricity_consumption_data( + hass: HomeAssistant, + client: OctopusEnergyApiClient, + start_date: str, + mpan: str, + serial_number: str, + tariff_code: str, + is_smart_meter: bool, + is_export: bool +): + # Inputs from automations can include quotes, so remove these + trimmed_date = start_date.strip('\"') + matches = re.search(REGEX_DATE, trimmed_date) + if matches is None: + raise vol.Invalid(f"Date '{trimmed_date}' must match format of YYYY-MM-DD.") + + persistent_notification.async_create( + hass, + title="Consumption data refreshing started", + message=f"Consumption data from {start_date} for electricity meter {serial_number}/{mpan} has started" + ) + + period_from = parse_datetime(f'{trimmed_date}T00:00:00Z') + while period_from < now(): + period_to = period_from + timedelta(days=2) + + consumption_data = await client.async_get_electricity_consumption(mpan, serial_number, period_from, period_to) + rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rates, + 0, + None, + tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if consumption_and_cost is not None: + await async_import_external_statistics_from_consumption( + hass, + get_electricity_consumption_statistic_unique_id(serial_number, mpan, is_export), + get_electricity_consumption_statistic_name(serial_number, mpan, is_export), + consumption_and_cost["charges"], + rates, + ENERGY_KILO_WATT_HOUR, + "consumption" + ) + + await async_import_external_statistics_from_cost( + hass, + get_electricity_cost_statistic_unique_id(serial_number, mpan, is_export), + get_electricity_cost_statistic_name(serial_number, mpan, is_export), + consumption_and_cost["charges"], + rates, + "GBP", + "consumption" + ) + + period_from = period_to + + persistent_notification.async_create( + hass, + title="Consumption data refreshed", + message=f"Consumption data from {start_date} for electricity meter {serial_number}/{mpan} has finished" + ) + +async def async_refresh_previous_gas_consumption_data( + hass: HomeAssistant, + client: OctopusEnergyApiClient, + start_date: str, + mprn: str, + serial_number: str, + tariff_code: str, + consumption_units: str, + calorific_value: float +): + # Inputs from automations can include quotes, so remove these + trimmed_date = start_date.strip('\"') + matches = re.search(REGEX_DATE, trimmed_date) + if matches is None: + raise vol.Invalid(f"Date '{trimmed_date}' must match format of YYYY-MM-DD.") + + persistent_notification.async_create( + hass, + title="Consumption data refreshing started", + message=f"Consumption data from {start_date} for gas meter {serial_number}/{mprn} has started" + ) + + period_from = parse_datetime(f'{trimmed_date}T00:00:00Z') + while period_from < now(): + period_to = period_from + timedelta(days=2) + + consumption_data = await client.async_get_gas_consumption(mprn, serial_number, period_from, period_to) + rates = await client.async_get_gas_rates(tariff_code, period_from, period_to) + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rates, + 0, + None, + tariff_code, + consumption_units, + calorific_value, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if consumption_and_cost is not None: + await async_import_external_statistics_from_consumption( + hass, + get_gas_consumption_statistic_unique_id(serial_number, mprn), + get_gas_consumption_statistic_name(serial_number, mprn), + consumption_and_cost["charges"], + rates, + VOLUME_CUBIC_METERS, + "consumption_m3", + False + ) + + await async_import_external_statistics_from_cost( + hass, + get_gas_cost_statistic_unique_id(serial_number, mprn), + get_gas_cost_statistic_name(serial_number, mprn), + consumption_and_cost["charges"], + rates, + "GBP", + "consumption_kwh", + False + ) + + period_from = period_to + + persistent_notification.async_create( + hass, + title="Consumption data refreshed", + message=f"Consumption data from {start_date} for gas meter {serial_number}/{mprn} has finished" + ) \ No newline at end of file