Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
notes
test.py
test.py
documents/investigation_plan.md
.github/copilot-instructions.md
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
# Eon Next

This is a custom component for Home Assistant which integrates with the Eon Next API and gets all the meter readings from your accounts.
This is a custom component for Home Assistant which integrates with the Eon Next API to retrieve meter readings and smart charging schedules from your Eon Next accounts.

## Features

### Meter Readings
A sensor will be created for each meter showing:

- The latest reading
- The date the latest reading was taken
- **Latest Reading**: The most recent consumption value
- **Reading Date**: When the latest reading was taken

For electric meters, readings are displayed in kWh. For gas meters, readings are in m³.

An additional sensor is created for gas meters showing the latest reading converted to kWh using standard calorific values.

### Smart Charging (EV Chargers)
For each connected smart charger, the following sensors are created:

- **Next Charge Start**: Scheduled start time of the next charging slot
- **Next Charge End**: Scheduled end time of the next charging slot
- **Next Charge Start 2**: Scheduled start time of the second charging slot
- **Next Charge End 2**: Scheduled end time of the second charging slot
- **Smart Charging Schedule**: Full schedule data for the charger

These sensors allow you to monitor and automate your EV charging based on Eon Next's smart charging recommendations.

### Tariff Information
For each account, the following tariff sensors are created:

- **Tariff Name**: The display name of your active tariff
- **Standing Charge**: Daily standing charge in GBP/day
- **Unit Rate**: Energy unit rate in GBP/kWh

These sensors include additional attributes such as tariff code, valid dates, and meter point information.

For electric meters the readings are in kWh. For gas meter the readings are in m³.
### Saving Sessions
If your account participates in saving sessions (similar to Octopus Energy's scheme):

An additional sensor is created for gas meters showing the latest reading in kWh.
- **Saving Sessions**: Count of active and upcoming saving sessions, with full session details in attributes including start/end times, reward amounts, and session status


## Installation
Expand Down
4 changes: 1 addition & 3 deletions custom_components/eon_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ async def async_setup_entry(hass, entry):

hass.data[DOMAIN][entry.entry_id] = api

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])

return True

Expand Down
117 changes: 114 additions & 3 deletions custom_components/eon_next/eonnext.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#!/usr/bin/env python3

import logging
import aiohttp
import datetime

_LOGGER = logging.getLogger(__name__)

METER_TYPE_GAS = "gas"
METER_TYPE_ELECTRIC = "electricity"
METER_TYPE_EV = "ev"
METER_TYPE_UNKNOWN = "unknown"


Expand All @@ -19,6 +23,8 @@ def __init__(self):

def _json_contains_key_chain(self, data: dict, key_chain: list) -> bool:
for key in key_chain:
if data is None:
return False
if key in data:
data = data[key]
else:
Expand Down Expand Up @@ -92,18 +98,30 @@ async def __auth_token(self) -> str:


async def _graphql_post(self, operation: str, query: str, variables: dict={}, authenticated: bool = True) -> dict:
use_headers = {}
use_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}

if authenticated == True:
use_headers['authorization'] = "JWT " + await self.__auth_token()

payload = {"operationName": operation, "variables": variables, "query": query}
_LOGGER.debug(f"GraphQL Payload: {payload}")

async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.eonnext-kraken.energy/v1/graphql/",
json={"operationName": operation, "variables": variables, "query": query},
json=payload,
headers=use_headers
) as response:
return await response.json()
try:
json_data = await response.json()
_LOGGER.debug(f"GraphQL Response for {operation}: {json_data}")
return json_data
except Exception as e:
text = await response.text()
_LOGGER.error(f"Failed to parse JSON response. Status: {response.status}. Body: {text}")
raise e


async def login_with_username_and_password(self, username: str, password: str, initialise: bool = True) -> bool:
Expand Down Expand Up @@ -185,6 +203,9 @@ async def __init_accounts(self):

account = EnergyAccount(self, account_number)
await account._load_meters()
await account._load_ev_chargers()
await account._load_tariff_data()
await account._load_saving_sessions()

self.accounts.append(account)

