diff --git a/_docs/setup/cost_tracker.md b/_docs/setup/cost_tracker.md index fd141be6..f9aa5906 100644 --- a/_docs/setup/cost_tracker.md +++ b/_docs/setup/cost_tracker.md @@ -100,6 +100,132 @@ This is the total cost of the tracked entity at off peak rate for the current da | `is_tracking` | `boolean` | Determines if the tracker is currently tracking consumption/cost data | | `total_consumption` | `float` | The total consumption that has been tracked for the current day at off peak rate | +### Week cost sensor + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_week` + +This is the total cost of the tracked entity for the current week. This will reset on the configured day. + +This is in pounds and pence (e.g. 1.01 = £1.01). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `accumulated_data` | `array` | The collection of accumulated cost in daily increments | +| `total_consumption` | `float` | The total consumption that has been tracked for the current week | + +Each item within the `accumulated_data` has the following attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `start` | `datetime` | The date/time when the consumption starts | +| `end` | `datetime` | The date/time when the consumption ends | +| `consumption` | `float` | The consumption value of the specified period | +| `cost` | `float` | The cost of the consumption at the specified rate. This is in pounds and pence (e.g. 1.01 = £1.01) | + +### Week cost sensor (Peak) + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_week_peak` + +This is the total cost of the tracked entity at peak rate for the current week. This is in pounds and pence (e.g. 1.01 = £1.01). + +!!! note + This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `total_consumption` | `float` | The total consumption that has been tracked for the current week at peak rate | + +### Week cost sensor (Off Peak) + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_week_off_peak` + +This is the total cost of the tracked entity at off peak rate for the current week. This is in pounds and pence (e.g. 1.01 = £1.01). + +!!! note + This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `total_consumption` | `float` | The total consumption that has been tracked for the current week at off peak rate | + +### Month cost sensor + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_month` + +This is the total cost of the tracked entity for the current month. This will reset on the configured day. + +This is in pounds and pence (e.g. 1.01 = £1.01). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `accumulated_data` | `array` | The collection of accumulated cost in daily increments | +| `total_consumption` | `float` | The total consumption that has been tracked for the current month | + +Each item within the `accumulated_data` has the following attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `start` | `datetime` | The date/time when the consumption starts | +| `end` | `datetime` | The date/time when the consumption ends | +| `consumption` | `float` | The consumption value of the specified period | +| `cost` | `float` | The cost of the consumption at the specified rate. This is in pounds and pence (e.g. 1.01 = £1.01) | + +### Month cost sensor (Peak) + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_month_peak` + +This is the total cost of the tracked entity at peak rate for the current month. This is in pounds and pence (e.g. 1.01 = £1.01). + +!!! note + This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `total_consumption` | `float` | The total consumption that has been tracked for the current month at peak rate | + +### Month cost sensor (Off Peak) + +`sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}_month_off_peak` + +This is the total cost of the tracked entity at off peak rate for the current month. This is in pounds and pence (e.g. 1.01 = £1.01). + +!!! note + This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them). + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | `string` | The base name of the cost tracker (based on config) | +| `mpan` | `string` | The mpan of the meter that determines how the cost is calculated (based on config) | +| `target_entity_id` | `string` | The entity whose consumption data is being tracked (based on config) | +| `entity_accumulative_value` | `boolean` | Determines if the tracked entity has accumulative data (based on config) | +| `account_id` | `string` | The id of the account the cost tracker is for (based on config) | +| `total_consumption` | `float` | The total consumption that has been tracked for the current month at off peak rate | + ## Services There are services available associated with cost tracker sensors. Please review them in the [services doc](../services.md#octopus_energyupdate_cost_tracker). \ No newline at end of file diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index bf229161..7071fa4d 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -281,10 +281,12 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" unload_ok = False - if CONFIG_MAIN_API_KEY in entry.data: + if entry.data[CONFIG_KIND] == CONFIG_KIND_ACCOUNT: unload_ok = await hass.config_entries.async_unload_platforms(entry, ACCOUNT_PLATFORMS) - elif CONFIG_TARGET_NAME in entry.data: + elif entry.data[CONFIG_KIND] == CONFIG_KIND_TARGET_RATE: unload_ok = await hass.config_entries.async_unload_platforms(entry, TARGET_RATE_PLATFORMS) + elif entry.data[CONFIG_KIND] == CONFIG_KIND_COST_TRACKER: + unload_ok = await hass.config_entries.async_unload_platforms(entry, COST_TRACKER_PLATFORMS) return unload_ok diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index 6cc27bd5..d4c0c9bf 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -60,6 +60,8 @@ CONFIG_COST_MPAN = "mpan" CONFIG_COST_TARGET_ENTITY_ID = "target_entity_id" CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE = "entity_accumulative_value" +CONFIG_COST_WEEKDAY_RESET = "weekday_reset" +CONFIG_COST_MONTH_DAY_RESET = "month_day_reset" DATA_CONFIG = "CONFIG" DATA_ELECTRICITY_RATES_COORDINATOR_KEY = "ELECTRICITY_RATES_COORDINATOR_{}_{}" diff --git a/custom_components/octopus_energy/cost_tracker/__init__.py b/custom_components/octopus_energy/cost_tracker/__init__.py index 5d3d2054..8323fe00 100644 --- a/custom_components/octopus_energy/cost_tracker/__init__.py +++ b/custom_components/octopus_energy/cost_tracker/__init__.py @@ -72,4 +72,52 @@ def add_consumption(current: datetime, else: new_untracked_consumption_data = __add_consumption(new_untracked_consumption_data, target_start, target_end, value) - return CostTrackerResult(new_tracked_consumption_data, new_untracked_consumption_data) \ No newline at end of file + return CostTrackerResult(new_tracked_consumption_data, new_untracked_consumption_data) + +class AccumulativeCostTrackerResult: + accumulative_data: list + total_consumption: float + total_cost: float + + def __init__(self, accumulative_data: list, total_consumption: float, total_cost: float): + self.accumulative_data = accumulative_data + self.total_consumption = total_consumption + self.total_cost = total_cost + +def accumulate_cost(current: datetime, accumulative_data: list, new_cost: float, new_consumption: float) -> AccumulativeCostTrackerResult: + start_of_day = current.replace(hour=0, minute=0, second=0, microsecond=0) + + if accumulative_data is None: + accumulative_data = [] + + total_cost = 0 + total_consumption = 0 + is_day_added = False + new_accumulative_data = [] + for item in accumulative_data: + new_item = item.copy() + + if "start" in new_item and new_item["start"] == start_of_day: + new_item["cost"] = new_cost + new_item["consumption"] = new_consumption + is_day_added = True + + if "consumption" in new_item: + total_consumption += new_item["consumption"] + + if "cost" in new_item: + total_cost += new_item["cost"] + + new_accumulative_data.append(new_item) + + if is_day_added == False: + new_accumulative_data.append({ + "start": start_of_day, + "end": start_of_day + timedelta(days=1), + "cost": new_cost, + "consumption": new_consumption, + }) + + return AccumulativeCostTrackerResult(new_accumulative_data, total_consumption, total_cost) + + \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker.py b/custom_components/octopus_energy/cost_tracker/cost_tracker.py index 2b2b2044..8e7d2589 100644 --- a/custom_components/octopus_energy/cost_tracker/cost_tracker.py +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker.py @@ -43,19 +43,17 @@ class OctopusEnergyCostTrackerSensor(CoordinatorEntity, RestoreSensor): """Sensor for calculating the cost for a given sensor.""" - def __init__(self, hass: HomeAssistant, coordinator, config, is_export): + def __init__(self, hass: HomeAssistant, coordinator, config): """Init sensor.""" # Pass coordinator to base class CoordinatorEntity.__init__(self, coordinator) self._state = None self._config = config - self._is_export = is_export self._attributes = self._config.copy() self._attributes["is_tracking"] = True self._attributes["tracked_charges"] = [] self._attributes["untracked_charges"] = [] - self._is_export = is_export self._last_reset = None self._hass = hass diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_month.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_month.py new file mode 100644 index 00000000..88ddfd11 --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_month.py @@ -0,0 +1,174 @@ +from datetime import datetime +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util.dt import (now) + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorStateClass, +) + +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, + async_track_entity_registry_updated_event, +) + +from homeassistant.helpers.typing import EventType + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from ..const import ( + CONFIG_COST_MONTH_DAY_RESET, + CONFIG_COST_NAME, +) + +from . import accumulate_cost + +from ..utils.attributes import dict_to_typed_dict + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerMonthSensor(RestoreSensor): + """Sensor for calculating the cost for a given sensor over the course of a month.""" + + def __init__(self, hass: HomeAssistant, config_entry, config, tracked_entity_id: str): + """Init sensor.""" + # Pass coordinator to base class + + self._state = None + self._config = config + self._attributes = self._config.copy() + self._attributes["total_consumption"] = 0 + self._attributes["accumulated_data"] = [] + self._last_reset = None + self._tracked_entity_id = tracked_entity_id + self._config_entry = config_entry + + self._hass = hass + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_month" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Month" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def native_unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def native_value(self): + """Determines the total cost of the tracked entity.""" + return self._state + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + current: datetime = now() + self._reset_if_new_week(current) + + return self._last_reset + + 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) + # Make sure our attributes don't override any changed settings + self._attributes.update(self._config) + + _LOGGER.debug(f'Restored {self.unique_id} state: {self._state}') + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._tracked_entity_id], self._async_calculate_cost + ) + ) + + self.async_on_remove( + async_track_entity_registry_updated_event( + self.hass, [self._tracked_entity_id], self._async_update_tracked_entity + ) + ) + + async def _async_update_tracked_entity(self, event) -> None: + data = event.data + if data["action"] != "update": + return + + if "entity_id" in data["changes"]: + _LOGGER.debug(f"Tracked entity for '{self.entity_id}' updated from '{self._tracked_entity_id}' to '{data["entity_id"]}'. Reloading...") + await self._hass.config_entries.async_reload(self._config_entry.entry_id) + + async def _async_calculate_cost(self, event: EventType[EventStateChangedData]): + current = now() + self._reset_if_new_week(current) + + new_state = event.data["new_state"] + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + _LOGGER.debug(f"Source entity updated for '{self.entity_id}'; Event: {event.data}") + + result = accumulate_cost(current, self._attributes["accumulated_data"], float(new_state.state), float(new_state.attributes["total_consumption"])) + + self._attributes["total_consumption"] = result.total_consumption + self._attributes["accumulated_data"] = result.accumulative_data + self._state = result.total_cost + + self.async_write_ha_state() + + def _reset_if_new_week(self, current: datetime): + current: datetime = now() + start_of_day = current.replace(hour=0, minute=0, second=0, microsecond=0) + if self._last_reset is None: + self._last_reset = start_of_day + return True + + target_day = self._config[CONFIG_COST_MONTH_DAY_RESET] if CONFIG_COST_MONTH_DAY_RESET in self._config else 1 + if self._last_reset.day != current.day and current.day == target_day: + self._state = 0 + self._attributes["total_consumption"] = 0 + self._attributes["accumulated_data"] = [] + self._last_reset = start_of_day + + return True + + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_month_off_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_month_off_peak.py new file mode 100644 index 00000000..5aa8485b --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_month_off_peak.py @@ -0,0 +1,30 @@ +import logging + +from ..const import ( + CONFIG_COST_NAME, +) + +from .cost_tracker_month import OctopusEnergyCostTrackerMonthSensor + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerMonthOffPeakSensor(OctopusEnergyCostTrackerMonthSensor): + """Sensor for calculating the cost for a given sensor over the course of a month.""" + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_month_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Month Off Peak" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_month_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_month_peak.py new file mode 100644 index 00000000..fab432c3 --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_month_peak.py @@ -0,0 +1,30 @@ +import logging + +from ..const import ( + CONFIG_COST_NAME, +) + +from .cost_tracker_month import OctopusEnergyCostTrackerMonthSensor + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerMonthPeakSensor(OctopusEnergyCostTrackerMonthSensor): + """Sensor for calculating the cost for a given sensor over the course of a month.""" + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_month_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Month Peak" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_off_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_off_peak.py index 6755c6c2..a73e804d 100644 --- a/custom_components/octopus_energy/cost_tracker/cost_tracker_off_peak.py +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_off_peak.py @@ -43,7 +43,7 @@ class OctopusEnergyCostTrackerOffPeakSensor(CoordinatorEntity, RestoreSensor): """Sensor for calculating the cost for a given sensor.""" - def __init__(self, hass: HomeAssistant, coordinator, config, is_export): + def __init__(self, hass: HomeAssistant, coordinator, config): """Init sensor.""" # Pass coordinator to base class CoordinatorEntity.__init__(self, coordinator) @@ -51,10 +51,8 @@ def __init__(self, hass: HomeAssistant, coordinator, config, is_export): self._state = None self._last_reset = None self._config = config - self._is_export = is_export self._attributes = self._config.copy() self._attributes["is_tracking"] = True - self._is_export = is_export self._hass = hass self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_peak.py index 28eef9b3..bef5fb18 100644 --- a/custom_components/octopus_energy/cost_tracker/cost_tracker_peak.py +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_peak.py @@ -43,7 +43,7 @@ class OctopusEnergyCostTrackerPeakSensor(CoordinatorEntity, RestoreSensor): """Sensor for calculating the cost for a given sensor.""" - def __init__(self, hass: HomeAssistant, coordinator, config, is_export): + def __init__(self, hass: HomeAssistant, coordinator, config): """Init sensor.""" # Pass coordinator to base class CoordinatorEntity.__init__(self, coordinator) @@ -51,10 +51,8 @@ def __init__(self, hass: HomeAssistant, coordinator, config, is_export): self._state = None self._last_reset = None self._config = config - self._is_export = is_export self._attributes = self._config.copy() self._attributes["is_tracking"] = True - self._is_export = is_export self._hass = hass self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_week.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_week.py new file mode 100644 index 00000000..c49b3589 --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_week.py @@ -0,0 +1,174 @@ +from datetime import datetime +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util.dt import (now) + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorStateClass, +) + +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, + async_track_entity_registry_updated_event, +) + +from homeassistant.helpers.typing import EventType + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from ..const import ( + CONFIG_COST_NAME, + CONFIG_COST_WEEKDAY_RESET, +) + +from . import accumulate_cost + +from ..utils.attributes import dict_to_typed_dict + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerWeekSensor(RestoreSensor): + """Sensor for calculating the cost for a given sensor over the course of a week.""" + + def __init__(self, hass: HomeAssistant, config_entry, config, tracked_entity_id: str): + """Init sensor.""" + # Pass coordinator to base class + + self._state = None + self._config = config + self._attributes = self._config.copy() + self._attributes["total_consumption"] = 0 + self._attributes["accumulated_data"] = [] + self._last_reset = None + self._tracked_entity_id = tracked_entity_id + self._config_entry = config_entry + + self._hass = hass + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_week" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Week" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def native_unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def native_value(self): + """Determines the total cost of the tracked entity.""" + return self._state + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + current: datetime = now() + self._reset_if_new_week(current) + + return self._last_reset + + 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) + # Make sure our attributes don't override any changed settings + self._attributes.update(self._config) + + _LOGGER.debug(f'Restored {self.unique_id} state: {self._state}') + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._tracked_entity_id], self._async_calculate_cost + ) + ) + + self.async_on_remove( + async_track_entity_registry_updated_event( + self.hass, [self._tracked_entity_id], self._async_update_tracked_entity + ) + ) + + async def _async_update_tracked_entity(self, event) -> None: + data = event.data + if data["action"] != "update": + return + + if "entity_id" in data["changes"]: + _LOGGER.debug(f"Tracked entity for '{self.entity_id}' updated from '{self._tracked_entity_id}' to '{data["entity_id"]}'. Reloading...") + await self._hass.config_entries.async_reload(self._config_entry.entry_id) + + async def _async_calculate_cost(self, event: EventType[EventStateChangedData]): + current = now() + self._reset_if_new_week(current) + + new_state = event.data["new_state"] + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + _LOGGER.debug(f"Source entity updated for '{self.entity_id}'; Event: {event.data}") + + result = accumulate_cost(current, self._attributes["accumulated_data"], float(new_state.state), float(new_state.attributes["total_consumption"])) + + self._attributes["total_consumption"] = result.total_consumption + self._attributes["accumulated_data"] = result.accumulative_data + self._state = result.total_cost + + self.async_write_ha_state() + + def _reset_if_new_week(self, current: datetime): + current: datetime = now() + start_of_day = current.replace(hour=0, minute=0, second=0, microsecond=0) + if self._last_reset is None: + self._last_reset = start_of_day + return True + + target_weekday = self._config[CONFIG_COST_WEEKDAY_RESET] if CONFIG_COST_WEEKDAY_RESET in self._config else 0 + if self._last_reset.weekday() != current.weekday() and current.weekday() == target_weekday: + self._state = 0 + self._attributes["total_consumption"] = 0 + self._attributes["accumulated_data"] = [] + self._last_reset = start_of_day + + return True + + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_week_off_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_week_off_peak.py new file mode 100644 index 00000000..d4d2503a --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_week_off_peak.py @@ -0,0 +1,30 @@ +import logging + +from ..const import ( + CONFIG_COST_NAME, +) + +from .cost_tracker_week import OctopusEnergyCostTrackerWeekSensor + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerWeekOffPeakSensor(OctopusEnergyCostTrackerWeekSensor): + """Sensor for calculating the cost for a given sensor over the course of a week.""" + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_week_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Week Off Peak" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/cost_tracker/cost_tracker_week_peak.py b/custom_components/octopus_energy/cost_tracker/cost_tracker_week_peak.py new file mode 100644 index 00000000..05627de6 --- /dev/null +++ b/custom_components/octopus_energy/cost_tracker/cost_tracker_week_peak.py @@ -0,0 +1,30 @@ +import logging + +from ..const import ( + CONFIG_COST_NAME, +) + +from .cost_tracker_week import OctopusEnergyCostTrackerWeekSensor + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCostTrackerWeekPeakSensor(OctopusEnergyCostTrackerWeekSensor): + """Sensor for calculating the cost for a given sensor over the course of a week.""" + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_cost_tracker_{self._config[CONFIG_COST_NAME]}_week_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Cost Tracker {self._config[CONFIG_COST_NAME]} Week Peak" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False \ No newline at end of file diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index cd3be086..cf6a7440 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -41,6 +41,12 @@ from .cost_tracker.cost_tracker import OctopusEnergyCostTrackerSensor from .cost_tracker.cost_tracker_off_peak import OctopusEnergyCostTrackerOffPeakSensor from .cost_tracker.cost_tracker_peak import OctopusEnergyCostTrackerPeakSensor +from .cost_tracker.cost_tracker_week import OctopusEnergyCostTrackerWeekSensor +from .cost_tracker.cost_tracker_week_off_peak import OctopusEnergyCostTrackerWeekOffPeakSensor +from .cost_tracker.cost_tracker_week_peak import OctopusEnergyCostTrackerWeekPeakSensor +from .cost_tracker.cost_tracker_month import OctopusEnergyCostTrackerMonthSensor +from .cost_tracker.cost_tracker_month_off_peak import OctopusEnergyCostTrackerMonthOffPeakSensor +from .cost_tracker.cost_tracker_month_peak import OctopusEnergyCostTrackerMonthPeakSensor from .greenness_forecast.current_index import OctopusEnergyGreennessForecastCurrentIndex from .greenness_forecast.next_index import OctopusEnergyGreennessForecastNextIndex @@ -119,7 +125,7 @@ async def async_setup_entry(hass, entry, async_add_entities): # supports_response=SupportsResponse.OPTIONAL ) elif config[CONFIG_KIND] == CONFIG_KIND_COST_TRACKER: - await async_setup_cost_sensors(hass, config, async_add_entities) + await async_setup_cost_sensors(hass, entry, config, async_add_entities) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -153,7 +159,6 @@ async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_ent OctopusEnergyGreennessForecastNextIndex(hass, greenness_forecast_coordinator, account_id) ] - registry = er.async_get(hass) entity_ids_to_migrate = [] if account_info["octoplus_enrolled"] == True: @@ -341,28 +346,43 @@ async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_ent async_add_entities(entities) -async def async_setup_cost_sensors(hass: HomeAssistant, config, async_add_entities): +async def async_setup_cost_sensors(hass: HomeAssistant, entry, config, async_add_entities): account_id = config[CONFIG_ACCOUNT_ID] account_result = hass.data[DOMAIN][account_id][DATA_ACCOUNT] account_info = account_result.account if account_result is not None else None mpan = config[CONFIG_COST_MPAN] + registry = er.async_get(hass) + now = utcnow() - is_export = False for point in account_info["electricity_meter_points"]: tariff_code = get_active_tariff_code(now, point["agreements"]) if tariff_code is not None: # For backwards compatibility, pick the first applicable meter if point["mpan"] == mpan or mpan is None: for meter in point["meters"]: - is_export = meter["is_export"] serial_number = meter["serial_number"] coordinator = hass.data[DOMAIN][account_id][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number)] + + sensor = OctopusEnergyCostTrackerSensor(hass, coordinator, config) + off_peak_sensor = OctopusEnergyCostTrackerOffPeakSensor(hass, coordinator, config) + peak_sensor = OctopusEnergyCostTrackerPeakSensor(hass, coordinator, config) + + sensor_entity_id = registry.async_get_entity_id("sensor", DOMAIN, sensor.unique_id) + off_peak_sensor_entity_id = registry.async_get_entity_id("sensor", DOMAIN, off_peak_sensor.unique_id) + peak_sensor_entity_id = registry.async_get_entity_id("sensor", DOMAIN, peak_sensor.unique_id) + entities = [ - OctopusEnergyCostTrackerSensor(hass, coordinator, config, is_export), - OctopusEnergyCostTrackerOffPeakSensor(hass, coordinator, config, is_export), - OctopusEnergyCostTrackerPeakSensor(hass, coordinator, config, is_export) + sensor, + off_peak_sensor, + peak_sensor, + OctopusEnergyCostTrackerWeekSensor(hass, entry, config, sensor_entity_id if sensor_entity_id is not None else sensor.entity_id), + OctopusEnergyCostTrackerWeekOffPeakSensor(hass, entry, config, off_peak_sensor_entity_id if off_peak_sensor_entity_id is not None else off_peak_sensor.entity_id), + OctopusEnergyCostTrackerWeekPeakSensor(hass, entry, config, peak_sensor_entity_id if peak_sensor_entity_id is not None else peak_sensor.entity_id), + OctopusEnergyCostTrackerMonthSensor(hass, entry, config, sensor_entity_id if sensor_entity_id is not None else sensor.entity_id), + OctopusEnergyCostTrackerMonthOffPeakSensor(hass, entry, config, off_peak_sensor_entity_id if off_peak_sensor_entity_id is not None else off_peak_sensor.entity_id), + OctopusEnergyCostTrackerMonthPeakSensor(hass, entry, config, peak_sensor_entity_id if peak_sensor_entity_id is not None else peak_sensor.entity_id), ] async_add_entities(entities) - return + break \ No newline at end of file