Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5649a72
feat: Updated underlying intelligent bump charge API due to deprecati…
BottlecapDave May 16, 2025
6cf07e7
feat: Added API client and coordinator for retrieving fan club discou…
BottlecapDave May 16, 2025
89d0164
feat: Added entities for showing previous, current and next fan club …
BottlecapDave May 17, 2025
99e670d
docs: Fixed broken link
BottlecapDave May 17, 2025
57ab2ed
feat: Added event sensor for exposing the discounts (1.5 hours dev time)
BottlecapDave May 18, 2025
7c64ef1
chore: Updated fan club mock
BottlecapDave May 18, 2025
2b6619b
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave May 19, 2025
3045936
chore: Handle optional forecasts
BottlecapDave May 19, 2025
9429df5
fix: Fixed handling of no forecasts (30 minutes dev time)
BottlecapDave May 31, 2025
9811ffa
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave May 31, 2025
19c6e91
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave May 31, 2025
aa4f83f
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Jun 28, 2025
1598d67
chore: Increased fan club discount update frequency
BottlecapDave Jun 28, 2025
5497438
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Jul 17, 2025
5991ee1
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Jul 17, 2025
ac313e1
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Aug 3, 2025
044e34c
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Aug 3, 2025
dfb3691
chore: Updated mocked fan club discounts to be consistent
BottlecapDave Aug 3, 2025
12036bc
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Aug 31, 2025
1866ba8
Merge branch 'develop' into feat/fan-club-discount
BottlecapDave Aug 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Below are the main features of the integration
* [Wheel of fortune support](https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/wheel_of_fortune/)
* [Greener days support](https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/greenness_forecast)
* [Heat Pump support](https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/heat_pump)
* [Fan Club support](https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/fan_club)

## How to install

Expand Down
15 changes: 15 additions & 0 deletions _docs/entities/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ This sensor states when wheel of fortune data was last retrieved.

This sensor states when heat pump data was last retrieved.

!!! 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 |
|-----------|------|-------------|
| `attempts` | `integer` | The number of attempts that have been made to retrieve the data |
| `next_refresh` | `datetime` | The timestamp of when the data will next be attempted to be retrieved |
| `last_error` | `string` | The error that was raised to cause the last retrieval attempt to fail |

## Fan Club Discount Last Retrieved

`sensor.octopus_energy_{{ACCOUNT_ID}}_fan_club_discount_data_last_retrieved`

This sensor states when fan club discount data was last retrieved.