Expand All @@ -196,8 +217,70 @@ class EnergyAccount:
def __init__(self, api: EonNext, account_number: str):
self.api = api
self.account_number = account_number
self.ev_chargers = []
self.tariff_data = None
self.saving_sessions = []
self.postcode = ""


async def _load_tariff_data(self):
"""Load active tariff/agreement details for this account"""
result = await self.api._graphql_post(
"getAccountAgreements",
"query getAccountAgreements($accountNumber: String!) { properties(accountNumber: $accountNumber) { electricityMeterPoints { mpan agreements { id validFrom validTo tariff { __typename ... on TariffType { displayName fullName tariffCode } ... on StandardTariff { unitRate standingCharge } ... on PrepayTariff { unitRate standingCharge } ... on HalfHourlyTariff { unitRates { value } standingCharge } } } } } }",
{
"accountNumber": self.account_number
}
)

self.tariff_data = []
if self.api._json_contains_key_chain(result, ["data", "properties"]):
for prop in result['data']['properties']:
if 'electricityMeterPoints' in prop:
for point in prop['electricityMeterPoints']:
mpan = point.get('mpan')
if 'agreements' in point:
for agreement in point['agreements']:
# Inject meterPoint info for sensor compatibility
agreement['meterPoint'] = {'mpan': mpan}
self.tariff_data.append(agreement)


async def _load_saving_sessions(self):
"""Load saving session data (similar to Octopus Saving Sessions)"""
result = await self.api._graphql_post(
"getSavingSessions",
"query getSavingSessions($postcode: String!) { appSessions(postcode: $postcode) { edges { node { id startedAt __typename } } } }",
{
"postcode": self.postcode
}
)

if self.api._json_contains_key_chain(result, ["data", "appSessions", "edges"]):
self.saving_sessions = [edge['node'] for edge in result['data']['appSessions']['edges']]
else:
self.saving_sessions = []


async def _load_ev_chargers(self):
result = await self.api._graphql_post(
"getAccountDevices",
"query getAccountDevices($accountNumber: String!) {\n devices(accountNumber: $accountNumber) {\n id\n provider\n deviceType\n status {\n current\n }\n __typename\n ... on SmartFlexVehicle {\n make\n model\n }\n ... on SmartFlexChargePoint {\n make\n model\n }\n }\n}\n",
{
"accountNumber": self.account_number
}
)

if self.api._json_contains_key_chain(result, ["data", "devices"]) == True:
for device in result['data']['devices']:
# We are interested in devices that are active and are either vehicles or chargers
# For now, we'll treat them all as "SmartCharging" entities
if device.get('status', {}).get('current') == 'LIVE':
name = f"{device.get('make', 'Unknown')} {device.get('model', 'Device')}"
charger = SmartCharging(self, device['id'], name)
self.ev_chargers.append(charger)


async def _load_meters(self):
result = await self.api._graphql_post(
"getAccountMeterSelector",
Expand All @@ -213,6 +296,7 @@ async def _load_meters(self):

self.meters = []
for property in result['data']['properties']:
self.postcode = property.get('postcode')

for electricity_point in property['electricityMeterPoints']:
for meter_config in electricity_point['meters']:
Expand Down Expand Up @@ -361,3 +445,30 @@ async def get_latest_reading_kwh(self) -> int:
kwh = kwh / 3.6

return round(kwh)


class SmartCharging(EnergyMeter):

def __init__(self, account: EnergyAccount, meter_id: str, serial: str):
super().__init__(account, meter_id, serial)
self.type = METER_TYPE_EV
self.schedule = None


async def _update(self):
result = await self.api._graphql_post(
"getSmartChargingSchedule",
"query getSmartChargingSchedule($deviceId: String!) {\n flexPlannedDispatches(deviceId: $deviceId) {\n start\n end\n type\n energyAddedKwh\n }\n}\n",
{
"deviceId": self.meter_id
}
)

if self.api._json_contains_key_chain(result, ["data", "flexPlannedDispatches"]) == True:
self.schedule = result['data']['flexPlannedDispatches']
self.last_updated = datetime.datetime.now()

async def get_schedule(self):
await self.update()
return self.schedule

Loading