Skip to content

Commit 908c105

Browse files
Next Release (#1322)
2 parents 53e9a52 + 4e59e29 commit 908c105

File tree

20 files changed

+528
-63
lines changed

20 files changed

+528
-63
lines changed

.husky/prepare-commit-msg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
exec < /dev/tty && npx cz --hook || true
1+
# exec < /dev/tty && npx cz --hook || true

_docs/entities/octoplus.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ Binary sensor to indicate if a saving session that the account has joined is act
4141

4242
The state of this sensor states when the saving session events were last updated. The attributes of this sensor exposes the joined and available saving sessions.
4343

44+
!!! note
45+
This will only be available if you have enrolled into Octoplus. Once enrolled, reload the integration to gain access to this sensor.
46+
4447
| Attribute | Type | Description |
4548
|-----------|------|-------------|
4649
| `available_events` | `array` | The collection of saving session events that you haven't joined |
@@ -76,6 +79,9 @@ This will indicate the baseline consumption that you need to be below for the cu
7679

7780
You can use the [current period consumption](./electricity.md#current-interval-accumulative-consumption) sensor (if available) to see how on track you are.
7881

82+
!!! note
83+
This will only be available if you have enrolled into Octoplus. Once enrolled, reload the integration to gain access to this sensor.
84+
7985
!!! note
8086
This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them).
8187

@@ -119,9 +125,8 @@ Each item within `baselines` consists of the following attributes
119125

120126
Binary sensor to indicate if a free electricity session is active.
121127

122-
!!! warning
123-
124-
This sensor uses public information supplied by https://github.com/BottlecapDave/OctopusEnergyApi. However it is only applicable to your account if you have joined Octoplus and have signed up to [free electricity sessions](https://octopus.energy/free-electricity/). Once enrolled into Octoplus, reload the integration to gain access to this sensor.
128+
!!! note
129+
This will only be available if you have enrolled into Octoplus. Once enrolled, reload the integration to gain access to this sensor. This is only applicable if you have signed up to [free electricity sessions](https://octopus.energy/free-electricity/). This sensor uses public information supplied by https://github.com/BottlecapDave/OctopusEnergyApi.
125130

126131
!!! note
127132
This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them).
@@ -145,6 +150,9 @@ Binary sensor to indicate if a free electricity session is active.
145150

146151
The state of this sensor states when the free electricity session events were last updated. The attributes of this sensor exposes the past, present and future free electricity sessions.
147152

153+
!!! note
154+
This will only be available if you have enrolled into Octoplus. Once enrolled, reload the integration to gain access to this sensor. This is only applicable if you have signed up to [free electricity sessions](https://octopus.energy/free-electricity/). This sensor uses public information supplied by https://github.com/BottlecapDave/OctopusEnergyApi.
155+
148156
!!! note
149157
This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them).
150158

@@ -169,6 +177,9 @@ This will indicate the baseline consumption that you need to be above for the cu
169177

170178
You can use the [current period consumption](./electricity.md#current-interval-accumulative-consumption) sensor (if available) to see how on track you are.
171179

180+
!!! note
181+
This will only be available if you have enrolled into Octoplus. Once enrolled, reload the integration to gain access to this sensor. This is only applicable if you have signed up to [free electricity sessions](https://octopus.energy/free-electricity/). This sensor uses public information supplied by https://github.com/BottlecapDave/OctopusEnergyApi.
182+
172183
!!! note
173184
This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them).
174185

_docs/faq.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,19 @@ Cost sensors are calculated by multiplying the consumption by the rate for each
182182

183183
Each half hour block has it's consumption rounded to the nearest 0.01kwh before multiplying by the rate, which is rounded to the nearest penny. The rounding method used is rounding half to even, where numbers ending in 5 are rounded up or down, towards the nearest even hundredth decimal place. As a result, 0.015 would be rounded up to 0.02, while 0.025 is rounded down to 0.02. This is based on [Octopus Energy API documentation](https://developer.octopus.energy/rest/guides/endpoints)
184184

185+
## I'm having issues with a sensor. Is there any way I can see the attributes for a certain point in time?
186+
187+
The majority of attributes for entities are stored in the database for a short amount of time (default is around 10 days). Unfortunately, the only way of obtaining historic attributes is via the database. This can be done via the [SQLite Web Add-On](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_sqlite-web), where the following SQL query can be executed. You'll need to change the target entity id from `binary_sensor.octopus_energy_xxx_intelligent_dispatching` to the one you want to target.
188+
189+
```
190+
SELECT states_meta.entity_id, states.state, state_attributes.shared_attrs, DATETIME(last_changed_ts, 'unixepoch'), DATETIME(last_updated_ts, 'unixepoch')
191+
FROM "states"
192+
join state_attributes on states.attributes_id = state_attributes.attributes_id
193+
join states_meta on states_meta.metadata_id = states.metadata_id
194+
where states_meta.entity_id = 'binary_sensor.octopus_energy_xxx_intelligent_dispatching'
195+
order by last_updated_ts desc
196+
```
197+
185198
## Do you support older versions of the integration?
186199

187200
Due to time constraints, I will only ever support the latest version of the integration. If you have an issue with an older version of the integration, my initial answer will always be to update to the latest version. This might be different to what HACS is reporting if you are not on the minimum supported Home Assistant version (which is highlighted in each release's changelog).

_docs/repairs/no_active_tariff.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
If you receive this repair notice it means that no active tariff was found for one or more of your meters. There are a few reasons why this might be the case.
44

55
1. You are in the middle of switching tariffs. There can sometimes be a gap in your tariff information coming through when your existing tariff ends and your new tariff begins.
6-
2. You are new to OE and your tariff hasn't begun yet. Once your tariff starts, the integration will pick it up and should start working.
7-
3. There is a configuration issue with your account. In this scenario, you'll need to contact Octopus Energy support to get this rectified.
6+
2. You have had a meter swap. When your meter is replaced, it can take time for systems to fully update, leaving a gap in tariff information until this is complete.
7+
3. You are new to OE and your tariff hasn't begun yet. Once your tariff starts, the integration will pick it up and should start working.
8+
4. There is a configuration issue with your account. In this scenario, you'll need to contact Octopus Energy support to get this rectified.
89

9-
All of the above scenarios can be confirmed within your [diagnostic data](../faq.md#ive-been-asked-for-my-meter-information-in-a-bug-request-how-do-i-obtain-this). This will contain your account data including your meters. Under each of your meters, you'll have a list of tariff agreements. For each meter, you should have an agreement with a start date in the past and an end date either in the future or with no end date.
10+
All of the above scenarios can be confirmed within your [diagnostic data](../faq.md#ive-been-asked-for-my-meter-information-in-a-bug-request-how-do-i-obtain-this). This will contain your account data including your meters. Under each of your meters, you'll have a list of tariff agreements. For each meter, you should have an agreement with a start date in the past and an end date either in the future or with no end date.

_docs/setup/cost_tracker.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Each item within the `tracked_changes` and `untracked_changes` have the followin
8888
| `rate` | `float` | The rate the consumption is charged at. This is in pounds and pence (e.g. 1.01 = £1.01) |
8989
| `consumption` | `float` | The consumption value of the specified period. This will be in `kwh`. |
9090
| `cost` | `float` | The cost of the consumption at the specified rate. This is in pounds and pence (e.g. 1.01 = £1.01) |
91+
| `cost_raw` | `float` | The raw cost of the consumption at the specified rate. This is in pounds and pence, but not rounded. This is to account for low cost devices |
9192

9293
#### Variants
9394

custom_components/octopus_energy/api_client/__init__.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -223,27 +223,21 @@
223223
}}'''
224224

225225
intelligent_turn_on_bump_charge_mutation = '''mutation {{
226-
triggerBoostCharge(
227-
input: {{
228-
accountNumber: "{account_id}"
229-
}}
230-
) {{
231-
krakenflexDevice {{
232-
krakenflexDeviceId
233-
}}
234-
}}
226+
updateBoostCharge(input: {{
227+
deviceId: "{device_id}"
228+
action: BOOST
229+
}}) {{
230+
id
231+
}}
235232
}}'''
236233

237234
intelligent_turn_off_bump_charge_mutation = '''mutation {{
238-
deleteBoostCharge(
239-
input: {{
240-
accountNumber: "{account_id}"
241-
}}
242-
) {{
243-
krakenflexDevice {{
244-
krakenflexDeviceId
245-
}}
246-
}}
235+
updateBoostCharge(input: {{
236+
deviceId: "{device_id}"
237+
action: CANCEL
238+
}}) {{
239+
id
240+
}}
247241
}}'''
248242

249243
intelligent_turn_on_smart_charge_mutation = '''mutation {{
@@ -1593,7 +1587,7 @@ async def async_update_intelligent_car_target_time(
15931587
raise TimeoutException()
15941588

15951589
async def async_turn_on_intelligent_bump_charge(
1596-
self, account_id: str,
1590+
self, device_id: str,
15971591
):
15981592
"""Turn on an intelligent bump charge"""
15991593
await self.async_refresh_token()
@@ -1603,7 +1597,7 @@ async def async_turn_on_intelligent_bump_charge(
16031597
client = self._create_client_session()
16041598
url = f'{self._base_url}/v1/graphql/'
16051599
payload = { "query": intelligent_turn_on_bump_charge_mutation.format(
1606-
account_id=account_id,
1600+
device_id=device_id,
16071601
) }
16081602

16091603
headers = { "Authorization": f"JWT {self._graphql_token}", integration_context_header: request_context }
@@ -1615,7 +1609,7 @@ async def async_turn_on_intelligent_bump_charge(
16151609
raise TimeoutException()
16161610

16171611
async def async_turn_off_intelligent_bump_charge(
1618-
self, account_id: str,
1612+
self, device_id: str,
16191613
):
16201614
"""Turn off an intelligent bump charge"""
16211615
await self.async_refresh_token()
@@ -1625,7 +1619,7 @@ async def async_turn_off_intelligent_bump_charge(
16251619
client = self._create_client_session()
16261620
url = f'{self._base_url}/v1/graphql/'
16271621
payload = { "query": intelligent_turn_off_bump_charge_mutation.format(
1628-
account_id=account_id,
1622+
device_id=device_id,
16291623
) }
16301624

16311625
headers = { "Authorization": f"JWT {self._graphql_token}", integration_context_header: request_context }

custom_components/octopus_energy/api_client/heat_pump.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ class Connectivity(BaseModel):
1111

1212

1313
class Telemetry(BaseModel):
14-
temperatureInCelsius: float
14+
temperatureInCelsius: Optional[float]
1515
humidityPercentage: Optional[float]
16-
retrievedAt: str
16+
retrievedAt: Optional[str]
1717

1818

1919
class Sensor(BaseModel):

custom_components/octopus_energy/cost_tracker/__init__.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
from datetime import datetime, timedelta
2+
import logging
23

34
from homeassistant.components.sensor import (
45
SensorStateClass,
56
)
67

78
from homeassistant.helpers.entity import DeviceInfo
89

10+
from ..utils.conversions import pence_to_pounds_pence, round_pounds, value_inc_vat_to_pounds
11+
from ..utils.cost import consumption_cost_in_pence
12+
13+
_LOGGER = logging.getLogger(__name__)
14+
915
def get_device_info_from_device_entry(device_entry):
1016
if device_entry is None:
1117
return None
@@ -152,4 +158,82 @@ def accumulate_cost(current: datetime, accumulative_data: list, new_cost: float,
152158

153159
return AccumulativeCostTrackerResult(new_accumulative_data, total_consumption, total_cost)
154160

155-
161+
def __get_to(item):
162+
return (item["end"].timestamp(), item["end"].fold)
163+
164+
def __sort_consumption(consumption_data):
165+
sorted = consumption_data.copy()
166+
sorted.sort(key=__get_to)
167+
return sorted
168+
169+
def calculate_consumption_and_cost(
170+
consumption_data,
171+
rate_data,
172+
standing_charge,
173+
last_reset,
174+
minimum_consumption_records = 0,
175+
target_rate = None
176+
):
177+
if (consumption_data is not None and len(consumption_data) >= minimum_consumption_records and rate_data is not None and len(rate_data) > 0 and standing_charge is not None):
178+
179+
sorted_consumption_data = __sort_consumption(consumption_data)
180+
181+
# Only calculate our consumption if our data has changed
182+
if (last_reset is None or last_reset < sorted_consumption_data[0]["start"]):
183+
184+
charges = []
185+
total_cost = 0
186+
total_consumption = 0
187+
188+
for consumption in sorted_consumption_data:
189+
consumption_value = consumption["consumption"]
190+
consumption_from = consumption["start"]
191+
consumption_to = consumption["end"]
192+
193+
try:
194+
rate = next(r for r in rate_data if r["start"] == consumption_from and r["end"] == consumption_to)
195+
except StopIteration:
196+
raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to}")
197+
198+
value = rate["value_inc_vat"]
199+
200+
if target_rate is not None and value != target_rate:
201+
continue
202+
203+
total_consumption = total_consumption + consumption_value
204+
cost = pence_to_pounds_pence(consumption_cost_in_pence(consumption_value, value))
205+
cost_raw = (consumption_value * value) / 100
206+
total_cost = total_cost + cost_raw
207+
208+
current_charge = {
209+
"start": rate["start"],
210+
"end": rate["end"],
211+
"rate": value_inc_vat_to_pounds(value),
212+
"consumption": consumption_value,
213+
"cost": cost,
214+
"cost_raw": cost_raw,
215+
}
216+
217+
charges.append(current_charge)
218+
219+
total_cost = round_pounds(total_cost)
220+
total_cost_plus_standing_charge = total_cost + pence_to_pounds_pence(standing_charge)
221+
222+
last_reset = sorted_consumption_data[0]["start"] if len(sorted_consumption_data) > 0 else None
223+
last_calculated_timestamp = sorted_consumption_data[-1]["end"] if len(sorted_consumption_data) > 0 else None
224+
225+
result = {
226+
"standing_charge": pence_to_pounds_pence(standing_charge),
227+
"total_cost_without_standing_charge": total_cost,
228+
"total_cost": total_cost_plus_standing_charge,
229+
"total_consumption": total_consumption,
230+
"last_reset": last_reset,
231+
"last_evaluated": last_calculated_timestamp,
232+
"charges": charges,
233+
}
234+
235+
return result
236+
else:
237+
_LOGGER.debug(f'Skipping consumption and cost calculation as last reset has not changed - last_reset: {last_reset}; consumption start: {sorted_consumption_data[0]["start"]}')
238+
else:
239+
_LOGGER.debug(f'Skipping consumption and cost calculation due to lack of data; consumption: {len(consumption_data) if consumption_data is not None else 0}; rates: {len(rate_data) if rate_data is not None else 0}; standing_charge: {standing_charge}')

custom_components/octopus_energy/cost_tracker/cost_tracker.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
from ..coordinators.electricity_rates import ElectricityRatesCoordinatorResult
4343
from . import add_consumption, get_device_info_from_device_entry
44-
from ..electricity import calculate_electricity_consumption_and_cost
44+
from ..cost_tracker import calculate_consumption_and_cost
4545
from ..utils.rate_information import get_rate_index, get_unique_rates
4646
from ..utils.attributes import dict_to_typed_dict
4747

@@ -283,7 +283,7 @@ def _recalculate_cost(self, current: datetime, tracked_consumption_data: list, u
283283
unique_rate_index = get_rate_index(len(unique_rates), self._peak_type)
284284
target_rate = unique_rates[unique_rate_index] if unique_rate_index is not None else None
285285

286-
tracked_result = calculate_electricity_consumption_and_cost(
286+
tracked_result = calculate_consumption_and_cost(
287287
tracked_consumption_data,
288288
rates,
289289
0,
@@ -292,7 +292,7 @@ def _recalculate_cost(self, current: datetime, tracked_consumption_data: list, u
292292
target_rate=target_rate
293293
)
294294

295-
untracked_result = calculate_electricity_consumption_and_cost(
295+
untracked_result = calculate_consumption_and_cost(
296296
untracked_consumption_data,
297297
rates,
298298
0,
@@ -309,15 +309,17 @@ def _recalculate_cost(self, current: datetime, tracked_consumption_data: list, u
309309
"end": charge["end"],
310310
"rate": charge["rate"],
311311
"consumption": charge["consumption"],
312-
"cost": charge["cost"]
312+
"cost": charge["cost"],
313+
"cost_raw": charge["cost_raw"]
313314
}, tracked_result["charges"]))
314315

315316
self._attributes["untracked_charges"] = list(map(lambda charge: {
316317
"start": charge["start"],
317318
"end": charge["end"],
318319
"rate": charge["rate"],
319320
"consumption": charge["consumption"],
320-
"cost": charge["cost"]
321+
"cost": charge["cost"],
322+
"cost_raw": charge["cost_raw"]
321323
}, untracked_result["charges"]))
322324

323325
self._attributes["total_consumption"] = tracked_result["total_consumption"] + untracked_result["total_consumption"]

custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def __init__(self, hass: HomeAssistant, account_id: str, coordinator, client: Oc
5050
self._next_refresh = None
5151
self._last_retrieved = None
5252
self._request_attempts = 1
53+
self._rates = None
54+
self._standing_charge = None
5355

5456
CoordinatorEntity.__init__(self, coordinator)
5557
OctopusEnergyElectricitySensor.__init__(self, hass, meter, point)
@@ -124,18 +126,28 @@ async def async_update(self):
124126
target_tariff_code = self._config[CONFIG_TARIFF_COMPARISON_TARIFF_CODE]
125127

126128
try:
127-
_LOGGER.debug(f"Retrieving rates and standing charge overrides for '{self._mpan}/{self._serial_number}' ({period_from} - {period_to})...")
128-
[rate_data, standing_charge] = await asyncio.gather(
129-
self._client.async_get_electricity_rates(target_product_code, target_tariff_code, self._is_smart_meter, period_from, period_to),
130-
self._client.async_get_electricity_standing_charge(target_product_code, target_tariff_code, period_from, period_to)
131-
)
129+
if (self._rates is None or
130+
self._standing_charge is None or
131+
self._rates[0]["start"] != period_from or
132+
self._rates[-1]["end"] != period_to or
133+
self._standing_charge["start"] != period_from or
134+
self._standing_charge["end"] != period_to):
135+
136+
_LOGGER.debug(f"Retrieving rates and standing charge overrides for '{self._mpan}/{self._serial_number}' ({period_from} - {period_to})...")
137+
[rate_data, standing_charge] = await asyncio.gather(
138+
self._client.async_get_electricity_rates(target_product_code, target_tariff_code, self._is_smart_meter, period_from, period_to),
139+
self._client.async_get_electricity_standing_charge(target_product_code, target_tariff_code, period_from, period_to)
140+
)
141+
142+
self._rates = rate_data
143+
self._standing_charge = standing_charge
132144

133-
_LOGGER.debug(f"Rates and standing charge overrides for '{self._mpan}/{self._serial_number}' ({period_from} - {period_to}) retrieved")
145+
_LOGGER.debug(f"Rates and standing charge overrides for '{self._mpan}/{self._serial_number}' ({period_from} - {period_to}) retrieved")
134146

135147
consumption_and_cost = calculate_electricity_consumption_and_cost(
136148
consumption_data,
137-
rate_data,
138-
standing_charge["value_inc_vat"] if standing_charge is not None else None,
149+
self._rates,
150+
self._standing_charge["value_inc_vat"] if self._standing_charge is not None else None,
139151
None
140152
)
141153

@@ -170,7 +182,7 @@ async def async_update(self):
170182
"serial_number": self._serial_number,
171183
"product_code": target_product_code,
172184
"tariff_code": target_tariff_code,
173-
"rates": private_rates_to_public_rates(rate_data)
185+
"rates": private_rates_to_public_rates(self._rates)
174186
}))
175187

176188
self._request_attempts = 1

0 commit comments

Comments
 (0)