diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4292909..1cfd401 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,7 +8,7 @@ on: pull_request: env: - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: 3.11 jobs: pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41e3943..b388b24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: @@ -19,31 +19,29 @@ 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$ @@ -51,7 +49,7 @@ repos: - 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: diff --git a/doc/Device Replies/DeviceListInitialValuesReceived.json b/doc/Device Replies/DeviceListInitialValuesReceived.json index e69de29..0967ef4 100644 --- a/doc/Device Replies/DeviceListInitialValuesReceived.json +++ b/doc/Device Replies/DeviceListInitialValuesReceived.json @@ -0,0 +1 @@ +{} diff --git a/doc/Readme.md b/doc/Readme.md index 4f7502d..6a38936 100644 --- a/doc/Readme.md +++ b/doc/Readme.md @@ -1,9 +1,11 @@ # 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 @@ -11,22 +13,22 @@ Negotiation will take place and you will receive three distinct tokens: 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. -image + image 2. Login using your Hilo Credentials - + image 3. Once you get to this page, select the URL and copy it to clipboard -image -image + image + image 4. Go back to get_tokens.py and press enter, you'll get all 3 tokens: image - # 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 @@ -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": [ @@ -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 @@ -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", @@ -93,13 +99,13 @@ This invoke is used: ``` And should return + ``` EventCHDetailsInitialValuesReceived -```` -Or -``` -EventFlexDetailsInitialValuesReceived ``` +Or - +``` +EventFlexDetailsInitialValuesReceived +``` diff --git a/pyhilo/__init__.py b/pyhilo/__init__.py index c09325e..e19e0d4 100644 --- a/pyhilo/__init__.py +++ b/pyhilo/__init__.py @@ -1,4 +1,5 @@ """Define the hilo package.""" + from pyhilo.api import API from pyhilo.const import UNMONITORED_DEVICES from pyhilo.device import HiloDevice diff --git a/pyhilo/api.py b/pyhilo/api.py index 739f58b..553743e 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -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: diff --git a/pyhilo/const.py b/pyhilo/const.py index f1a7870..a1857f9 100755 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -1,7 +1,7 @@ import logging import platform -import uuid from typing import Final +import uuid import aiohttp @@ -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 @@ -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 diff --git a/pyhilo/device/climate.py b/pyhilo/device/climate.py index c0b4ff1..067478a 100644 --- a/pyhilo/device/climate.py +++ b/pyhilo/device/climate.py @@ -1,4 +1,5 @@ """Climate object.""" + from __future__ import annotations from typing import Any, cast diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index a44c16c..ac5765c 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -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: @@ -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: @@ -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) @@ -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) ) diff --git a/pyhilo/event.py b/pyhilo/event.py index fd8a0a5..4af1750 100755 --- a/pyhilo/event.py +++ b/pyhilo/event.py @@ -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", {}) diff --git a/pyhilo/exceptions.py b/pyhilo/exceptions.py index c16a2ea..d980214 100644 --- a/pyhilo/exceptions.py +++ b/pyhilo/exceptions.py @@ -1,4 +1,5 @@ """Define package errors.""" + from __future__ import annotations diff --git a/pyhilo/graphql.py b/pyhilo/graphql.py index 8935a20..07f036a 100644 --- a/pyhilo/graphql.py +++ b/pyhilo/graphql.py @@ -1,7 +1,6 @@ import asyncio import hashlib import json -import logging from typing import Any, Callable, Dict, List, Optional import httpx @@ -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( @@ -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( @@ -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, @@ -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 @@ -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")) diff --git a/pyhilo/util/__init__.py b/pyhilo/util/__init__.py index a3b25fd..294508f 100755 --- a/pyhilo/util/__init__.py +++ b/pyhilo/util/__init__.py @@ -1,4 +1,5 @@ """Define utility modules.""" + import asyncio from datetime import datetime, timedelta import re diff --git a/pyhilo/util/state.py b/pyhilo/util/state.py index 11d8e3c..87a00bf 100644 --- a/pyhilo/util/state.py +++ b/pyhilo/util/state.py @@ -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 diff --git a/pyhilo/websocket.py b/pyhilo/websocket.py index 9a479ab..4522f73 100755 --- a/pyhilo/websocket.py +++ b/pyhilo/websocket.py @@ -1,4 +1,5 @@ """Define a connection to the Hilo websocket.""" + from __future__ import annotations import asyncio