!!! note
This is [disabled by default](../faq.md#there-are-entities-that-are-disabled-why-are-they-disabled-and-how-do-i-enable-them).

Expand Down
65 changes: 65 additions & 0 deletions _docs/entities/fan_club.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Fan Club

The following entities are available if you are a member of the [Octopus Energy Fan Club](https://www.octopusenergygeneration.com/fan-club/).

## Current Discount

`sensor.octopus_energy_fan_club_{{ACCOUNT_ID}}_{{FAN_CLUB_ID}}_current_discount`

The current discount applied for the specified fan club.

This is in pounds and pence (e.g. 1.01 = £1.01).

| Attribute | Type | Description |
|-----------|------|-------------|
| `source` | `string` | The source of the discount |
| `start` | `datetime` | The date/time when the discount started |
| `end` | `datetime` | The date/time when the discount ends |
| `current_day_min_discount` | `float` | The minimum discount available for the current day |
| `current_day_max_discount` | `float` | The maximum discount available for the current day |
| `current_day_average_discount` | `float` | The average discount for the current day |

## Previous Discount

`sensor.octopus_energy_fan_club_{{ACCOUNT_ID}}_{{FAN_CLUB_ID}}_previous_discount`

The previous discount for the specified fan club, that differs from the current discount. If there is no previous discount (e.g. discounts before now are of the same value as the current discount), then this will be reported as `unknown`/`none`.

| Attribute | Type | Description |
|-----------|------|-------------|
| `source` | `string` | The source of the discount |
| `start` | `datetime` | The date/time when the discount started |
| `end` | `datetime` | The date/time when the discount ended |

## Next Discount

`sensor.octopus_energy_fan_club_{{ACCOUNT_ID}}_{{FAN_CLUB_ID}}_next_discount`

The next discount for the specified fan club, that differs from the current discount. If there is no next discount (e.g. discounts after now are of the same value as the current discount), then this will be reported as `unknown`/`none`.

| Attribute | Type | Description |
|-----------|------|-------------|
| `source` | `string` | The source of the discount |
| `start` | `datetime` | The date/time when the discount starts |
| `end` | `datetime` | The date/time when the discount ends |

## Discounts

`event.octopus_energy_fan_club_{{ACCOUNT_ID}}_{{FAN_CLUB_ID}}_discounts`

The state of this sensor states when the discounts were last retrieved. The attributes of this sensor exposes the available discounts for a given fan club source.

| Attribute | Type | Description |
|-----------|------|-------------|
| `discounts` | `array` | The list of past, present and future discounts |
| `account_id` | `string` | The id of the account the discounts are for |
| `source` | `string` | The source of the discounts (e.g. Fan #1) |

Each rate item has the following attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `start` | `datetime` | The date/time when the discount starts/started |
| `end` | `datetime` | The date/time when the discount ends/ended |
| `discount` | `float` | The value of the discount |
| `is_estimated` | `boolean` | Determines if the discount is estimated |
1 change: 1 addition & 0 deletions _docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Based on a request from [Octopus Energy](https://forum.octopus.energy/t/pending-
| Greenness Forecast | 180 | Doesn't change frequently |
| Free electricity sessions | 90 | Data is provided by my own [private API](https://github.com/BottlecapDave/OctopusEnergyApi) and there is usually at least half a day notice before the sessions which is why this is refreshed slightly less than saving sessions. |
| Heat Pump state | 1 | Data is updated frequently and doesn't seem to cause any issues around rate limits. This might change in the future. |
| Fan Club Discounts | 15 | As forecasts change frequently, trying to balance between updates and not overloading the API. |

If data cannot be refreshed for any reason (e.g. no internet or APIs are down), then the integration will attempt to retrieve data as soon as possible, slowly waiting longer between each attempt, to a maximum of 30 minutes between each attempt. Below is a rough example assuming the first (failed) scheduled refresh was at `10:35`.

Expand Down
5 changes: 5 additions & 0 deletions _docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Below are the main features of the integration
* [Wheel of fortune support](#wheel-of-fortune)
* [Greener days support](#greenness-forecast)
* [Heat Pump support](#heat-pumps)
* [Fan Club support](#fan-club)

## How to install

Expand Down Expand Up @@ -99,6 +100,10 @@ To support Octopus Energy's [greener days](https://octopus.energy/smart/greener-

To support heat pumps connected to Octopus Energy, like the [Cosy 6](https://octopus.energy/cosy-heat-pump/). [Full list of heat pump entities](./entities/heat_pump.md).

### Fan Club

To support the [Octopus Energy Fan Club](https://www.octopusenergygeneration.com/fan-club/). [Full list of fan club entities](./entities/fan_club.md).

## Target Rate Sensors

These sensors calculate the lowest continuous or intermittent rates **within a 24 hour period** or on a rolling basis and turn on when these periods are active. If you are targeting an export meter, then the sensors will calculate the highest continuous or intermittent rates **within a 24 hour period** or on a rolling basis and turn on when these periods are active.
Expand Down
23 changes: 23 additions & 0 deletions custom_components/octopus_energy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from .intelligent import get_intelligent_features, is_intelligent_product, mock_intelligent_device
from .config.rolling_target_rates import async_migrate_rolling_target_config
from .coordinators.heat_pump_configuration_and_status import HeatPumpCoordinatorResult, async_setup_heat_pump_coordinator
from .coordinators.fan_club_discounts import DiscountSource, FanClubDiscountCoordinatorResult, async_setup_fan_club_discounts_coordinator
from .fan_club import combine_discounts, mock_fan_club_forecast
from .api_client.fan_club import FanClubResponse

from .config.main import async_migrate_main_config
from .config.target_rates import async_migrate_target_config
Expand Down Expand Up @@ -60,6 +63,7 @@
CONFIG_MAIN_PRICE_CAP_SETTINGS,
CONFIG_VERSION,
DATA_DISCOVERY_MANAGER,
DATA_FAN_CLUB_DISCOUNTS,
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY,
DATA_CUSTOM_RATE_WEIGHTINGS_KEY,
DATA_HOME_PRO_CLIENT,
Expand Down Expand Up @@ -549,6 +553,25 @@ async def async_setup_dependencies(hass, config):

await async_setup_greenness_forecast_coordinator(hass, account_id)

# Setup Fan Club coordinator
mock_fan_club = account_debug_override.mock_fan_club if account_debug_override is not None else False
fan_club_response: FanClubResponse | None = None
if mock_fan_club:
fan_club_response = mock_fan_club_forecast()
else:
fan_club_response = await client.async_get_fan_club_discounts(account_id)

if fan_club_response is not None:
discounts: list[DiscountSource] = []
if fan_club_response is not None and fan_club_response.fanClubStatus is not None:
for item in fan_club_response.fanClubStatus:
discounts.append(DiscountSource(source=item.discountSource, discounts=combine_discounts(item)))

if (len(discounts) > 0):
# Make it old so we can force a refresh
hass.data[DOMAIN][account_id][DATA_FAN_CLUB_DISCOUNTS] = FanClubDiscountCoordinatorResult(now - timedelta(days=1), 1, discounts)
await async_setup_fan_club_discounts_coordinator(hass, account_id, mock_fan_club)

async def options_update_listener(hass, entry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
Expand Down
49 changes: 49 additions & 0 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .greenness_forecast import GreennessForecast
from .free_electricity_sessions import FreeElectricitySession, FreeElectricitySessionsResponse
from .heat_pump import HeatPumpResponse
from .fan_club import FanClubResponse

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -555,6 +556,29 @@
}}
'''

fan_club_discount_query = '''
query {{
fanClubStatus(accountNumber: "{account_id}") {{
discountSource
current {{
startAt
discount
}}
historic {{
startAt
discount
}}
forecast {{
baseTime
data {{
validTime
projectedDiscount
}}
}}
}}
}}
'''


user_agent_value = "bottlecapdave-ha-octopus-energy"

Expand Down Expand Up @@ -908,6 +932,31 @@ async def async_get_account(self, account_id):
raise TimeoutException()

return None

async def async_get_fan_club_discounts(self, account_id: str) -> FanClubResponse | None:
"""Get fan club discounts"""
await self.async_refresh_token()

try:
request_context = "fan-club-discounts"
client = self._create_client_session()
url = f'{self._base_url}/v1/graphql/'
payload = { "query": fan_club_discount_query.format(account_id=account_id) }
headers = { "Authorization": f"JWT {self._graphql_token}", integration_context_header: request_context }
async with client.post(url, json=payload, headers=headers) as fan_club_response:
response = await self.__async_read_response__(fan_club_response, url)
_LOGGER.debug(f'async_get_fan_club_discounts response: {response}')

if (response is not None
and "data" in response
and "fanClubStatus" in response["data"] ):
return FanClubResponse.parse_obj(response["data"])

return None

except TimeoutError:
_LOGGER.warning(f'Failed to connect. Timeout of {self._timeout} exceeded.')
raise TimeoutException()

async def async_get_heat_pump_configuration_and_status(self, account_id: str, euid: str):
"""Get a heat pump configuration and status"""
Expand Down
32 changes: 32 additions & 0 deletions custom_components/octopus_energy/api_client/fan_club.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from typing import List, Optional
from datetime import datetime

from pydantic import BaseModel


class DiscountPeriod(BaseModel):
startAt: datetime
discount: str


class ForecastData(BaseModel):
validTime: datetime
projectedDiscount: str


class ForecastInfo(BaseModel):
baseTime: datetime
data: List[ForecastData]


class FanClubStatusItem(BaseModel):
discountSource: str
current: DiscountPeriod
historic: List[DiscountPeriod]
forecast: Optional[ForecastInfo] = None


class FanClubResponse(BaseModel):
fanClubStatus: List[FanClubStatusItem]
5 changes: 5 additions & 0 deletions custom_components/octopus_energy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
REFRESH_RATE_IN_MINUTES_GREENNESS_FORECAST = 180
REFRESH_RATE_IN_MINUTES_HOME_PRO_CONSUMPTION = 0.17
REFRESH_RATE_IN_MINUTES_HEAT_PUMP = 1
REFRESH_RATE_IN_MINUTES_FAN_CLUB_DISCOUNTS = 15

CONFIG_VERSION = 8

Expand Down Expand Up @@ -153,6 +154,8 @@
DATA_FREE_ELECTRICITY_SESSIONS_COORDINATOR = "FREE_ELECTRICITY_SESSIONS_COORDINATOR"
DATA_CUSTOM_RATE_WEIGHTINGS_KEY = "DATA_CUSTOM_RATE_WEIGHTINGS_{}"
DATA_DISCOVERY_MANAGER = "DATA_DISCOVERY_MANAGER"
DATA_FAN_CLUB_DISCOUNTS_COORDINATOR = "FAN_CLUB_DISCOUNTS_COORDINATOR"
DATA_FAN_CLUB_DISCOUNTS = "FAN_CLUB_DISCOUNTS"

DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_KEY = "HEAT_PUMP_CONFIGURATION_AND_STATUS_{}"
DATA_HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR = "HEAT_PUMP_CONFIGURATION_AND_STATUS_COORDINATOR_{}"
Expand Down Expand Up @@ -205,6 +208,8 @@
EVENT_NEW_FREE_ELECTRICITY_SESSION = "octopus_energy_new_octoplus_free_electricity_session"
EVENT_ALL_FREE_ELECTRICITY_SESSIONS = "octopus_energy_all_octoplus_free_electricity_sessions"

EVENT_FAN_CLUB_DISCOUNTS = "octopus_energy_fan_club_discounts"

REPAIR_UNIQUE_RATES_CHANGED_KEY = "electricity_unique_rates_updated_{}"
REPAIR_INVALID_API_KEY = "invalid_api_key_{}"
REPAIR_ACCOUNT_NOT_FOUND = "account_not_found_{}"
Expand Down
Loading
Loading