Skip to content

Commit

Permalink
feat: Updated cost tracker config to be updatable
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Mar 8, 2024
1 parent 01c3733 commit c24db54
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 25 deletions.
8 changes: 8 additions & 0 deletions _docs/setup/cost_tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ This is the entity whose consumption should be tracked and the cost calculated a

This should be true if the tracked entity's state increases over time (true) or if it's the difference between updates (false).

### Week sensor day reset

This is the day of the week the accumulative week sensor should reset. This defaults to Monday.

### Month sensor day reset

This is the day of the month the accumulative month sensor should reset. This must be between 1 and 28 (inclusively). This defaults to the 1st.

## Handling Exporting

Due to everyone's HA setup being different for how they track importing/exporting, the sensors themselves assume that all consumption changes should be tracked and the cost calculated. However, you may wish to turn off tracking when you're exporting. This can be done via the related [services](../services.md#octopus_energyupdate_cost_tracker).
Expand Down
11 changes: 11 additions & 0 deletions custom_components/octopus_energy/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ..utils import get_active_tariff_code

def get_meter_tariffs(account_info, now):
meters = {}
if account_info is not None and len(account_info["electricity_meter_points"]) > 0:
for point in account_info["electricity_meter_points"]:
active_tariff_code = get_active_tariff_code(now, point["agreements"])
if active_tariff_code is not None:
meters[point["mpan"]] = active_tariff_code

return meters
26 changes: 25 additions & 1 deletion custom_components/octopus_energy/config/cost_tracker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import re

from . import get_meter_tariffs

from ..const import (
CONFIG_COST_MONTH_DAY_RESET,
CONFIG_COST_MPAN,
CONFIG_COST_NAME,
CONFIG_COST_WEEKDAY_RESET,
REGEX_ENTITY_NAME
)

Expand All @@ -20,11 +25,30 @@ def merge_cost_tracker_config(data: dict, options: dict, updated_config: dict =

return config

def validate_cost_tracker_config(data):
def validate_cost_tracker_config(data, account_info, now):
errors = {}

matches = re.search(REGEX_ENTITY_NAME, data[CONFIG_COST_NAME])
if matches is None:
errors[CONFIG_COST_NAME] = "invalid_target_name"

meter_tariffs = get_meter_tariffs(account_info, now)
if (data[CONFIG_COST_MPAN] not in meter_tariffs):
errors[CONFIG_COST_MPAN] = "invalid_mpan"

# For some reason int type isn't working properly - reporting user input malformed
if CONFIG_COST_WEEKDAY_RESET in data:
if isinstance(data[CONFIG_COST_WEEKDAY_RESET], int) == False:
matches = re.search("^[0-9]+$", data[CONFIG_COST_WEEKDAY_RESET])
if matches is None:
errors[CONFIG_COST_WEEKDAY_RESET] = "invalid_week_day"
else:
data[CONFIG_COST_WEEKDAY_RESET] = int(data[CONFIG_COST_WEEKDAY_RESET])

if (data[CONFIG_COST_WEEKDAY_RESET] < 0 or data[CONFIG_COST_WEEKDAY_RESET] > 6):
errors[CONFIG_COST_WEEKDAY_RESET] = "invalid_week_day"

if (CONFIG_COST_MONTH_DAY_RESET in data and (data[CONFIG_COST_MONTH_DAY_RESET] < 1 or data[CONFIG_COST_MONTH_DAY_RESET] > 28)):
errors[CONFIG_COST_MONTH_DAY_RESET] = "invalid_month_day"

return errors
12 changes: 1 addition & 11 deletions custom_components/octopus_energy/config/target_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
REGEX_TIME
)

from ..utils import get_active_tariff_code
from . import get_meter_tariffs
from ..utils.tariff_check import is_agile_tariff

async def async_migrate_target_config(version: int, data: {}, get_entries):
Expand Down Expand Up @@ -80,16 +80,6 @@ def merge_target_rate_config(data: dict, options: dict, updated_config: dict = N

return config

def get_meter_tariffs(account_info, now):
meters = {}
if account_info is not None and len(account_info["electricity_meter_points"]) > 0:
for point in account_info["electricity_meter_points"]:
active_tariff_code = get_active_tariff_code(now, point["agreements"])
if active_tariff_code is not None:
meters[point["mpan"]] = active_tariff_code

return meters

def is_time_frame_long_enough(hours, start_time, end_time):
start_time = parse_datetime(f"2023-08-01T{start_time}:00Z")
end_time = parse_datetime(f"2023-08-01T{end_time}:00Z")
Expand Down
111 changes: 102 additions & 9 deletions custom_components/octopus_energy/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
)

from .coordinators.account import AccountCoordinatorResult
from .config.cost_tracker import validate_cost_tracker_config
from .config.cost_tracker import merge_cost_tracker_config, validate_cost_tracker_config
from .config.target_rates import merge_target_rate_config, validate_target_rate_config
from .config.main import async_validate_main_config, merge_main_config
from .const import (
CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE,
CONFIG_COST_MONTH_DAY_RESET,
CONFIG_COST_TARGET_ENTITY_ID,
CONFIG_COST_MPAN,
CONFIG_COST_NAME,
CONFIG_COST_WEEKDAY_RESET,
CONFIG_DEFAULT_LIVE_ELECTRICITY_CONSUMPTION_REFRESH_IN_MINUTES,
CONFIG_DEFAULT_LIVE_GAS_CONSUMPTION_REFRESH_IN_MINUTES,
CONFIG_DEFAULT_PREVIOUS_CONSUMPTION_OFFSET_IN_DAYS,
Expand Down Expand Up @@ -77,6 +79,17 @@ def get_target_rate_meters(account_info, now):

return meters

def get_weekday_options():
return [
selector.SelectOptionDict(value="0", label="Monday"),
selector.SelectOptionDict(value="1", label="Tuesday"),
selector.SelectOptionDict(value="2", label="Wednesday"),
selector.SelectOptionDict(value="3", label="Thursday"),
selector.SelectOptionDict(value="4", label="Friday"),
selector.SelectOptionDict(value="5", label="Saturday"),
selector.SelectOptionDict(value="6", label="Sunday"),
]

def get_account_ids(hass):
account_ids = {}
for entry in hass.config_entries.async_entries(DOMAIN):
Expand Down Expand Up @@ -166,6 +179,13 @@ async def __async_setup_cost_tracker_schema__(self):
selector.EntitySelectorConfig(domain="sensor", device_class=[SensorDeviceClass.ENERGY]),
),
vol.Optional(CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE, default=False): bool,
vol.Required(CONFIG_COST_WEEKDAY_RESET, default="0"): selector.SelectSelector(
selector.SelectSelectorConfig(
options=get_weekday_options(),
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONFIG_COST_MONTH_DAY_RESET, default=1): cv.positive_int,
})

