python-hilo 2025.11.1__tar.gz → 2025.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/PKG-INFO +1 -1
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/api.py +6 -4
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/const.py +2 -1
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/devices.py +2 -2
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/event.py +9 -2
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/graphql.py +4 -4
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/websocket.py +11 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyproject.toml +1 -1
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/LICENSE +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/README.md +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/__init__.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/__init__.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/climate.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/graphql_value_mapper.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/light.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/sensor.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/device/switch.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/exceptions.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/oauth2helper.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/util/__init__.py +0 -0
- {python_hilo-2025.11.1 → python_hilo-2025.12.2}/pyhilo/util/state.py +0 -0
|
@@ -12,6 +12,7 @@ from urllib import parse
|
|
|
12
12
|
from aiohttp import ClientSession
|
|
13
13
|
from aiohttp.client_exceptions import ClientResponseError
|
|
14
14
|
import backoff
|
|
15
|
+
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
|
15
16
|
|
|
16
17
|
from pyhilo.const import (
|
|
17
18
|
ANDROID_CLIENT_ENDPOINT,
|
|
@@ -66,7 +67,7 @@ class API:
|
|
|
66
67
|
self,
|
|
67
68
|
*,
|
|
68
69
|
session: ClientSession,
|
|
69
|
-
oauth_session,
|
|
70
|
+
oauth_session: OAuth2Session,
|
|
70
71
|
request_retries: int = REQUEST_RETRY,
|
|
71
72
|
log_traces: bool = False,
|
|
72
73
|
) -> None:
|
|
@@ -97,7 +98,7 @@ class API:
|
|
|
97
98
|
cls,
|
|
98
99
|
*,
|
|
99
100
|
session: ClientSession,
|
|
100
|
-
oauth_session,
|
|
101
|
+
oauth_session: OAuth2Session,
|
|
101
102
|
request_retries: int = REQUEST_RETRY,
|
|
102
103
|
log_traces: bool = False,
|
|
103
104
|
) -> API:
|
|
@@ -382,11 +383,12 @@ class API:
|
|
|
382
383
|
# Create both websocket clients
|
|
383
384
|
# ic-dev21 need to work on this as it can't lint as is, may need to
|
|
384
385
|
# instantiate differently
|
|
385
|
-
|
|
386
|
+
# TODO: fix type ignore after refactor
|
|
387
|
+
self.websocket_devices = WebsocketClient(self.websocket_manager.devicehub) # type: ignore
|
|
386
388
|
|
|
387
389
|
# For backward compatibility during the transition to challengehub websocket
|
|
388
390
|
self.websocket = self.websocket_devices
|
|
389
|
-
self.websocket_challenges = WebsocketClient(self.websocket_manager.challengehub)
|
|
391
|
+
self.websocket_challenges = WebsocketClient(self.websocket_manager.challengehub) # type: ignore
|
|
390
392
|
|
|
391
393
|
async def refresh_ws_token(self) -> None:
|
|
392
394
|
"""Refresh the websocket token."""
|
|
@@ -7,7 +7,7 @@ import aiohttp
|
|
|
7
7
|
LOG: Final = logging.getLogger(__package__)
|
|
8
8
|
DEFAULT_STATE_FILE: Final = "hilo_state.yaml"
|
|
9
9
|
REQUEST_RETRY: Final = 9
|
|
10
|
-
PYHILO_VERSION: Final = "2025.
|
|
10
|
+
PYHILO_VERSION: Final = "2025.12.02"
|
|
11
11
|
# TODO: Find a way to keep previous line in sync with pyproject.toml automatically
|
|
12
12
|
|
|
13
13
|
CONTENT_TYPE_FORM: Final = "application/x-www-form-urlencoded"
|
|
@@ -32,6 +32,7 @@ API_GD_SERVICE_ENDPOINT: Final = f"/GDService/{API_END}"
|
|
|
32
32
|
API_NOTIFICATIONS_ENDPOINT: Final = "/Notifications"
|
|
33
33
|
API_EVENTS_ENDPOINT: Final = "/Notifications"
|
|
34
34
|
API_REGISTRATION_ENDPOINT: Final = f"{API_NOTIFICATIONS_ENDPOINT}/Registrations"
|
|
35
|
+
PLATFORM_HOST: Final = "platform.hiloenergie.com"
|
|
35
36
|
|
|
36
37
|
API_REGISTRATION_HEADERS: Final = {
|
|
37
38
|
"AppId": ANDROID_PKG_NAME,
|
|
@@ -55,7 +55,7 @@ class Devices:
|
|
|
55
55
|
"""Uses the dict from parse_values_received to map the values to devices."""
|
|
56
56
|
updated_devices = []
|
|
57
57
|
for reading in readings:
|
|
58
|
-
device_identifier = reading.device_id
|
|
58
|
+
device_identifier: Union[int, str] = reading.device_id
|
|
59
59
|
if device_identifier == 0:
|
|
60
60
|
device_identifier = reading.hilo_id
|
|
61
61
|
if device := self.find_device(device_identifier):
|
|
@@ -69,7 +69,7 @@ class Devices:
|
|
|
69
69
|
)
|
|
70
70
|
return updated_devices
|
|
71
71
|
|
|
72
|
-
def find_device(self, device_identifier: int | str) -> HiloDevice:
|
|
72
|
+
def find_device(self, device_identifier: int | str) -> HiloDevice | None:
|
|
73
73
|
"""Makes sure the devices received have an identifier, this means some need to be hardcoded
|
|
74
74
|
like the unknown power meter.
|
|
75
75
|
"""
|
|
@@ -27,7 +27,7 @@ class Event:
|
|
|
27
27
|
def __init__(self, **event: dict[str, Any]):
|
|
28
28
|
"""Initialize."""
|
|
29
29
|
self._convert_phases(cast(dict[str, Any], event.get("phases")))
|
|
30
|
-
params: dict[str, Any] = event.get("parameters"
|
|
30
|
+
params: dict[str, Any] = event.get("parameters") or {}
|
|
31
31
|
devices: list[dict[str, Any]] = params.get("devices", [])
|
|
32
32
|
consumption: dict[str, Any] = event.get("consumption", {})
|
|
33
33
|
allowed_wH: int = consumption.get("baselineWh", 0) or 0
|
|
@@ -71,6 +71,12 @@ class Event:
|
|
|
71
71
|
self.used_kWh = round(used_wH / 1000, 2)
|
|
72
72
|
self.last_update = datetime.now(timezone.utc).astimezone()
|
|
73
73
|
|
|
74
|
+
def update_allowed_wh(self, allowed_wH: float) -> None:
|
|
75
|
+
"""This function is used to update the allowed_kWh attribute during a Hilo Challenge Event"""
|
|
76
|
+
LOG.debug("Updating allowed Wh: %s", allowed_wH)
|
|
77
|
+
self.allowed_kWh = round(allowed_wH / 1000, 2)
|
|
78
|
+
self.last_update = datetime.now(timezone.utc).astimezone()
|
|
79
|
+
|
|
74
80
|
def should_check_for_allowed_wh(self) -> bool:
|
|
75
81
|
"""This function is used to authorize subscribing to a specific event in Hilo to receive the allowed_kWh
|
|
76
82
|
that is made available in the pre_heat phase"""
|
|
@@ -102,8 +108,9 @@ class Event:
|
|
|
102
108
|
self.phases_list.append(phase)
|
|
103
109
|
for phase in self.__annotations__:
|
|
104
110
|
if phase not in self.phases_list:
|
|
111
|
+
now_with_tz = datetime.now(timezone.utc).astimezone()
|
|
105
112
|
# On t'aime Carl
|
|
106
|
-
setattr(self, phase,
|
|
113
|
+
setattr(self, phase, now_with_tz)
|
|
107
114
|
|
|
108
115
|
def _create_phases(
|
|
109
116
|
self, hours: int, phase_name: str, parent_phase: str
|
|
@@ -8,7 +8,7 @@ from gql.transport.aiohttp import AIOHTTPTransport
|
|
|
8
8
|
from gql.transport.websockets import WebsocketsTransport
|
|
9
9
|
|
|
10
10
|
from pyhilo import API
|
|
11
|
-
from pyhilo.const import LOG
|
|
11
|
+
from pyhilo.const import LOG, PLATFORM_HOST
|
|
12
12
|
from pyhilo.device.graphql_value_mapper import GraphqlValueMapper
|
|
13
13
|
from pyhilo.devices import Devices
|
|
14
14
|
|
|
@@ -534,7 +534,7 @@ class GraphQlHelper:
|
|
|
534
534
|
"""This functions calls the digital-twin and requests location id"""
|
|
535
535
|
access_token = await self._get_access_token()
|
|
536
536
|
transport = AIOHTTPTransport(
|
|
537
|
-
url="https://
|
|
537
|
+
url=f"https://{PLATFORM_HOST}/api/digital-twin/v3/graphql",
|
|
538
538
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
539
539
|
)
|
|
540
540
|
client = Client(transport=transport, fetch_schema_from_transport=True)
|
|
@@ -562,7 +562,7 @@ class GraphQlHelper:
|
|
|
562
562
|
LOG.debug("subscribe_to_device_updated while true")
|
|
563
563
|
access_token = await self._get_access_token()
|
|
564
564
|
transport = WebsocketsTransport(
|
|
565
|
-
url=f"wss://
|
|
565
|
+
url=f"wss://{PLATFORM_HOST}/api/digital-twin/v3/graphql?access_token={access_token}",
|
|
566
566
|
ssl=ssl_context,
|
|
567
567
|
)
|
|
568
568
|
client = Client(transport=transport, fetch_schema_from_transport=True)
|
|
@@ -602,7 +602,7 @@ class GraphQlHelper:
|
|
|
602
602
|
) -> None:
|
|
603
603
|
access_token = await self._get_access_token()
|
|
604
604
|
transport = WebsocketsTransport(
|
|
605
|
-
url=f"wss://
|
|
605
|
+
url=f"wss://{PLATFORM_HOST}/api/digital-twin/v3/graphql?access_token={access_token}"
|
|
606
606
|
)
|
|
607
607
|
client = Client(transport=transport, fetch_schema_from_transport=True)
|
|
608
608
|
query = gql(self.SUBSCRIPTION_LOCATION_UPDATED)
|
|
@@ -274,6 +274,10 @@ class WebsocketClient:
|
|
|
274
274
|
LOG.debug("Websocket: async_connect() called but already connected")
|
|
275
275
|
return
|
|
276
276
|
|
|
277
|
+
if self._api.session.closed:
|
|
278
|
+
LOG.error("Websocket: Cannot connect, session is closed")
|
|
279
|
+
raise CannotConnectError("Session is closed")
|
|
280
|
+
|
|
277
281
|
LOG.info("Websocket: Connecting to server %s", self._api.endpoint)
|
|
278
282
|
if self._api.log_traces:
|
|
279
283
|
LOG.debug("[TRACE] Websocket URL: %s", self._api.full_ws_url)
|
|
@@ -310,6 +314,10 @@ class WebsocketClient:
|
|
|
310
314
|
raise CannotConnectError(err) from err
|
|
311
315
|
|
|
312
316
|
LOG.info(f"Connected to websocket server {self._api.endpoint}")
|
|
317
|
+
|
|
318
|
+
# Quick pause to prevent race condition
|
|
319
|
+
await asyncio.sleep(0.05)
|
|
320
|
+
|
|
313
321
|
self._watchdog.trigger()
|
|
314
322
|
for callback in self._connect_callbacks:
|
|
315
323
|
schedule_callback(callback)
|
|
@@ -340,6 +348,9 @@ class WebsocketClient:
|
|
|
340
348
|
messages = await self._async_receive_json()
|
|
341
349
|
for msg in messages:
|
|
342
350
|
self._parse_message(msg)
|
|
351
|
+
except asyncio.CancelledError:
|
|
352
|
+
LOG.info("Websocket: Listen cancelled.")
|
|
353
|
+
raise
|
|
343
354
|
except ConnectionClosedError as err:
|
|
344
355
|
LOG.error(f"Websocket: Closed while listening: {err}")
|
|
345
356
|
LOG.exception(err)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|