Skip to content

Commit

Permalink
feat: Added sensor to show the current raw intelligent state (1 hour …
Browse files Browse the repository at this point in the history
…dev time)
  • Loading branch information
BottlecapDave committed Dec 20, 2024
1 parent 4dcc751 commit fb0f8e3
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 73 deletions.
17 changes: 15 additions & 2 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@
}}'''

intelligent_dispatches_query = '''query {{
devices(accountNumber: "{account_id}", deviceId: "{device_id}") {{
id
status {{
currentState
}}
}}
plannedDispatches(accountNumber: "{account_id}") {{
start
end
Expand Down Expand Up @@ -1258,22 +1264,29 @@ async def async_get_gas_standing_charge(self, product_code: str, tariff_code: st
_LOGGER.warning(f'Failed to connect. Timeout of {self._timeout} exceeded.')
raise TimeoutException()

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

try:
client = self._create_client_session()
url = f'{self._base_url}/v1/graphql/'
# Get account response
payload = { "query": intelligent_dispatches_query.format(account_id=account_id) }
payload = { "query": intelligent_dispatches_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_dispatches: {response_body}')

current_state = None
if (response_body is not None and "data" in response_body and "devices" in response_body["data"]):
for device in response_body["data"]["devices"]:
if device["id"] == device_id:
current_state = device["status"]["currentState"]

if (response_body is not None and "data" in response_body):
return IntelligentDispatches(
current_state,
list(map(lambda ev: IntelligentDispatchItem(
as_utc(parse_datetime(ev["start"])),
as_utc(parse_datetime(ev["end"])),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ def __init__(
self.location = location

class IntelligentDispatches:
current_state: str | None
planned: list[IntelligentDispatchItem]
completed: list[IntelligentDispatchItem]

def __init__(
self,
current_state: str | None,
planned: list[IntelligentDispatchItem],
completed: list[IntelligentDispatchItem]
):
self.current_state = current_state
self.planned = planned
self.completed = completed
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async def async_refresh_intelligent_dispatches(
raised_exception = None
if has_intelligent_tariff(current, account_info) and intelligent_device is not None:
try:
dispatches = await client.async_get_intelligent_dispatches(account_id)
dispatches = await client.async_get_intelligent_dispatches(account_id, intelligent_device.id)
_LOGGER.debug(f'Intelligent dispatches retrieved for account {account_id}')
except Exception as e:
if isinstance(e, ApiException) == False:
Expand Down
19 changes: 14 additions & 5 deletions custom_components/octopus_energy/intelligent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
def mock_intelligent_dispatches() -> IntelligentDispatches:
planned: list[IntelligentDispatchItem] = []
completed: list[IntelligentDispatchItem] = []
current_state = "SMART_CONTROL_CAPABLE"

dispatches = [
IntelligentDispatchItem(
Expand Down Expand Up @@ -71,12 +72,18 @@ def mock_intelligent_dispatches() -> IntelligentDispatches:
)

for dispatch in dispatches:
if utcnow() >= dispatch.start and utcnow() <= dispatch.end:
if dispatch.source == INTELLIGENT_SOURCE_SMART_CHARGE:
current_state = "SMART_CONTROL_IN_PROGRESS"
elif dispatch.source == INTELLIGENT_SOURCE_BUMP_CHARGE:
current_state = "BOOSTING"

if (dispatch.end > utcnow()):
planned.append(dispatch)
else:
completed.append(dispatch)

return IntelligentDispatches(planned, completed)
return IntelligentDispatches(current_state, planned, completed)

def mock_intelligent_settings():
return IntelligentSettings(
Expand Down Expand Up @@ -206,13 +213,15 @@ def __init__(self,
charge_limit_supported: bool,
planned_dispatches_supported: bool,
ready_time_supported: bool,
smart_charge_supported: bool):
smart_charge_supported: bool,
current_state_supported: bool):
self.is_default_features = is_default_features
self.bump_charge_supported = bump_charge_supported
self.charge_limit_supported = charge_limit_supported
self.planned_dispatches_supported = planned_dispatches_supported
self.ready_time_supported = ready_time_supported
self.smart_charge_supported = smart_charge_supported
self.current_state_supported = current_state_supported

FULLY_SUPPORTED_INTELLIGENT_PROVIDERS = [
"DAIKIN",
Expand All @@ -236,8 +245,8 @@ def __init__(self,
def get_intelligent_features(provider: str) -> IntelligentFeatures:
normalised_provider = provider.upper() if provider is not None else None
if normalised_provider is not None and normalised_provider in FULLY_SUPPORTED_INTELLIGENT_PROVIDERS:
return IntelligentFeatures(False, True, True, True, True, True)
return IntelligentFeatures(False, True, True, True, True, True, True)
elif normalised_provider == "OHME":
return IntelligentFeatures(False, False, False, False, False, False)
return IntelligentFeatures(False, False, False, False, False, False, False)

return IntelligentFeatures(True, False, False, False, False, False)
return IntelligentFeatures(True, False, False, False, False, False, False)
111 changes: 111 additions & 0 deletions custom_components/octopus_energy/intelligent/current_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import logging

from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import generate_entity_id

from homeassistant.util.dt import (now)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity
)
from homeassistant.components.sensor import (
RestoreSensor,
)

from .base import OctopusEnergyIntelligentSensor
from ..coordinators.intelligent_dispatches import IntelligentDispatchesCoordinatorResult
from ..utils.attributes import dict_to_typed_dict
from ..api_client.intelligent_device import IntelligentDevice

_LOGGER = logging.getLogger(__name__)

class OctopusEnergyIntelligentCurrentState(CoordinatorEntity, OctopusEnergyIntelligentSensor, RestoreSensor):
"""Sensor for determining the current intelligent state."""

_unrecorded_attributes = frozenset({"known_states"})

known_states = [
"AUTHENTICATION_PENDING",
"AUTHENTICATION_FAILED",
"AUTHENTICATION_COMPLETE",
"TEST_CHARGE_IN_PROGRESS",
"TEST_CHARGE_FAILED",
"TEST_CHARGE_NOT_AVAILABLE",
"SETUP_COMPLETE",
"SMART_CONTROL_CAPABLE",
"SMART_CONTROL_IN_PROGRESS",
"BOOSTING",
"SMART_CONTROL_OFF",
"SMART_CONTROL_NOT_AVAILABLE",
"LOST_CONNECTION",
"RETIRED",
]

def __init__(self, hass: HomeAssistant, coordinator, device: IntelligentDevice, account_id: str):
"""Init sensor."""

CoordinatorEntity.__init__(self, coordinator)
OctopusEnergyIntelligentSensor.__init__(self, device)

self._account_id = account_id
self._state = None
self._attributes = {
"known_states": self.known_states
}

self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass)

@property
def unique_id(self):
"""The id of the sensor."""
return f"octopus_energy_{self._account_id}_intelligent_state"

@property
def name(self):
"""Name of the sensor."""
return f"Intelligent State ({self._account_id})"

@property
def icon(self):
"""Icon of the sensor."""
return "mdi:power-plug-battery"

@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
return self._attributes

@property
def native_value(self):
return self._state

@callback
def _handle_coordinator_update(self) -> None:
"""Retrieve the current rate for the sensor."""
# Find the current rate. We only need to do this every half an hour
current = now()
result: IntelligentDispatchesCoordinatorResult = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None
if (result is not None and result.dispatches is not None):
_LOGGER.debug(f"Updating OctopusEnergyIntelligentCurrentState for '{self._device.id}'")

self._state = result.dispatches.current_state
else:
self._state = None

self._attributes = dict_to_typed_dict(self._attributes)
super()._handle_coordinator_update()

async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
# If not None, we got an initial value.
await super().async_added_to_hass()
state = await self.async_get_last_state()

if state is not None and self._state is None:
self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state
self._attributes = dict_to_typed_dict(state.attributes, [])
self._attributes["known_states"] = self.known_states
_LOGGER.debug(f'Restored OctopusEnergyIntelligentCurrentState state: {self._state}')
8 changes: 7 additions & 1 deletion custom_components/octopus_energy/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import timedelta
from custom_components.octopus_energy.api_client.intelligent_device import IntelligentDevice
import voluptuous as vol
import logging

Expand Down Expand Up @@ -57,6 +56,9 @@
from .diagnostics_entities.intelligent_dispatches_data_last_retrieved import OctopusEnergyIntelligentDispatchesDataLastRetrieved
from .diagnostics_entities.intelligent_settings_data_last_retrieved import OctopusEnergyIntelligentSettingsDataLastRetrieved
from .diagnostics_entities.free_electricity_sessions_data_last_retrieved import OctopusEnergyFreeElectricitySessionsDataLastRetrieved
from .api_client.intelligent_device import IntelligentDevice
from .intelligent.current_state import OctopusEnergyIntelligentCurrentState
from .intelligent import get_intelligent_features

from .utils.debug_overrides import async_get_debug_override

Expand Down Expand Up @@ -280,6 +282,10 @@ async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_ent
intelligent_dispatches_coordinator = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] if DATA_INTELLIGENT_DISPATCHES_COORDINATOR in hass.data[DOMAIN][account_id] else None
if intelligent_dispatches_coordinator is not None:
entities.append(OctopusEnergyIntelligentDispatchesDataLastRetrieved(hass, intelligent_dispatches_coordinator, account_id))

intelligent_features = get_intelligent_features(intelligent_device.provider)
if intelligent_features.current_state_supported:
entities.append(OctopusEnergyIntelligentCurrentState(hass, intelligent_dispatches_coordinator, intelligent_device, account_id))

intelligent_settings_coordinator = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_SETTINGS_COORDINATOR] if DATA_INTELLIGENT_SETTINGS_COORDINATOR in hass.data[DOMAIN][account_id] else None
if intelligent_settings_coordinator is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from custom_components.octopus_energy.api_client import OctopusEnergyApiClient

@pytest.mark.asyncio
async def test_when_get_intelligent_dispatches_is_called_for_account_on_different_tariff_then_results_are_returned():
async def test_when_get_intelligent_dispatches_is_called_for_account_on_different_tariff_then_exception_is_raised():
# Arrange
context = get_test_context()

Expand All @@ -14,7 +14,7 @@ async def test_when_get_intelligent_dispatches_is_called_for_account_on_differen
# Act
exception_raised = False
try:
await client.async_get_intelligent_dispatches(account_id)
await client.async_get_intelligent_dispatches(account_id, "123")
except:
exception_raised = True

Expand Down
Loading

0 comments on commit fb0f8e3

Please sign in to comment.