async def async_step_target_rate(self, user_input):
Expand Down Expand Up @@ -197,12 +217,19 @@ async def async_step_target_rate(self, user_input):

async def async_step_cost_tracker(self, user_input):
"""Setup a target based on the provided user input"""
errors = validate_cost_tracker_config(user_input) if user_input is not None else {}
account_ids = get_account_ids(self.hass)
account_id = list(account_ids.keys())[0]

account_info: AccountCoordinatorResult = self.hass.data[DOMAIN][account_id][DATA_ACCOUNT]
if (account_info is None):
return self.async_abort(reason="account_not_found")

now = utcnow()
errors = validate_cost_tracker_config(user_input, account_info.account, now) if user_input is not None else {}

if len(errors) < 1 and user_input is not None:
user_input[CONFIG_KIND] = CONFIG_KIND_COST_TRACKER
user_input[CONFIG_ACCOUNT_ID] = list(account_ids.keys())[0]
user_input[CONFIG_ACCOUNT_ID] = account_id
return self.async_create_entry(
title=f"{user_input[CONFIG_COST_NAME]} (cost tracker)",
data=user_input
Expand Down Expand Up @@ -264,8 +291,7 @@ def __init__(self, entry) -> None:
self._entry = entry

async def __async_setup_target_rate_schema__(self, config, errors):
account_ids = get_account_ids(self.hass)
account_id = list(account_ids.keys())[0]
account_id = config[CONFIG_ACCOUNT_ID]

account_info: AccountCoordinatorResult = self.hass.data[DOMAIN][account_id][DATA_ACCOUNT]
if account_info is None:
Expand Down Expand Up @@ -396,6 +422,51 @@ async def __async_setup_main_schema__(self, config, errors):
),
errors=errors
)

async def __async_setup_cost_tracker_schema__(self, config, errors):
account_id = config[CONFIG_ACCOUNT_ID]

account_info: AccountCoordinatorResult = self.hass.data[DOMAIN][account_id][DATA_ACCOUNT]
if account_info is None:
errors[CONFIG_TARGET_MPAN] = "account_not_found"

now = utcnow()
meters = get_target_rate_meters(account_info.account, now)

return self.async_show_form(
step_id="cost_tracker",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({
vol.Required(CONFIG_COST_NAME): str,
vol.Required(CONFIG_TARGET_MPAN): selector.SelectSelector(
selector.SelectSelectorConfig(
options=meters,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONFIG_COST_TARGET_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class=[SensorDeviceClass.ENERGY]),
),
vol.Optional(CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE): bool,
vol.Required(CONFIG_COST_WEEKDAY_RESET): selector.SelectSelector(
selector.SelectSelectorConfig(
options=get_weekday_options(),
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONFIG_COST_MONTH_DAY_RESET): cv.positive_int,
}),
{
CONFIG_COST_NAME: config[CONFIG_COST_NAME],
CONFIG_TARGET_MPAN: config[CONFIG_TARGET_MPAN],
CONFIG_COST_TARGET_ENTITY_ID: config[CONFIG_COST_TARGET_ENTITY_ID],
CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE: config[CONFIG_COST_ENTITY_ACCUMULATIVE_VALUE],
CONFIG_COST_WEEKDAY_RESET: f"{config[CONFIG_COST_WEEKDAY_RESET]}" if CONFIG_COST_WEEKDAY_RESET in config else "0",
CONFIG_COST_MONTH_DAY_RESET: config[CONFIG_COST_MONTH_DAY_RESET] if CONFIG_COST_MONTH_DAY_RESET in config else 1,
}
),
errors=errors
)

