python-hilo 2025.12.4__tar.gz → 2026.1.1__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.12.4 → python_hilo-2026.1.1}/PKG-INFO +1 -1
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/api.py +44 -13
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/const.py +1 -1
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/event.py +15 -3
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyproject.toml +1 -1
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/LICENSE +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/README.md +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/__init__.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/__init__.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/climate.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/graphql_value_mapper.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/light.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/sensor.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/switch.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/devices.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/exceptions.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/graphql.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/oauth2helper.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/util/__init__.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/util/state.py +0 -0
- {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/websocket.py +0 -0
|
@@ -153,6 +153,9 @@ class API:
|
|
|
153
153
|
@property
|
|
154
154
|
def urn(self) -> str | None:
|
|
155
155
|
"""Extract URN from the JWT access token."""
|
|
156
|
+
if self._urn is not None:
|
|
157
|
+
return self._urn
|
|
158
|
+
|
|
156
159
|
try:
|
|
157
160
|
if not self._oauth_session.valid_token:
|
|
158
161
|
return None
|
|
@@ -170,7 +173,7 @@ class API:
|
|
|
170
173
|
self._urn = urn_claim[0] # Get the first URN from the array
|
|
171
174
|
else:
|
|
172
175
|
self._urn = None
|
|
173
|
-
|
|
176
|
+
|
|
174
177
|
return self._urn
|
|
175
178
|
except (IndexError, json.JSONDecodeError, KeyError):
|
|
176
179
|
LOG.error("Failed to extract URN from access token")
|
|
@@ -299,11 +302,12 @@ class API:
|
|
|
299
302
|
def _get_url(
|
|
300
303
|
self,
|
|
301
304
|
endpoint: Union[str, None],
|
|
302
|
-
location_id: int,
|
|
303
305
|
gd: bool = False,
|
|
304
306
|
drms: bool = False,
|
|
305
307
|
events: bool = False,
|
|
306
308
|
challenge: bool = False,
|
|
309
|
+
location_id: Union[int, None] = None,
|
|
310
|
+
urn: Union[str, None] = None,
|
|
307
311
|
) -> str:
|
|
308
312
|
"""Generate a path to the requested endpoint.
|
|
309
313
|
|
|
@@ -329,9 +333,12 @@ class API:
|
|
|
329
333
|
base = API_EVENTS_ENDPOINT + API_NOTIFICATIONS_ENDPOINT
|
|
330
334
|
if challenge:
|
|
331
335
|
base = API_CHALLENGE_ENDPOINT
|
|
332
|
-
|
|
336
|
+
|
|
337
|
+
url = base + (f"/Locations/{urn}" if urn else f"/Locations/{location_id}")
|
|
338
|
+
|
|
333
339
|
if endpoint:
|
|
334
|
-
url += "/"
|
|
340
|
+
url += f"/{endpoint}"
|
|
341
|
+
|
|
335
342
|
return url
|
|
336
343
|
|
|
337
344
|
async def _async_handle_on_backoff(self, _: dict[str, Any]) -> None:
|
|
@@ -537,7 +544,7 @@ class API:
|
|
|
537
544
|
|
|
538
545
|
async def get_devices(self, location_id: int) -> list[dict[str, Any]]:
|
|
539
546
|
"""Get list of all devices"""
|
|
540
|
-
url = self._get_url("Devices", location_id)
|
|
547
|
+
url = self._get_url("Devices", location_id=location_id)
|
|
541
548
|
LOG.debug("Devices URL is %s", url)
|
|
542
549
|
devices: list[dict[str, Any]] = await self.async_request("get", url)
|
|
543
550
|
devices.append(await self.get_gateway(location_id))
|
|
@@ -554,7 +561,9 @@ class API:
|
|
|
554
561
|
value: Union[str, float, int, None],
|
|
555
562
|
) -> None:
|
|
556
563
|
"""Sets device attributes"""
|
|
557
|
-
url = self._get_url(
|
|
564
|
+
url = self._get_url(
|
|
565
|
+
f"Devices/{device.id}/Attributes", location_id=device.location_id
|
|
566
|
+
)
|
|
558
567
|
LOG.debug("Device Attribute URL is %s", url)
|
|
559
568
|
await self.async_request("put", url, json={key.hilo_attribute: value})
|
|
560
569
|
|
|
@@ -582,7 +591,7 @@ class API:
|
|
|
582
591
|
"notificationDataJSON": "{\"NotificationType\":null,\"Title\":\"\",\"SubTitle\":null,\"Body\":\"Test manuel de l’alarme détecté.\",\"Badge\":0,\"Sound\":null,\"Data\":null,\"Tags\":null,\"Type\":\"TestDetected\",\"DeviceId\":324236,\"LocationId\":4051}",
|
|
583
592
|
"viewed": false
|
|
584
593
|
}"""
|
|
585
|
-
url = self._get_url(None,
|
|
594
|
+
url = self._get_url(None, events=True, location_id=location_id)
|
|
586
595
|
LOG.debug("Event Notifications URL is %s", url)
|
|
587
596
|
return cast(dict[str, Any], await self.async_request("get", url))
|
|
588
597
|
|
|
@@ -650,7 +659,7 @@ class API:
|
|
|
650
659
|
}
|
|
651
660
|
"""
|
|
652
661
|
# ic-dev21 need to check but this is probably dead code
|
|
653
|
-
url = self._get_url("Events",
|
|
662
|
+
url = self._get_url("Events", True, location_id=location_id)
|
|
654
663
|
if not event_id:
|
|
655
664
|
url += "?active=true"
|
|
656
665
|
else:
|
|
@@ -659,7 +668,8 @@ class API:
|
|
|
659
668
|
LOG.debug("get_gd_events URL is %s", url)
|
|
660
669
|
return cast(dict[str, Any], await self.async_request("get", url))
|
|
661
670
|
|
|
662
|
-
|
|
671
|
+
# keep location_id for now for backward compatibility with existing hilo branch
|
|
672
|
+
async def get_seasons(self, location_id: int) -> list[dict[str, Any]]:
|
|
663
673
|
"""This will return the rewards and current season total
|
|
664
674
|
https://api.hiloenergie.com/challenge/v1/api/Locations/XXXX/Seasons
|
|
665
675
|
[
|
|
@@ -678,13 +688,34 @@ class API:
|
|
|
678
688
|
}
|
|
679
689
|
]
|
|
680
690
|
"""
|
|
681
|
-
url = self._get_url("
|
|
691
|
+
url = self._get_url("seasonssummary", challenge=True, urn=self.urn)
|
|
682
692
|
LOG.debug("Seasons URL is %s", url)
|
|
683
|
-
|
|
693
|
+
|
|
694
|
+
seasons = await self.async_request("get", url)
|
|
695
|
+
LOG.debug("Seasons API response: %s", seasons)
|
|
696
|
+
|
|
697
|
+
all_seasons: list[dict[str, Any]] = []
|
|
698
|
+
|
|
699
|
+
for season_data in seasons:
|
|
700
|
+
season = season_data.get("season")
|
|
701
|
+
ratePlan = season_data.get("ratePlan")
|
|
702
|
+
periodId = season_data.get("periodId")
|
|
703
|
+
|
|
704
|
+
url = self._get_url(
|
|
705
|
+
f"rates/{ratePlan}/seasons/{season}/events?periodId={periodId}",
|
|
706
|
+
challenge=True,
|
|
707
|
+
urn=self.urn,
|
|
708
|
+
)
|
|
709
|
+
LOG.debug("Seasons Events URL is %s", url)
|
|
710
|
+
season_events = await self.async_request("get", url)
|
|
711
|
+
LOG.debug("Season %s Events API response: %s", season, season_events)
|
|
712
|
+
all_seasons.append(season_events)
|
|
713
|
+
|
|
714
|
+
return all_seasons
|
|
684
715
|
|
|
685
716
|
async def get_gateway(self, location_id: int) -> dict[str, Any]:
|
|
686
717
|
"""Gets info about the Hilo hub (gateway)"""
|
|
687
|
-
url = self._get_url("Gateways/Info", location_id)
|
|
718
|
+
url = self._get_url("Gateways/Info", location_id=location_id)
|
|
688
719
|
LOG.debug("Gateway URL is %s", url)
|
|
689
720
|
req = await self.async_request("get", url)
|
|
690
721
|
saved_attrs = [
|
|
@@ -727,7 +758,7 @@ class API:
|
|
|
727
758
|
}
|
|
728
759
|
]
|
|
729
760
|
"""
|
|
730
|
-
url = self._get_url("Weather", location_id)
|
|
761
|
+
url = self._get_url("Weather", location_id=location_id)
|
|
731
762
|
LOG.debug("Weather URL is %s", url)
|
|
732
763
|
response = await self.async_request("get", url)
|
|
733
764
|
LOG.debug("Weather API response: %s", response)
|
|
@@ -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 = "
|
|
10
|
+
PYHILO_VERSION: Final = "2026.1.01"
|
|
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"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""Event object
|
|
1
|
+
"""Event object"""
|
|
2
|
+
|
|
2
3
|
from datetime import datetime, timedelta, timezone
|
|
3
4
|
import logging
|
|
4
5
|
import re
|
|
@@ -26,7 +27,7 @@ class Event:
|
|
|
26
27
|
|
|
27
28
|
def __init__(self, **event: dict[str, Any]):
|
|
28
29
|
"""Initialize."""
|
|
29
|
-
self._convert_phases(cast(dict[str, Any], event.get("phases")))
|
|
30
|
+
self._convert_phases(cast(dict[str, Any], event.get("phases", {})))
|
|
30
31
|
params: dict[str, Any] = event.get("parameters") or {}
|
|
31
32
|
devices: list[dict[str, Any]] = params.get("devices", [])
|
|
32
33
|
consumption: dict[str, Any] = event.get("consumption", {})
|
|
@@ -34,7 +35,7 @@ class Event:
|
|
|
34
35
|
used_wH: int = consumption.get("currentWh", 0) or 0
|
|
35
36
|
self.participating: bool = cast(bool, event.get("isParticipating", False))
|
|
36
37
|
self.configurable: bool = cast(bool, event.get("isConfigurable", False))
|
|
37
|
-
self.period: str = cast(str, event.get("period", ""))
|
|
38
|
+
self.period: str = (cast(str, event.get("period", "")) or "").lower()
|
|
38
39
|
self.event_id: int = cast(int, event["id"])
|
|
39
40
|
self.total_devices: int = len(devices)
|
|
40
41
|
self.opt_out_devices: int = len([x for x in devices if x["optOut"]])
|
|
@@ -44,6 +45,7 @@ class Event:
|
|
|
44
45
|
self.allowed_kWh: float = round(allowed_wH / 1000, 2)
|
|
45
46
|
self.used_kWh: float = round(used_wH / 1000, 2)
|
|
46
47
|
self.used_percentage: float = 0
|
|
48
|
+
self.reward = cast(float, event.get("reward", 0.0))
|
|
47
49
|
self.last_update = datetime.now(timezone.utc).astimezone()
|
|
48
50
|
if allowed_wH > 0:
|
|
49
51
|
self.used_percentage = round(used_wH / allowed_wH * 100, 2)
|
|
@@ -63,6 +65,7 @@ class Event:
|
|
|
63
65
|
"used_kWh",
|
|
64
66
|
"used_percentage",
|
|
65
67
|
"last_update",
|
|
68
|
+
"reward",
|
|
66
69
|
]
|
|
67
70
|
|
|
68
71
|
def update_wh(self, used_wH: float) -> None:
|
|
@@ -70,12 +73,21 @@ class Event:
|
|
|
70
73
|
LOG.debug("Updating Wh: %s", used_wH)
|
|
71
74
|
self.used_kWh = round(used_wH / 1000, 2)
|
|
72
75
|
self.last_update = datetime.now(timezone.utc).astimezone()
|
|
76
|
+
self._recalculate_percentage()
|
|
73
77
|
|
|
74
78
|
def update_allowed_wh(self, allowed_wH: float) -> None:
|
|
75
79
|
"""This function is used to update the allowed_kWh attribute during a Hilo Challenge Event"""
|
|
76
80
|
LOG.debug("Updating allowed Wh: %s", allowed_wH)
|
|
77
81
|
self.allowed_kWh = round(allowed_wH / 1000, 2)
|
|
78
82
|
self.last_update = datetime.now(timezone.utc).astimezone()
|
|
83
|
+
self._recalculate_percentage()
|
|
84
|
+
|
|
85
|
+
def _recalculate_percentage(self) -> None:
|
|
86
|
+
"""Recalculate used percentage based on current values"""
|
|
87
|
+
if self.allowed_kWh > 0:
|
|
88
|
+
self.used_percentage = round(self.used_kWh / self.allowed_kWh * 100, 2)
|
|
89
|
+
else:
|
|
90
|
+
self.used_percentage = 0
|
|
79
91
|
|
|
80
92
|
def should_check_for_allowed_wh(self) -> bool:
|
|
81
93
|
"""This function is used to authorize subscribing to a specific event in Hilo to receive the allowed_kWh
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|