Skip to content

Commit

Permalink
feat: Migrated to use new intelligent APIs (2.5 hours dev time)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

vehicle_battery_size_in_kwh and charge_point_power_in_kw are no longer both provided together. It's either one or the other depending on which part is integrated into OE intelligent. This is due to not being available in the new APIs.
  • Loading branch information
BottlecapDave authored Sep 18, 2024
1 parent ec116e0 commit 80eb8bf
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 117 deletions.
2 changes: 1 addition & 1 deletion _docs/entities/gas.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ Each charge item has the following attributes

The total consumption reported by the meter for for all time in m3. This is calculated/estimated using your set [calorific value](../setup/account.md#calorific-value) from the kWh data reported by Octopus Energy. This will try and update every minute for Home Mini and every 10 seconds for Home Pro.

!!! info
!!! warning

Because this is calculated from your set calorific value across the lifetime of your meter, the value will not be 100% accurate due to calorific values changing over time which cannot be captured.

Expand Down
2 changes: 1 addition & 1 deletion custom_components/octopus_energy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ async def async_setup_dependencies(hass, config):

await async_setup_intelligent_dispatches_coordinator(hass, account_id)

await async_setup_intelligent_settings_coordinator(hass, account_id)
await async_setup_intelligent_settings_coordinator(hass, account_id, intelligent_device.id if intelligent_device is not None else None)

await async_setup_saving_sessions_coordinators(hass, account_id)

Expand Down
213 changes: 146 additions & 67 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,17 @@

intelligent_dispatches_query = '''query {{
plannedDispatches(accountNumber: "{account_id}") {{
startDt
endDt
start
end
delta
meta {{
source
location
}}
}}
completedDispatches(accountNumber: "{account_id}") {{
startDt
endDt
start
end
delta
meta {{
source
Expand All @@ -135,27 +135,65 @@
}}'''

intelligent_device_query = '''query {{
registeredKrakenflexDevice(accountNumber: "{account_id}") {{
krakenflexDeviceId
provider
vehicleMake
vehicleModel
vehicleBatterySizeInKwh
chargePointMake
chargePointModel
chargePointPowerInKw
electricVehicles {{
make
models {{
model
batterySize
}}
}}
chargePointVariants {{
make
models {{
model
powerInKw
}}
}}
devices(accountNumber: "{account_id}") {{
id
provider
deviceType
status {{
current
}}
__typename
... on SmartFlexVehicle {{
make
model
}}
... on SmartFlexChargePoint {{
make
model
}}
}}
}}'''

intelligent_settings_query = '''query vehicleChargingPreferences {{
vehicleChargingPreferences(accountNumber: "{account_id}") {{
weekdayTargetTime
weekdayTargetSoc
weekendTargetTime
weekendTargetSoc
}}
registeredKrakenflexDevice(accountNumber: "{account_id}") {{
suspended
intelligent_settings_query = '''query {{
devices(accountNumber: "{account_id}", deviceId: "{device_id}") {{
id
status {{
isSuspended
}}
... on SmartFlexVehicle {{
chargingPreferences {{
weekdayTargetTime
weekdayTargetSoc
weekendTargetTime
weekendTargetSoc
minimumSoc
maximumSoc
}}
}}
... on SmartFlexChargePoint {{
chargingPreferences {{
weekdayTargetTime
weekdayTargetSoc
weekendTargetTime
weekendTargetSoc
minimumSoc
maximumSoc
}}
}}
}}
}}'''

Expand Down Expand Up @@ -1002,8 +1040,8 @@ async def async_get_intelligent_dispatches(self, account_id: str):
if (response_body is not None and "data" in response_body):
return IntelligentDispatches(
list(map(lambda ev: IntelligentDispatchItem(
as_utc(parse_datetime(ev["startDt"])),
as_utc(parse_datetime(ev["endDt"])),
as_utc(parse_datetime(ev["start"])),
as_utc(parse_datetime(ev["end"])),
float(ev["delta"]) if "delta" in ev and ev["delta"] is not None else None,
ev["meta"]["source"] if "meta" in ev and "source" in ev["meta"] else None,
ev["meta"]["location"] if "meta" in ev and "location" in ev["meta"] else None,
Expand All @@ -1012,8 +1050,8 @@ async def async_get_intelligent_dispatches(self, account_id: str):
else [])
),
list(map(lambda ev: IntelligentDispatchItem(
as_utc(parse_datetime(ev["startDt"])),
as_utc(parse_datetime(ev["endDt"])),
as_utc(parse_datetime(ev["start"])),
as_utc(parse_datetime(ev["end"])),
float(ev["delta"]) if "delta" in ev and ev["delta"] is not None else None,
ev["meta"]["source"] if "meta" in ev and "source" in ev["meta"] else None,
ev["meta"]["location"] if "meta" in ev and "location" in ev["meta"] else None,
Expand All @@ -1030,39 +1068,41 @@ async def async_get_intelligent_dispatches(self, account_id: str):
_LOGGER.warning(f'Failed to connect. Timeout of {self._timeout} exceeded.')
raise TimeoutException()

async def async_get_intelligent_settings(self, account_id: str):
async def async_get_intelligent_settings(self, account_id: str, device_id: str):
"""Get the user's intelligent settings"""
await self.async_refresh_token()

try:
client = self._create_client_session()
url = f'{self._base_url}/v1/graphql/'
payload = { "query": intelligent_settings_query.format(account_id=account_id) }
payload = { "query": intelligent_settings_query.format(account_id=account_id, device_id=device_id) }
headers = { "Authorization": f"JWT {self._graphql_token}" }
async with client.post(url, json=payload, headers=headers) as response:
response_body = await self.__async_read_response__(response, url)
_LOGGER.debug(f'async_get_intelligent_settings: {response_body}')

_LOGGER.debug(f'Intelligent Settings: {response_body}')
if (response_body is not None and "data" in response_body):

return IntelligentSettings(
response_body["data"]["registeredKrakenflexDevice"]["suspended"] == False
if "registeredKrakenflexDevice" in response_body["data"] and "suspended" in response_body["data"]["registeredKrakenflexDevice"]
else None,
int(response_body["data"]["vehicleChargingPreferences"]["weekdayTargetSoc"])
if "vehicleChargingPreferences" in response_body["data"] and "weekdayTargetSoc" in response_body["data"]["vehicleChargingPreferences"]
else None,
int(response_body["data"]["vehicleChargingPreferences"]["weekendTargetSoc"])
if "vehicleChargingPreferences" in response_body["data"] and "weekendTargetSoc" in response_body["data"]["vehicleChargingPreferences"]
else None,
self.__ready_time_to_time__(response_body["data"]["vehicleChargingPreferences"]["weekdayTargetTime"])
if "vehicleChargingPreferences" in response_body["data"] and "weekdayTargetTime" in response_body["data"]["vehicleChargingPreferences"]
else None,
self.__ready_time_to_time__(response_body["data"]["vehicleChargingPreferences"]["weekendTargetTime"])
if "vehicleChargingPreferences" in response_body["data"] and "weekendTargetTime" in response_body["data"]["vehicleChargingPreferences"]
else None
)
if (response_body is not None and "data" in response_body and "devices" in response_body["data"]):

devices = list(response_body["data"]["devices"])
if len(devices) == 1:
smart_charge = devices[0]["status"]["isSuspended"] == 'false' if "status" in devices[0] and "isSuspended" in devices[0]["status"] else None
charging_preferences = devices[0]["chargingPreferences"] if "chargingPreferences" in devices[0] else None
return IntelligentSettings(
smart_charge,
int(charging_preferences["weekdayTargetSoc"])
if charging_preferences is not None and "weekdayTargetSoc" in charging_preferences
else None,
int(charging_preferences["weekendTargetSoc"])
if charging_preferences is not None and "weekendTargetSoc" in charging_preferences
else None,
self.__ready_time_to_time__(charging_preferences["weekdayTargetTime"])
if charging_preferences is not None and "weekdayTargetTime" in charging_preferences
else None,
self.__ready_time_to_time__(charging_preferences["weekendTargetTime"])
if charging_preferences is not None and "weekendTargetTime" in charging_preferences
else None
)
else:
_LOGGER.error("Failed to retrieve intelligent settings")

Expand All @@ -1075,22 +1115,23 @@ async def async_get_intelligent_settings(self, account_id: str):
def __ready_time_to_time__(self, time_str: str) -> time:
if time_str is not None:
parts = time_str.split(':')
if len(parts) != 2:
if len(parts) != 3:
raise Exception(f"Unexpected number of parts in '{time_str}'")

return time(int(parts[0]), int(parts[1]))
return time(int(parts[0]), int(parts[1]), int(parts[2]))

return None

async def async_update_intelligent_car_target_percentage(
self,
account_id: str,
device_id: str,
target_percentage: int
):
"""Update a user's intelligent car target percentage"""
await self.async_refresh_token()

settings = await self.async_get_intelligent_settings(account_id)
settings = await self.async_get_intelligent_settings(account_id, device_id)

try:
client = self._create_client_session()
Expand All @@ -1112,13 +1153,15 @@ async def async_update_intelligent_car_target_percentage(
raise TimeoutException()

async def async_update_intelligent_car_target_time(
self, account_id: str,
self,
account_id: str,
device_id: str,
target_time: time,
):
"""Update a user's intelligent car target time"""
await self.async_refresh_token()

settings = await self.async_get_intelligent_settings(account_id)
settings = await self.async_get_intelligent_settings(account_id, device_id)

try:
client = self._create_client_session()
Expand Down Expand Up @@ -1236,22 +1279,58 @@ async def async_get_intelligent_device(self, account_id: str) -> IntelligentDevi
response_body = await self.__async_read_response__(response, url)
_LOGGER.debug(f'async_get_intelligent_device: {response_body}')

if (response_body is not None and "data" in response_body and
"registeredKrakenflexDevice" in response_body["data"]):
device = response_body["data"]["registeredKrakenflexDevice"]
if device["krakenflexDeviceId"] is not None:
return IntelligentDevice(
device["krakenflexDeviceId"],
result = []
if (response_body is not None and "data" in response_body and "devices" in response_body["data"]):
devices: list = response_body["data"]["devices"]

for device in devices:
if (device["deviceType"] != "ELECTRIC_VEHICLES" or device["status"]["current"] != "LIVE"):
continue

is_charger = device["__typename"] == "SmartFlexChargePoint"

make = device["make"]
model = device["model"]
vehicleBatterySizeInKwh = None
chargePointPowerInKw = None

if is_charger:
if "chargePointVariants" in response_body["data"] and response_body["data"]["chargePointVariants"] is not None:
for charger in response_body["data"]["chargePointVariants"]:
if charger["make"] == make:
if "models" in charger and charger["models"] is not None:
for charger_model in charger["models"]:
if charger_model["model"] == model:
chargePointPowerInKw = float(charger_model["powerInKw"]) if "powerInKw" in charger_model and charger_model["powerInKw"] is not None else 0
break

break
else:
if "electricVehicles" in response_body["data"] and response_body["data"]["electricVehicles"] is not None:
for charger in response_body["data"]["electricVehicles"]:
if charger["make"] == make:
if "models" in charger and charger["models"] is not None:
for charger_model in charger["models"]:
if charger_model["model"] == model:
vehicleBatterySizeInKwh = float(charger_model["batterySize"]) if "batterySize" in charger_model and charger_model["batterySize"] is not None else 0
break

break

result.append(IntelligentDevice(
device["id"],
device["provider"],
device["vehicleMake"],
device["vehicleModel"],
float(device["vehicleBatterySizeInKwh"]) if "vehicleBatterySizeInKwh" in device and device["vehicleBatterySizeInKwh"] is not None else None,
device["chargePointMake"],
device["chargePointModel"],
float(device["chargePointPowerInKw"]) if "chargePointPowerInKw" in device and device["chargePointPowerInKw"] is not None else None
)
else:
_LOGGER.debug('Skipping intelligent device as id is not available')
make,
model,
vehicleBatterySizeInKwh,
chargePointPowerInKw,
is_charger
))

if len(result) > 1:
_LOGGER.warning("Multiple intelligent devices discovered. Picking first one")

return result[0] if len(result) > 0 else None
else:
_LOGGER.error("Failed to retrieve intelligent device")

Expand Down
31 changes: 10 additions & 21 deletions custom_components/octopus_energy/api_client/intelligent_device.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
class IntelligentDevice:
krakenflexDeviceId: str
provider: str
vehicleMake:str
vehicleModel: str
vehicleBatterySizeInKwh: float | None
chargePointMake: str
chargePointModel: str
chargePointPowerInKw: float | None

def __init__(
self,
krakenflexDeviceId: str,
id: str,
provider: str,
vehicleMake:str,
vehicleModel: str,
make:str,
model: str,
vehicleBatterySizeInKwh: float | None,
chargePointMake: str,
chargePointModel: str,
chargePointPowerInKw: float | None
chargePointPowerInKw: float | None,
is_charger: bool
):
self.krakenflexDeviceId = krakenflexDeviceId
self.id = id
self.provider = provider
self.vehicleMake = vehicleMake
self.vehicleModel = vehicleModel
self.make = make
self.model = model
self.vehicleBatterySizeInKwh = vehicleBatterySizeInKwh
self.chargePointMake = chargePointMake
self.chargePointModel = chargePointModel
self.chargePointPowerInKw = chargePointPowerInKw
self.chargePointPowerInKw = chargePointPowerInKw
self.is_charger = is_charger
Loading

0 comments on commit 80eb8bf

Please sign in to comment.