async def async_step_init(self, user_input):
"""Manage the options for the custom component."""
Expand All @@ -408,7 +479,9 @@ async def async_step_init(self, user_input):
config = merge_target_rate_config(self._entry.data, self._entry.options, user_input)
return await self.__async_setup_target_rate_schema__(config, {})

# if (kind == CONFIG_KIND_COST_SENSOR):
if (kind == CONFIG_KIND_COST_TRACKER):
config = merge_cost_tracker_config(self._entry.data, self._entry.options, user_input)
return await self.__async_setup_cost_tracker_schema__(config, {})

return self.async_abort(reason="not_supported")

Expand All @@ -431,13 +504,33 @@ async def async_step_target_rate(self, user_input):

config = merge_target_rate_config(self._entry.data, self._entry.options, user_input)

client = self.hass.data[DOMAIN][account_id][DATA_CLIENT]
account_info = await client.async_get_account(account_id)
account_info: AccountCoordinatorResult = self.hass.data[DOMAIN][account_id][DATA_ACCOUNT]
if (account_info is None):
return self.async_abort(reason="account_not_found")

now = utcnow()
errors = validate_target_rate_config(user_input, account_info, now)
errors = validate_target_rate_config(config, account_info.account, now)

if (len(errors) > 0):
return await self.__async_setup_target_rate_schema__(config, errors)

return self.async_create_entry(title="", data=config)

async def async_step_cost_tracker(self, user_input):
"""Manage the options for the custom component."""
account_ids = get_account_ids(self.hass)
account_id = list(account_ids.keys())[0]

config = merge_cost_tracker_config(self._entry.data, self._entry.options, user_input)

account_info: AccountCoordinatorResult = self.hass.data[DOMAIN][account_id][DATA_ACCOUNT]
if (account_info is None):
return self.async_abort(reason="account_not_found")

now = utcnow()
errors = validate_cost_tracker_config(config, account_info.account, now)

if (len(errors) > 0):
return await self.__async_setup_cost_tracker_schema__(config, errors)

return self.async_create_entry(title="", data=config)
16 changes: 12 additions & 4 deletions custom_components/octopus_energy/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
"name": "The name of your cost sensor",
"mpan": "The meter the cost rates should be associated with",
"target_entity_id": "The entity to track the costs for.",
"entity_accumulative_value": "Tracked entity state is accumulative"
"entity_accumulative_value": "Tracked entity state is accumulative",
"weekday_reset": "The day when the week cost sensor should reset",
"month_day_reset": "The day when the month cost sensor should reset"
}
}
},
Expand All @@ -58,7 +60,9 @@
"invalid_offset": "Offset must be in the form of HH:MM:SS with an optional negative symbol",
"invalid_hours_time_frame": "The target hours do not fit in the elected target time frame",
"invalid_mpan": "Meter not found in account with an active tariff",
"invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information."
"invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information.",
"invalid_week_day": "Week reset day must be between 0 and 6 (inclusively)",
"invalid_month_day": "Month reset day must be between 1 and 28 (inclusively)"
},
"abort": {
"not_supported": "Configuration for target rates is not supported at the moment.",
Expand Down Expand Up @@ -106,7 +110,9 @@
"name": "The name of your cost sensor",
"mpan": "The meter the cost rates should be associated with",
"target_entity_id": "The entity to track the costs for.",
"entity_accumulative_value": "Tracked entity state is accumulative"
"entity_accumulative_value": "Tracked entity state is accumulative",
"weekday_reset": "The day when the week cost sensor should reset",
"month_day_reset": "The day when the month cost sensor should reset"
}
}
},
Expand All @@ -119,7 +125,9 @@
"invalid_offset": "Offset must be in the form of HH:MM:SS with an optional negative symbol",
"invalid_hours_time_frame": "The target hours do not fit in the elected target time frame",
"invalid_mpan": "Meter not found in account with an active tariff",
"invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information."
"invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information.",
"invalid_week_day": "Week reset day must be between 0 and 6 (inclusively)",
"invalid_month_day": "Month reset day must be between 1 and 28 (inclusively)"
},
"abort": {
"not_supported": "Configuration for target rates is not supported at the moment.",
Expand Down

0 comments on commit c24db54

Please sign in to comment.