Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
pull_request:

env:
DEFAULT_PYTHON: 3.9
DEFAULT_PYTHON: 3.11

jobs:
pre-commit:
Expand Down
24 changes: 11 additions & 13 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ repos:
- --format=custom
- --configfile=.bandit.yaml
files: ^pyhilo/.+\.py$
- repo: https://github.com/python/black
rev: 22.12.0
- repo: https://github.com/psf/black
rev: 24.1.1
hooks:
- id: black
args:
Expand All @@ -19,39 +19,37 @@ repos:
language_version: python3
files: ^((pyhilo|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v1.16.0
rev: v2.2.6
hooks:
- id: codespell
args:
- --skip="./.*,*.json"
- --quiet-level=4
- -L ba,hass
- -L ba,hass,manuel,connexion
exclude_types: [json]
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.0.1
- flake8-docstrings==1.7.0
- pydocstyle==6.3.0
files: ^pyhilo/.+\.py$
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
additional_dependencies:
- toml
files: ^(pyhilo|tests)/.+\.py$
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
rev: v1.19.1
hooks:
- id: mypy
files: ^pyhilo/.+\.py$
additional_dependencies:
- types-python-dateutil==2.8.0
- types-aiofiles==23.2.0
- types-PyYAML

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
Expand Down
1 change: 1 addition & 0 deletions doc/Device Replies/DeviceListInitialValuesReceived.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
32 changes: 19 additions & 13 deletions doc/Readme.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
# Using get_tokens.py

This python script will let you use it to log in to your Hilo account.

From there, you will be asked to copy the full HA redirect(callback) URL and go back to your python script and press enter.

Negotiation will take place and you will receive three distinct tokens:

- Your access token
- Your devicehub token
- Your challengehub token

Take care not to share your tokens as they are not encrypted and contain personally identifiable information.

1. Run get_tokens.py, you will be provided with a link. Either click it or copy it in your favourite browser.
<img width="1393" height="186" alt="image" src="https://github.com/user-attachments/assets/0b9559b1-1d9a-41ae-b4bf-185a5a5c588b" />
<img width="1393" height="186" alt="image" src="https://github.com/user-attachments/assets/0b9559b1-1d9a-41ae-b4bf-185a5a5c588b" />

2. Login using your Hilo Credentials

<img width="631" height="569" alt="image" src="https://github.com/user-attachments/assets/ad7c79a7-2402-44e8-9a0b-962406b89527" />

3. Once you get to this page, select the URL and copy it to clipboard
<img width="694" height="274" alt="image" src="https://github.com/user-attachments/assets/85989069-31a3-418d-b305-1f2378432d84" />
<img width="567" height="46" alt="image" src="https://github.com/user-attachments/assets/1d979626-6d93-46cb-aae0-50ef061a0d61" />
<img width="694" height="274" alt="image" src="https://github.com/user-attachments/assets/85989069-31a3-418d-b305-1f2378432d84" />
<img width="567" height="46" alt="image" src="https://github.com/user-attachments/assets/1d979626-6d93-46cb-aae0-50ef061a0d61" />

4. Go back to get_tokens.py and press enter, you'll get all 3 tokens:

<img width="1208" height="805" alt="image" src="https://github.com/user-attachments/assets/10258890-e935-4b16-8d16-12bdd340c001" />


# Using Postman to send payloads to devicehub or challengehub

## Prerequisite: you need to have your tokens on hand, they are relatively short lived so once you have them, get to connecting!

### DeviceHub
Expand All @@ -43,7 +45,9 @@ Take care not to share your tokens as they are not encrypted and contain persona
"version": 1
}
```
5. You can then invoke "SubscribeToLocation" to get your device informations.

5. You can then invoke "SubscribeToLocation" to get your device information.

```
{
"arguments": [
Expand All @@ -54,6 +58,7 @@ Take care not to share your tokens as they are not encrypted and contain persona
"type": 1
}
```

6. You should received the DeviceListInitialValuesReceived message back.

### ChallengeHub
Expand All @@ -70,20 +75,21 @@ Take care not to share your tokens as they are not encrypted and contain persona
"version": 1
}
```
5. You can send various messages to the ChallengeHub to request different information. For the 2025-2026 season with the addition of Flex D, the messages were split between Winter Credit (Crédit Hivernal) and Flex D. Winter Credit messages will contain CH in their target, and Flex D event will contain Flex in their target. Each of rates has its own event id, for example, event 337 was for Winter Credits, and event 338 was for Flex D, both occured at the same time. You will have access to one, or the other, depending on your rate.

5. You can send various messages to the ChallengeHub to request different information. For the 2025-2026 season with the addition of Flex D, the messages were split between Winter Credit (Crédit Hivernal) and Flex D. Winter Credit messages will contain CH in their target, and Flex D event will contain Flex in their target. Each of rates has its own event id, for example, event 337 was for Winter Credits, and event 338 was for Flex D, both occurred at the same time. You will have access to one, or the other, depending on your rate.

### Examples of various invokesto the ChallengeHub

#### SubscribeToEventCH or SubscribeToEventFlex

This invoke is used:

```
{
"arguments": [
{
"locationHiloId": "urn:YOUR-URN",
"eventId": ID(numberic)
"eventId": ID(numeric)
}
],
"invocationId": "1",
Expand All @@ -93,13 +99,13 @@ This invoke is used:
```

And should return

```
EventCHDetailsInitialValuesReceived
````
Or
```
EventFlexDetailsInitialValuesReceived
```

Or


```
EventFlexDetailsInitialValuesReceived
```
1 change: 1 addition & 0 deletions pyhilo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Define the hilo package."""

from pyhilo.api import API
from pyhilo.const import UNMONITORED_DEVICES
from pyhilo.device import HiloDevice
Expand Down
8 changes: 5 additions & 3 deletions pyhilo/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ def dev_atts(
for x in self.device_attributes
if x.hilo_attribute == attribute or x.attr == attribute
),
DeviceAttribute(attribute, HILO_READING_TYPES.get(value_type, "null"))
if value_type
else attribute,
(
DeviceAttribute(attribute, HILO_READING_TYPES.get(value_type, "null"))
if value_type
else attribute
),
)

async def _get_fid_state(self) -> bool:
Expand Down
10 changes: 7 additions & 3 deletions pyhilo/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import platform
import uuid
from typing import Final
import uuid

import aiohttp

Expand All @@ -24,7 +24,9 @@
AUTH_TOKEN: Final = f"https://{AUTH_HOSTNAME}{AUTH_ENDPOINT}token"
AUTH_CHALLENGE_METHOD: Final = "S256"
AUTH_CLIENT_ID: Final = "1ca9f585-4a55-4085-8e30-9746a65fa561"
AUTH_SCOPE: Final = "openid https://HiloDirectoryB2C.onmicrosoft.com/hiloapis/user_impersonation offline_access"
AUTH_SCOPE: Final = (
"openid https://HiloDirectoryB2C.onmicrosoft.com/hiloapis/user_impersonation offline_access"
)
SUBSCRIPTION_KEY: Final = "20eeaedcb86945afa3fe792cea89b8bf"

# API constants
Expand All @@ -50,7 +52,9 @@


# Request constants
DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION}-{INSTANCE_ID} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}"
DEFAULT_USER_AGENT: Final = (
f"PyHilo/{PYHILO_VERSION}-{INSTANCE_ID} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}"
)


# NOTE(dvd): Not sure how to get new ones so I'm using the ones from my emulator
Expand Down
1 change: 1 addition & 0 deletions pyhilo/device/climate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Climate object."""

from __future__ import annotations

from typing import Any, cast
Expand Down
9 changes: 5 additions & 4 deletions pyhilo/device/graphql_value_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class GraphqlValueMapper:

OnState = "on"

def map_query_values(self, values: Dict[str, Any]) -> list[Dict[str, Any]]:
def map_query_values(self, values: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
readings: list[Dict[str, Any]] = []
for device in values:
if device.get("deviceType") is not None:
Expand All @@ -20,7 +20,7 @@ def map_query_values(self, values: Dict[str, Any]) -> list[Dict[str, Any]]:
return readings

def map_device_subscription_values(
self, device: list[Dict[str, Any]]
self, device: Dict[str, Any]
) -> list[Dict[str, Any]]:
readings: list[Dict[str, Any]] = []
if device.get("deviceType") is not None:
Expand All @@ -32,7 +32,7 @@ def map_location_subscription_values(
self, values: Dict[str, Any]
) -> list[Dict[str, Any]]:
readings: list[Dict[str, Any]] = []
for device in values:
for device in values.get("devices", []):
if device.get("deviceType") is not None:
reading = self._map_devices_values(device)
readings.extend(reading)
Expand Down Expand Up @@ -425,7 +425,8 @@ def _build_light(self, device: Dict[str, Any]) -> list[Dict[str, Any]]:
device["hiloId"], "Intensity", device["level"] / 100
)
)
if device.get("lightType").lower() == "color":
light_type = device.get("lightType")
if light_type and light_type.lower() == "color":
attributes.append(
self.build_attribute(device["hiloId"], "Hue", device.get("hue") or 0)
)
Expand Down
2 changes: 1 addition & 1 deletion pyhilo/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Event:

def __init__(self, **event: dict[str, Any]):
"""Initialize."""
self._convert_phases(cast(dict[str, Any], event.get("phases", {})))
self._convert_phases(event.get("phases", {}))
params: dict[str, Any] = event.get("parameters") or {}
devices: list[dict[str, Any]] = params.get("devices", [])
consumption: dict[str, Any] = event.get("consumption", {})
Expand Down
1 change: 1 addition & 0 deletions pyhilo/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Define package errors."""

from __future__ import annotations


Expand Down
31 changes: 17 additions & 14 deletions pyhilo/graphql.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import hashlib
import json
import logging
from typing import Any, Callable, Dict, List, Optional

import httpx
Expand Down Expand Up @@ -581,7 +580,9 @@ async def call_get_location_query(self, location_hilo_id: str) -> None:
self._handle_query_result(response_json["data"])

async def subscribe_to_device_updated(
self, location_hilo_id: str, callback: callable = None
self,
location_hilo_id: str,
callback: Optional[Callable[[str], None]] = None,
) -> None:
LOG.debug("subscribe_to_device_updated called")
await self._listen_to_sse(
Expand All @@ -593,7 +594,9 @@ async def subscribe_to_device_updated(
)

async def subscribe_to_location_updated(
self, location_hilo_id: str, callback: callable = None
self,
location_hilo_id: str,
callback: Optional[Callable[[str], None]] = None,
) -> None:
LOG.debug("subscribe_to_location_updated called")
await self._listen_to_sse(
Expand All @@ -610,10 +613,10 @@ async def _listen_to_sse(
variables: Dict[str, Any],
handler: Callable[[Dict[str, Any]], str],
callback: Optional[Callable[[str], None]] = None,
location_hilo_id: str = None,
location_hilo_id: Optional[str] = None,
) -> None:
query_hash = hashlib.sha256(query.encode("utf-8")).hexdigest()
payload = {
payload: Dict[str, Any] = {
"extensions": {
"persistedQuery": {
"version": 1,
Expand Down Expand Up @@ -659,9 +662,9 @@ async def _listen_to_sse(
LOG.debug(
"Received subscription result %s", data["data"]
)
result = handler(data["data"])
handler_result = handler(data["data"])
if callback:
callback(result)
callback(handler_result)

if retry_with_full_query:
payload["query"] = query
Expand Down Expand Up @@ -692,22 +695,22 @@ async def _get_access_token(self) -> str:

def _handle_query_result(self, result: Dict[str, Any]) -> None:
"""This receives query results and maps them to the proper device."""
devices_values: list[any] = result["getLocation"]["devices"]
devices_values: List[Dict[str, Any]] = result["getLocation"]["devices"]
attributes = self.mapper.map_query_values(devices_values)
self._devices.parse_values_received(attributes)

def _handle_device_subscription_result(self, result: Dict[str, Any]) -> str:
devices_values: list[any] = result["onAnyDeviceUpdated"]["device"]
attributes = self.mapper.map_device_subscription_values(devices_values)
device_value: Dict[str, Any] = result["onAnyDeviceUpdated"]["device"]
attributes = self.mapper.map_device_subscription_values(device_value)
updated_device = self._devices.parse_values_received(attributes)
# callback to update the device in the UI
LOG.debug("Device updated: %s", updated_device)
return devices_values.get("hiloId")
return str(device_value.get("hiloId"))

def _handle_location_subscription_result(self, result: Dict[str, Any]) -> str:
devices_values: list[any] = result["onAnyLocationUpdated"]["location"]
attributes = self.mapper.map_location_subscription_values(devices_values)
location_value: Dict[str, Any] = result["onAnyLocationUpdated"]["location"]
attributes = self.mapper.map_location_subscription_values(location_value)
updated_device = self._devices.parse_values_received(attributes)
# callback to update the device in the UI
LOG.debug("Device updated: %s", updated_device)
return devices_values.get("hiloId")
return str(location_value.get("hiloId"))
1 change: 1 addition & 0 deletions pyhilo/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Define utility modules."""

import asyncio
from datetime import datetime, timedelta
import re
Expand Down
8 changes: 3 additions & 5 deletions pyhilo/util/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,9 @@ async def get_state(state_yaml: str) -> StateDict:
async def set_state(
state_yaml: str,
key: str,
state: TokenDict
| RegistrationDict
| FirebaseDict
| AndroidDeviceDict
| WebsocketDict,
state: (
TokenDict | RegistrationDict | FirebaseDict | AndroidDeviceDict | WebsocketDict
),
) -> None:
"""Save state yaml.
:param state_yaml: filename where to read the state
Expand Down
1 change: 1 addition & 0 deletions pyhilo/websocket.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Define a connection to the Hilo websocket."""

from __future__ import annotations

import asyncio
Expand Down