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.
Files changed (21) hide show
  1. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/PKG-INFO +1 -1
  2. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/api.py +44 -13
  3. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/const.py +1 -1
  4. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/event.py +15 -3
  5. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyproject.toml +1 -1
  6. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/LICENSE +0 -0
  7. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/README.md +0 -0
  8. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/__init__.py +0 -0
  9. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/__init__.py +0 -0
  10. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/climate.py +0 -0
  11. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/graphql_value_mapper.py +0 -0
  12. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/light.py +0 -0
  13. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/sensor.py +0 -0
  14. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/device/switch.py +0 -0
  15. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/devices.py +0 -0
  16. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/exceptions.py +0 -0
  17. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/graphql.py +0 -0
  18. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/oauth2helper.py +0 -0
  19. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/util/__init__.py +0 -0
  20. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/util/state.py +0 -0
  21. {python_hilo-2025.12.4 → python_hilo-2026.1.1}/pyhilo/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hilo
3
- Version: 2025.12.4
3
+ Version: 2026.1.1
4
4
  Summary: A Python3, async interface to the Hilo API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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
- url = base + "/Locations/" + str(location_id)
336
+
337
+ url = base + (f"/Locations/{urn}" if urn else f"/Locations/{location_id}")
338
+
333
339
  if endpoint:
334
- url += "/" + str(endpoint)
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(f"Devices/{device.id}/Attributes", device.location_id)
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, location_id, events=True)
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", location_id, True)
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
- async def get_seasons(self, location_id: int) -> dict[str, Any]:
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("Seasons", location_id, challenge=True)
691
+ url = self._get_url("seasonssummary", challenge=True, urn=self.urn)
682
692
  LOG.debug("Seasons URL is %s", url)
683
- return cast(dict[str, Any], await self.async_request("get", url))
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 = "2025.12.04"
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
@@ -40,7 +40,7 @@ exclude = ".venv/.*"
40
40
 
41
41
  [tool.poetry]
42
42
  name = "python-hilo"
43
- version = "2025.12.4"
43
+ version = "2026.1.1"
44
44
  description = "A Python3, async interface to the Hilo API"
45
45
  readme = "README.md"
46
46
  authors = ["David Vallee Delisle <me@dvd.dev>"]
File without changes