Skip to content

Commit

Permalink
feat: standardised attributes around time periods across the integration
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
Any previous references to valid_from/valid_to, interval_start/interval_end or from/to have now been changed to
start/end. This change may take a little while to propagate while old data is replaced. Any reliances on these attribute names will need to be updated.
  • Loading branch information
BottlecapDave committed Nov 16, 2023
1 parent ad829cc commit 89a6985
Show file tree
Hide file tree
Showing 74 changed files with 602 additions and 598 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
**/__pycache__/**
run_tests.sh
run_unit_tests.sh
run_integration_tests.sh
45 changes: 24 additions & 21 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@

def get_valid_from(rate):
return rate["valid_from"]

def get_from(rate):
return rate["start"]

def rates_to_thirty_minute_increments(data, period_from: datetime, period_to: datetime, tariff_code: str, price_cap: float = None):
"""Process the collection of rates to ensure they're in 30 minute periods"""
Expand Down Expand Up @@ -326,8 +329,8 @@ def rates_to_thirty_minute_increments(data, period_from: datetime, period_to: da
valid_to = valid_from + timedelta(minutes=30)
results.append({
"value_inc_vat": value_inc_vat,
"valid_from": valid_from,
"valid_to": valid_to,
"start": valid_from,
"end": valid_to,
"tariff_code": tariff_code,
"is_capped": is_capped
})
Expand Down Expand Up @@ -434,8 +437,8 @@ async def async_get_account(self, account_id):
else []
)),
"agreements": list(map(lambda a: {
"valid_from": a["validFrom"],
"valid_to": a["validTo"],
"start": a["validFrom"],
"end": a["validTo"],
"tariff_code": a["tariff"]["tariffCode"] if "tariff" in a and "tariffCode" in a["tariff"] else None,
"product_code": a["tariff"]["productCode"] if "tariff" in a and "productCode" in a["tariff"] else None,
},
Expand Down Expand Up @@ -470,8 +473,8 @@ async def async_get_account(self, account_id):
else []
)),
"agreements": list(map(lambda a: {
"valid_from": a["validFrom"],
"valid_to": a["validTo"],
"start": a["validFrom"],
"end": a["validTo"],
"tariff_code": a["tariff"]["tariffCode"] if "tariff" in a and "tariffCode" in a["tariff"] else None,
"product_code": a["tariff"]["productCode"] if "tariff" in a and "productCode" in a["tariff"] else None,
},
Expand Down Expand Up @@ -572,8 +575,8 @@ async def async_get_smart_meter_consumption(self, device_id: str, period_from: d
return list(map(lambda mp: {
"consumption": float(mp["consumptionDelta"]) / 1000,
"demand": float(mp["demand"]) if "demand" in mp and mp["demand"] is not None else None,
"interval_start": parse_datetime(mp["readAt"]),
"interval_end": parse_datetime(mp["readAt"]) + timedelta(minutes=30)
"start": parse_datetime(mp["readAt"]),
"end": parse_datetime(mp["readAt"]) + timedelta(minutes=30)
}, response_body["data"]["smartMeterTelemetry"]))
else:
_LOGGER.debug(f"Failed to retrieve smart meter consumption data - device_id: {device_id}; period_from: {period_from}; period_to: {period_to}")
Expand Down Expand Up @@ -625,7 +628,7 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code,
results.append(rate)

# Because we retrieve our day and night periods separately over a 2 day period, we need to sort our rates
results.sort(key=get_valid_from)
results.sort(key=get_from)

return results

Expand Down Expand Up @@ -659,7 +662,7 @@ async def async_get_electricity_consumption(self, mpan, serial_number, period_fr

# For some reason, the end point returns slightly more data than we requested, so we need to filter out
# the results
if as_utc(item["interval_start"]) >= period_from and as_utc(item["interval_end"]) <= period_to:
if as_utc(item["start"]) >= period_from and as_utc(item["end"]) <= period_to:
results.append(item)

results.sort(key=self.__get_interval_end)
Expand Down Expand Up @@ -703,7 +706,7 @@ async def async_get_gas_consumption(self, mprn, serial_number, period_from, peri

# For some reason, the end point returns slightly more data than we requested, so we need to filter out
# the results
if as_utc(item["interval_start"]) >= period_from and as_utc(item["interval_end"]) <= period_to:
if as_utc(item["start"]) >= period_from and as_utc(item["end"]) <= period_to:
results.append(item)

results.sort(key=self.__get_interval_end)
Expand Down Expand Up @@ -735,8 +738,8 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from,
data = await self.__async_read_response__(response, url)
if (data is not None and "results" in data and len(data["results"]) > 0):
result = {
"valid_from": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None,
"valid_to": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None,
"start": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None,
"end": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None,
"value_inc_vat": float(data["results"][0]["value_inc_vat"])
}

Expand All @@ -758,8 +761,8 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t
data = await self.__async_read_response__(response, url)
if (data is not None and "results" in data and len(data["results"]) > 0):
result = {
"valid_from": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None,
"valid_to": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None,
"start": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None,
"end": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None,
"value_inc_vat": float(data["results"][0]["value_inc_vat"])
}

Expand Down Expand Up @@ -1050,7 +1053,7 @@ async def async_spin_wheel_of_fortune(self, account_id: str, is_electricity: boo
return None

def __get_interval_end(self, item):
return item["interval_end"]
return item["end"]

def __is_night_rate(self, rate, is_smart_meter):
# Normally the economy seven night rate is between 12am and 7am UK time
Expand All @@ -1065,11 +1068,11 @@ def __is_night_rate(self, rate, is_smart_meter):

def __is_between_times(self, rate, target_from_time, target_to_time, use_utc):
"""Determines if a current rate is between two times"""
rate_local_valid_from = as_local(rate["valid_from"])
rate_local_valid_to = as_local(rate["valid_to"])
rate_local_valid_from = as_local(rate["start"])
rate_local_valid_to = as_local(rate["end"])

if use_utc:
rate_utc_valid_from = as_utc(rate["valid_from"])
rate_utc_valid_from = as_utc(rate["start"])
# We need to convert our times into local time to account for BST to ensure that our rate is valid between the target times.
from_date_time = as_local(parse_datetime(rate_utc_valid_from.strftime(f"%Y-%m-%dT{target_from_time}Z")))
to_date_time = as_local(parse_datetime(rate_utc_valid_from.strftime(f"%Y-%m-%dT{target_to_time}Z")))
Expand All @@ -1086,8 +1089,8 @@ def __is_between_times(self, rate, target_from_time, target_to_time, use_utc):
def __process_consumption(self, item):
return {
"consumption": float(item["consumption"]),
"interval_start": as_utc(parse_datetime(item["interval_start"])),
"interval_end": as_utc(parse_datetime(item["interval_end"]))
"start": as_utc(parse_datetime(item["interval_start"])),
"end": as_utc(parse_datetime(item["interval_end"]))
}

async def __async_read_response__(self, response, url):
Expand Down
4 changes: 2 additions & 2 deletions custom_components/octopus_energy/coordinators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ def raise_rate_events(now: datetime,
next_rates = []

for rate in rates:
if (rate["valid_from"] < today_start):
if (rate["start"] < today_start):
previous_rates.append(rate)
elif (rate["valid_from"] >= today_end):
elif (rate["start"] >= today_end):
next_rates.append(rate)
else:
current_rates.append(rate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async def async_refresh_electricity_rates_data(
existing_rates_result is None or
existing_rates_result.rates is None or
len(existing_rates_result.rates) < 1 or
existing_rates_result.rates[-1]["valid_from"] < period_from):
existing_rates_result.rates[-1]["start"] < period_from):
try:
new_rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to)
except:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def async_refresh_electricity_standing_charges_data(
if ((current.minute % 30) == 0 or
existing_standing_charges_result is None or
existing_standing_charges_result.standing_charge is None or
(existing_standing_charges_result.standing_charge["valid_from"] is not None and existing_standing_charges_result.standing_charge["valid_from"] < period_from)):
(existing_standing_charges_result.standing_charge["start"] is not None and existing_standing_charges_result.standing_charge["start"] < period_from)):
try:
new_standing_charge = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to)
_LOGGER.debug(f'Electricity standing charges retrieved for {target_mpan}/{target_serial_number} ({tariff_code})')
Expand Down
2 changes: 1 addition & 1 deletion custom_components/octopus_energy/coordinators/gas_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def async_refresh_gas_rates_data(
existing_rates_result is None or
existing_rates_result.rates is None or
len(existing_rates_result.rates) < 1 or
existing_rates_result.rates[-1]["valid_from"] < period_from):
existing_rates_result.rates[-1]["start"] < period_from):
try:
new_rates = await client.async_get_gas_rates(tariff_code, period_from, period_to)
except:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def async_refresh_gas_standing_charges_data(
if ((current.minute % 30) == 0 or
existing_standing_charges_result is None or
existing_standing_charges_result.standing_charge is None or
(existing_standing_charges_result.standing_charge["valid_from"] is not None and existing_standing_charges_result.standing_charge["valid_from"] < period_from)):
(existing_standing_charges_result.standing_charge["start"] is not None and existing_standing_charges_result.standing_charge["start"] < period_from)):
try:
new_standing_charge = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to)
_LOGGER.debug(f'Gas standing charges retrieved for {target_mprn}/{target_serial_number} ({tariff_code})')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
_LOGGER = logging.getLogger(__name__)

def __get_interval_end(item):
return item["interval_end"]
return item["end"]

def __sort_consumption(consumption_data):
sorted = consumption_data.copy()
Expand All @@ -53,7 +53,7 @@ async def async_fetch_consumption_and_rates(

if (previous_data == None or
((len(previous_data["consumption"]) < 1 or
previous_data["consumption"][-1]["interval_end"] < period_to) and
previous_data["consumption"][-1]["end"] < period_to) and
utc_now.minute % 30 == 0)):

_LOGGER.debug(f"Retrieving previous consumption data for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}...")
Expand Down
22 changes: 11 additions & 11 deletions custom_components/octopus_energy/electricity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from ..utils.conversions import value_inc_vat_to_pounds
from ..utils import get_off_peak_cost

def __get_interval_end(item):
return item["interval_end"]
def __get_to(item):
return item["end"]

def __sort_consumption(consumption_data):
sorted = consumption_data.copy()
sorted.sort(key=__get_interval_end)
sorted.sort(key=__get_to)
return sorted

def calculate_electricity_consumption_and_cost(
Expand All @@ -25,7 +25,7 @@ def calculate_electricity_consumption_and_cost(
sorted_consumption_data = __sort_consumption(consumption_data)

# Only calculate our consumption if our data has changed
if (last_reset is None or last_reset < sorted_consumption_data[0]["interval_start"]):
if (last_reset is None or last_reset < sorted_consumption_data[0]["start"]):

charges = []
total_cost_in_pence = 0
Expand All @@ -39,12 +39,12 @@ def calculate_electricity_consumption_and_cost(

for consumption in sorted_consumption_data:
consumption_value = consumption["consumption"]
consumption_from = consumption["interval_start"]
consumption_to = consumption["interval_end"]
consumption_from = consumption["start"]
consumption_to = consumption["end"]
total_consumption = total_consumption + consumption_value

try:
rate = next(r for r in rate_data if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to)
rate = next(r for r in rate_data if r["start"] == consumption_from and r["end"] == consumption_to)
except StopIteration:
raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {tariff_code}")

Expand All @@ -60,8 +60,8 @@ def calculate_electricity_consumption_and_cost(
total_cost_peak = total_cost_peak + cost

charges.append({
"from": rate["valid_from"],
"to": rate["valid_to"],
"start": rate["start"],
"end": rate["end"],
"rate": value_inc_vat_to_pounds(value),
"consumption": consumption_value,
"cost": round(cost / 100, 2)
Expand All @@ -70,8 +70,8 @@ def calculate_electricity_consumption_and_cost(
total_cost = round(total_cost_in_pence / 100, 2)
total_cost_plus_standing_charge = round((total_cost_in_pence + standing_charge) / 100, 2)

last_reset = sorted_consumption_data[0]["interval_start"]
last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"]
last_reset = sorted_consumption_data[0]["start"]
last_calculated_timestamp = sorted_consumption_data[-1]["end"]

result = {
"standing_charge": round(standing_charge / 100, 2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ def state(self):
"total": consumption_and_cost["total_consumption"],
"last_evaluated": consumption_and_cost["last_evaluated"],
"charges": list(map(lambda charge: {
"from": charge["from"],
"to": charge["to"],
"start": charge["start"],
"end": charge["end"],
"consumption": charge["consumption"]
}, consumption_and_cost["charges"]))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ def state(self):
"total": consumption_and_cost["total_cost"],
"last_evaluated": consumption_and_cost["last_evaluated"],
"charges": list(map(lambda charge: {
"from": charge["from"],
"to": charge["to"],
"start": charge["start"],
"end": charge["end"],
"rate": charge["rate"],
"consumption": charge["consumption"],
"cost": charge["cost"]
Expand Down
12 changes: 6 additions & 6 deletions custom_components/octopus_energy/electricity/current_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def __init__(self, hass: HomeAssistant, coordinator, meter, point, tariff_code,
"tariff": self._tariff_code,
"all_rates": [],
"applicable_rates": [],
"valid_from": None,
"valid_to": None,
"start": None,
"end": None,
"is_capped": None,
"is_intelligent_adjusted": None,
"current_day_min_rate": None,
Expand Down Expand Up @@ -103,8 +103,8 @@ def state(self):
"is_export": self._is_export,
"is_smart_meter": self._is_smart_meter,
"tariff": self._tariff_code,
"valid_from": rate_information["current_rate"]["valid_from"],
"valid_to": rate_information["current_rate"]["valid_to"],
"start": rate_information["current_rate"]["start"],
"end": rate_information["current_rate"]["end"],
"is_capped": rate_information["current_rate"]["is_capped"],
"is_intelligent_adjusted": rate_information["current_rate"]["is_intelligent_adjusted"],
"current_day_min_rate": rate_information["min_rate_today"],
Expand All @@ -122,8 +122,8 @@ def state(self):
"is_export": self._is_export,
"is_smart_meter": self._is_smart_meter,
"tariff": self._tariff_code,
"valid_from": None,
"valid_to": None,
"start": None,
"end": None,
"is_capped": None,
"is_intelligent_adjusted": None,
"current_day_min_rate": None,
Expand Down
12 changes: 6 additions & 6 deletions custom_components/octopus_energy/electricity/next_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def __init__(self, hass: HomeAssistant, coordinator, meter, point):
"is_export": self._is_export,
"is_smart_meter": self._is_smart_meter,
"applicable_rates": [],
"valid_from": None,
"valid_to": None,
"start": None,
"end": None,
}

@property
Expand Down Expand Up @@ -93,8 +93,8 @@ def state(self):
"serial_number": self._serial_number,
"is_export": self._is_export,
"is_smart_meter": self._is_smart_meter,
"valid_from": rate_information["next_rate"]["valid_from"],
"valid_to": rate_information["next_rate"]["valid_to"],
"start": rate_information["next_rate"]["start"],
"end": rate_information["next_rate"]["end"],
"applicable_rates": rate_information["applicable_rates"],
}

Expand All @@ -105,8 +105,8 @@ def state(self):
"serial_number": self._serial_number,
"is_export": self._is_export,
"is_smart_meter": self._is_smart_meter,
"valid_from": None,
"valid_to": None,
"start": None,
"end": None,
"applicable_rates": [],
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async def async_update(self):
consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None
rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None
standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None
current = consumption_data[0]["interval_start"] if consumption_data is not None and len(consumption_data) > 0 else None
current = consumption_data[0]["start"] if consumption_data is not None and len(consumption_data) > 0 else None

consumption_and_cost = calculate_electricity_consumption_and_cost(
current,
Expand Down Expand Up @@ -144,8 +144,8 @@ async def async_update(self):
"total": consumption_and_cost["total_consumption"],
"last_evaluated": consumption_and_cost["last_evaluated"],
"charges": list(map(lambda charge: {
"from": charge["from"],
"to": charge["to"],
"start": charge["start"],
"end": charge["end"],
"consumption": charge["consumption"]
}, consumption_and_cost["charges"]))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def state(self):
consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None
rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None
standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None
current = consumption_data[0]["interval_start"] if consumption_data is not None and len(consumption_data) > 0 else None
current = consumption_data[0]["start"] if consumption_data is not None and len(consumption_data) > 0 else None

consumption_and_cost = calculate_electricity_consumption_and_cost(
current,
Expand Down
Loading

0 comments on commit 89a6985

Please sign in to comment.