From ee66b43f23dccf543fd636e38ea634fcc6fbdf01 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 16:47:40 -0500 Subject: [PATCH 01/12] Add GraphQL query for location details and refactor location ID retrieval --- pyhilo/api.py | 34 ++++++++++++++++++++++++++++++++-- pyhilo/devices.py | 6 +++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index ff26d1b..629107a 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -12,6 +12,8 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientResponseError import backoff +from gql import gql, Client +from gql.transport.aiohttp import AIOHTTPTransport from pyhilo.const import ( ANDROID_CLIENT_ENDPOINT, @@ -492,11 +494,11 @@ async def android_register(self) -> None: }, ) - async def get_location_id(self) -> int: + async def get_location_ids(self) -> tuple[int, str]: url = f"{API_AUTOMATION_ENDPOINT}/Locations" LOG.debug(f"LocationId URL is {url}") req: list[dict[str, Any]] = await self.async_request("get", url) - return int(req[0]["id"]) + return (int(req[0]["id"]), string(req[0]["locationHiloId"])) async def get_devices(self, location_id: int) -> list[dict[str, Any]]: """Get list of all devices""" @@ -509,6 +511,34 @@ async def get_devices(self, location_id: int) -> list[dict[str, Any]]: for callback in self._get_device_callbacks: devices.append(callback()) return devices + + async def call_get_location_query(self, location_hilo_id: string) -> None: + access_token = await self.async_get_access_token() + transport = AIOHTTPTransport( + url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", + headers={"Authorization": f"Bearer {access_token}"}, + ) + client = Client(transport=transport, fetch_schema_from_transport=True) + query = gql(""" + query getLocation($locationHiloId: String!) { + getLocation(id:$locationHiloId) { + hiloId + lastUpdate + lastUpdateVersion + devices { + hiloId + deviceType + physicalAddress + } + } + } + """) + async with client as session: + result = await session.execute( + query, variable_values={"locationHiloId": location_hilo_id} + ) + LOG.info(result) + return result async def _set_device_attribute( self, diff --git a/pyhilo/devices.py b/pyhilo/devices.py index ff1ca6d..008f1e5 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -14,6 +14,7 @@ def __init__(self, api: API): self._api = api self.devices: list[HiloDevice] = [] self.location_id: int = 0 + self.location_hilo_id: str = "" @property def all(self) -> list[HiloDevice]: @@ -108,5 +109,8 @@ async def update_devicelist_from_signalr( async def async_init(self) -> None: """Initialize the Hilo "manager" class.""" LOG.info("Initialising after websocket is connected") - self.location_id = await self._api.get_location_id() + location_ids = await self._api.get_location_ids() + self.location_id = location_ids[0] + self.location_hilo_id = location_ids[1] await self.update() + await self._api.call_get_location_query(self.location_hilo_id) From e5b84579dd42030e07cf4f75344201d6828aba62 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 18:04:40 -0500 Subject: [PATCH 02/12] Refactor API methods for improved GraphQL client handling and add GraphqlValueMapper for thermostat --- pyhilo/api.py | 44 +++++++++---- pyhilo/device/graphql_value_mapper.py | 92 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 pyhilo/device/graphql_value_mapper.py diff --git a/pyhilo/api.py b/pyhilo/api.py index 629107a..b6d9c06 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -498,7 +498,7 @@ async def get_location_ids(self) -> tuple[int, str]: url = f"{API_AUTOMATION_ENDPOINT}/Locations" LOG.debug(f"LocationId URL is {url}") req: list[dict[str, Any]] = await self.async_request("get", url) - return (int(req[0]["id"]), string(req[0]["locationHiloId"])) + return (req[0]["id"], req[0]["locationHiloId"]) async def get_devices(self, location_id: int) -> list[dict[str, Any]]: """Get list of all devices""" @@ -511,14 +511,9 @@ async def get_devices(self, location_id: int) -> list[dict[str, Any]]: for callback in self._get_device_callbacks: devices.append(callback()) return devices - + async def call_get_location_query(self, location_hilo_id: string) -> None: - access_token = await self.async_get_access_token() - transport = AIOHTTPTransport( - url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", - headers={"Authorization": f"Bearer {access_token}"}, - ) - client = Client(transport=transport, fetch_schema_from_transport=True) + client = self._get_graphql_client() query = gql(""" query getLocation($locationHiloId: String!) { getLocation(id:$locationHiloId) { @@ -526,9 +521,28 @@ async def call_get_location_query(self, location_hilo_id: string) -> None: lastUpdate lastUpdateVersion devices { - hiloId - deviceType - physicalAddress + ... on BasicThermostat { + deviceType + hiloId + physicalAddress + connectionStatus + ambientHumidity + gDState + version + zigbeeVersion + ambientTemperature { + value + kind + } + ambientTempSetpoint { + value + kind + } + power { + value + kind + } + } } } } @@ -540,6 +554,14 @@ async def call_get_location_query(self, location_hilo_id: string) -> None: LOG.info(result) return result + async def _get_graphql_client(self) -> Client: + access_token = await self.async_get_access_token() + transport = AIOHTTPTransport( + url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", + headers={"Authorization": f"Bearer {access_token}"}, + ) + return Client(transport=transport, fetch_schema_from_transport=True) + async def _set_device_attribute( self, device: HiloDevice, diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py new file mode 100644 index 0000000..3c7ac25 --- /dev/null +++ b/pyhilo/device/graphql_value_mapper.py @@ -0,0 +1,92 @@ +from pyhilo.device import DeviceAttribute + + +class GraphqlValueMapper: + def __init__(self, value): + self.value = value + + def map_thermostat(self)-> list[DeviceAttribute]: + return [ + DeviceAttribute( + name="disconnected", + value=self.value["disconnected"], + unit="none", + value_type="bool", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="unpaired", + value=False, + unit="none", + value_type="bool", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="CurrentTemperature", + value=self.value["mode"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="TargetTemperature", + value=self.value["battery"], + unit="%", + value_type="int", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="HeatDemand", + value=self.value["low_battery"], + value_type="bool", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="Heating", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="Power", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="GdState", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="Version", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="ZigbeeVersion", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="Humidity", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="ThermostatAllowedModes", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + DeviceAttribute( + name="ThermostatMode", + value=self.value["state"], + value_type="string", + device_id=self.value["device_id"], + ), + ] + From 18673fc1d512ecd69fd7bdf7d3a9e697efe000e1 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 22:03:09 -0500 Subject: [PATCH 03/12] Refactor call_get_location_query to return a dictionary and streamline GraphQL client initialization --- pyhilo/api.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index b6d9c06..f45aad5 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -6,7 +6,7 @@ import random import string import sys -from typing import Any, Callable, Union, cast +from typing import Any, Callable, Dict, Union, cast from urllib import parse from aiohttp import ClientSession @@ -512,8 +512,13 @@ async def get_devices(self, location_id: int) -> list[dict[str, Any]]: devices.append(callback()) return devices - async def call_get_location_query(self, location_hilo_id: string) -> None: - client = self._get_graphql_client() + async def call_get_location_query(self, location_hilo_id: string) -> Dict[str, Any]: + access_token = await self.async_get_access_token() + transport = AIOHTTPTransport( + url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", + headers={"Authorization": f"Bearer {access_token}"}, + ) + client = Client(transport=transport, fetch_schema_from_transport=True) query = gql(""" query getLocation($locationHiloId: String!) { getLocation(id:$locationHiloId) { @@ -547,21 +552,14 @@ async def call_get_location_query(self, location_hilo_id: string) -> None: } } """) + async with client as session: result = await session.execute( query, variable_values={"locationHiloId": location_hilo_id} ) - LOG.info(result) - return result - - async def _get_graphql_client(self) -> Client: - access_token = await self.async_get_access_token() - transport = AIOHTTPTransport( - url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", - headers={"Authorization": f"Bearer {access_token}"}, - ) - return Client(transport=transport, fetch_schema_from_transport=True) - + LOG.info(result) + return result + async def _set_device_attribute( self, device: HiloDevice, From 062db7922ab5ba84d7dc2002e454d51cda3be67b Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 22:03:47 -0500 Subject: [PATCH 04/12] Test du call Graphql --- pyhilo/devices.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyhilo/devices.py b/pyhilo/devices.py index 008f1e5..2f64be4 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -4,6 +4,7 @@ from pyhilo.const import HILO_DEVICE_TYPES, LOG from pyhilo.device import DeviceReading, HiloDevice from pyhilo.device.climate import Climate # noqa +from pyhilo.device.graphql_value_mapper import GraphqlValueMapper from pyhilo.device.light import Light # noqa from pyhilo.device.sensor import Sensor # noqa from pyhilo.device.switch import Switch # noqa @@ -61,8 +62,11 @@ def _map_readings_to_devices( ) return updated_devices - def find_device(self, id: int) -> HiloDevice: - return next((d for d in self.devices if d.id == id), None) # type: ignore + def find_device(self, identifier: Union[int, str]) -> HiloDevice: + if isinstance(identifier, int): + return next((d for d in self.devices if d.id == identifier), None) # type: ignore + elif isinstance(identifier, str): + return next((d for d in self.devices if d.hilo_id == identifier), None) # type: ignore def generate_device(self, device: dict) -> HiloDevice: device["location_id"] = self.location_id @@ -113,4 +117,6 @@ async def async_init(self) -> None: self.location_id = location_ids[0] self.location_hilo_id = location_ids[1] await self.update() - await self._api.call_get_location_query(self.location_hilo_id) + values = await self._api.call_get_location_query(self.location_hilo_id) + mapper = GraphqlValueMapper() + mapper.map_values(values) From 5d927cb66b8f9807104942622abb573f66c296ad Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 22:04:06 -0500 Subject: [PATCH 05/12] revert de l'overload que j'ai pas besoin tout de suite --- pyhilo/devices.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyhilo/devices.py b/pyhilo/devices.py index 2f64be4..9cf0106 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -62,11 +62,8 @@ def _map_readings_to_devices( ) return updated_devices - def find_device(self, identifier: Union[int, str]) -> HiloDevice: - if isinstance(identifier, int): - return next((d for d in self.devices if d.id == identifier), None) # type: ignore - elif isinstance(identifier, str): - return next((d for d in self.devices if d.hilo_id == identifier), None) # type: ignore + def find_device(self, id: int) -> HiloDevice: + return next((d for d in self.devices if d.id == id), None) # type: ignore def generate_device(self, device: dict) -> HiloDevice: device["location_id"] = self.location_id From d4fac94db9e3b8c8966b97553053efb5f80435f9 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 22:04:21 -0500 Subject: [PATCH 06/12] add hiloId to device --- pyhilo/device/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyhilo/device/__init__.py b/pyhilo/device/__init__.py index ce48ade..12a0fac 100644 --- a/pyhilo/device/__init__.py +++ b/pyhilo/device/__init__.py @@ -1,4 +1,5 @@ """Define devices""" + from __future__ import annotations from dataclasses import dataclass, field @@ -36,6 +37,7 @@ def __init__( ) -> None: self._api = api self.id = 0 + self.hilo_id: str = "" self.location_id = 0 self.type = "Unknown" self.name = "Unknown" @@ -59,6 +61,7 @@ def update(self, **kwargs: Dict[str, Union[str, int, Dict]]) -> None: value = val.get("value") reading = { "deviceId": self.id, + "hiloId": self.hilo_id, "locationId": self.location_id, "timeStampUTC": datetime.utcnow().isoformat(), "value": value, @@ -234,6 +237,7 @@ def __init__(self, **kwargs: Dict[str, Any]): self.id = 0 self.value: Union[int, bool, str] = 0 self.device_id = 0 + self.hilo_id: str = "" self.device_attribute: DeviceAttribute self.__dict__.update({camel_to_snake(k): v for k, v in kwargs.items()}) self.unit_of_measurement = ( From 6907877ae3867130cc0501fb3635ba5f04950b87 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 2 Mar 2025 22:04:29 -0500 Subject: [PATCH 07/12] mapping qui marche pas --- pyhilo/device/graphql_value_mapper.py | 169 ++++++++++++-------------- 1 file changed, 81 insertions(+), 88 deletions(-) diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index 3c7ac25..8506df5 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -1,92 +1,85 @@ -from pyhilo.device import DeviceAttribute +from typing import Any, Dict +from pyhilo.device import DeviceAttribute, DeviceReading -class GraphqlValueMapper: - def __init__(self, value): - self.value = value +class GraphqlValueMapper: + """ + A class to map GraphQL values to DeviceReading instances. + """ - def map_thermostat(self)-> list[DeviceAttribute]: - return [ - DeviceAttribute( - name="disconnected", - value=self.value["disconnected"], - unit="none", - value_type="bool", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="unpaired", - value=False, - unit="none", - value_type="bool", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="CurrentTemperature", - value=self.value["mode"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="TargetTemperature", - value=self.value["battery"], - unit="%", - value_type="int", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="HeatDemand", - value=self.value["low_battery"], - value_type="bool", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="Heating", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="Power", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="GdState", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="Version", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="ZigbeeVersion", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="Humidity", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="ThermostatAllowedModes", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - DeviceAttribute( - name="ThermostatMode", - value=self.value["state"], - value_type="string", - device_id=self.value["device_id"], - ), - ] + def map_values(self, values: Dict[str, Any]) -> list[DeviceReading]: + devices_values: list[any] = values["getLocation"]["devices"] + readings: list[DeviceReading] = [] + for device in devices_values: + if device.get("deviceType") is not None: + attributes = self._map_devices_values(device) + reading = DeviceReading(hilo_id=device["hiloId"], attributes=attributes) + readings.append(reading) + return readings + def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceAttribute]: + match device["deviceType"]: + case "Tstat": + return [] # self._map_thermostats(device) + case _: + # Add the default logic here if needed + pass + + # TODO Abi: Figurer comment bien faire le mapping + # def _map_thermostats(self, thermostat: Dict[str, Any]) -> list[DeviceAttribute]: + # return [ + # DeviceAttribute( + # hilo_attribute="Disconnected", + # # TODO mettre dans des constantes + # attr=thermostat["connectionStatus"] == "Disconnected", + # ), + # DeviceAttribute( + # hilo_attribute="Unpaired", + # attr=False, + # ), + # DeviceAttribute( + # hilo_attribute="CurrentTemperature", + # attr=thermostat["ambientTemperature"]["value"], + # ), + # DeviceAttribute( + # hilo_attribute="TargetTemperature", + # attr=thermostat["ambientTempSetpoint"]["value"], + # ), + # DeviceAttribute( + # hilo_attribute="HeatDemand", + # attr=thermostat["heatDemand"], + # ), + # DeviceAttribute( + # hilo_attribute="Heating", + # attr=thermostat["power"]["value"] != "0" + # or thermostat["power"]["value"] is not None, + # ), + # DeviceAttribute( + # hilo_attribute="Power", + # attr=thermostat["power"]["value"], + # ), + # DeviceAttribute( + # hilo_attribute="GdState", + # attr=thermostat["gdState"] == "Active", + # ), + # DeviceAttribute( + # hilo_attribute="Version", + # attr=thermostat["version"], + # ), + # DeviceAttribute( + # hilo_attribute="ZigbeeVersion", + # attr=thermostat["zigbeeVersion"], + # ), + # DeviceAttribute( + # hilo_attribute="Humidity", + # attr=thermostat["ambientHumidity"], + # ), + # DeviceAttribute( + # hilo_attribute="ThermostatAllowedModes", + # attr=thermostat["allowedModes"], + # ), + # DeviceAttribute( + # hilo_attribute="ThermostatMode", + # attr=thermostat["mode"], + # ), + # ] From 39ac926b438ea0e966add3711b3a4e5324e2fb39 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Tue, 4 Mar 2025 22:17:28 -0500 Subject: [PATCH 08/12] Refactor GraphQL value mapping and device identification logic --- pyhilo/api.py | 11 +- pyhilo/device/graphql_value_mapper.py | 165 ++++++++++++++++---------- pyhilo/devices.py | 17 ++- 3 files changed, 119 insertions(+), 74 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index f45aad5..2dbfb6c 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -535,11 +535,11 @@ async def call_get_location_query(self, location_hilo_id: string) -> Dict[str, A gDState version zigbeeVersion - ambientTemperature { + ambientTemperature { value kind } - ambientTempSetpoint { + ambientTempSetpoint { value kind } @@ -547,19 +547,22 @@ async def call_get_location_query(self, location_hilo_id: string) -> Dict[str, A value kind } + heatDemand + allowedModes + mode } } } } """) - + async with client as session: result = await session.execute( query, variable_values={"locationHiloId": location_hilo_id} ) LOG.info(result) return result - + async def _set_device_attribute( self, device: HiloDevice, diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index 8506df5..f54aaa0 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -1,5 +1,6 @@ -from typing import Any, Dict -from pyhilo.device import DeviceAttribute, DeviceReading +from typing import Any, Dict, Union +from pyhilo.device import DeviceReading +from datetime import datetime, timezone class GraphqlValueMapper: @@ -7,79 +8,113 @@ class GraphqlValueMapper: A class to map GraphQL values to DeviceReading instances. """ + def __init__(self, api: Any): + self._api = api + def map_values(self, values: Dict[str, Any]) -> list[DeviceReading]: devices_values: list[any] = values["getLocation"]["devices"] readings: list[DeviceReading] = [] for device in devices_values: if device.get("deviceType") is not None: - attributes = self._map_devices_values(device) - reading = DeviceReading(hilo_id=device["hiloId"], attributes=attributes) - readings.append(reading) + reading = self._map_devices_values(device) + readings.extend(reading) return readings - def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceAttribute]: + def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: match device["deviceType"]: case "Tstat": - return [] # self._map_thermostats(device) + attributes = self._map_thermostats(device) + return self._map_to_device_reading(attributes) case _: # Add the default logic here if needed pass - # TODO Abi: Figurer comment bien faire le mapping - # def _map_thermostats(self, thermostat: Dict[str, Any]) -> list[DeviceAttribute]: - # return [ - # DeviceAttribute( - # hilo_attribute="Disconnected", - # # TODO mettre dans des constantes - # attr=thermostat["connectionStatus"] == "Disconnected", - # ), - # DeviceAttribute( - # hilo_attribute="Unpaired", - # attr=False, - # ), - # DeviceAttribute( - # hilo_attribute="CurrentTemperature", - # attr=thermostat["ambientTemperature"]["value"], - # ), - # DeviceAttribute( - # hilo_attribute="TargetTemperature", - # attr=thermostat["ambientTempSetpoint"]["value"], - # ), - # DeviceAttribute( - # hilo_attribute="HeatDemand", - # attr=thermostat["heatDemand"], - # ), - # DeviceAttribute( - # hilo_attribute="Heating", - # attr=thermostat["power"]["value"] != "0" - # or thermostat["power"]["value"] is not None, - # ), - # DeviceAttribute( - # hilo_attribute="Power", - # attr=thermostat["power"]["value"], - # ), - # DeviceAttribute( - # hilo_attribute="GdState", - # attr=thermostat["gdState"] == "Active", - # ), - # DeviceAttribute( - # hilo_attribute="Version", - # attr=thermostat["version"], - # ), - # DeviceAttribute( - # hilo_attribute="ZigbeeVersion", - # attr=thermostat["zigbeeVersion"], - # ), - # DeviceAttribute( - # hilo_attribute="Humidity", - # attr=thermostat["ambientHumidity"], - # ), - # DeviceAttribute( - # hilo_attribute="ThermostatAllowedModes", - # attr=thermostat["allowedModes"], - # ), - # DeviceAttribute( - # hilo_attribute="ThermostatMode", - # attr=thermostat["mode"], - # ), - # ] + def _map_to_device_reading( + self, attributes: list[Dict[str, Any]] + ) -> list[DeviceReading]: + return [DeviceReading(**attr) for attr in attributes] + + def _map_thermostats(self, thermostat: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Disconnected", "null"), + "value": thermostat["connectionStatus"] == "Disconnected", + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Unpaired", "null"), + "value": False, + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("CurrentTemperature", "null"), + "value": thermostat["ambientTemperature"]["value"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("TargetTemperature", "null"), + "value": thermostat["ambientTempSetpoint"]["value"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("HeatDemand", "null"), + "value": thermostat["heatDemand"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Heating", "null"), + "value": thermostat["power"]["value"] != "0" + or thermostat["power"]["value"] is not None, + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Power", "null"), + "value": thermostat["power"]["value"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("GdState", "null"), + "value": thermostat["gDState"] == "Active", + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Version", "null"), + "value": thermostat["version"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("ZigbeeVersion", "null"), + "value": thermostat["zigbeeVersion"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("Humidity", "null"), + "value": thermostat["ambientHumidity"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts( + "ThermostatAllowedModes", "null" + ), + "value": thermostat["allowedModes"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + { + "hilo_id": thermostat["hiloId"], + "device_attribute": self._api.dev_atts("ThermostatMode", "null"), + "value": thermostat["mode"], + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + }, + ] diff --git a/pyhilo/devices.py b/pyhilo/devices.py index 9cf0106..8e05686 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -51,7 +51,10 @@ def _map_readings_to_devices( ) -> list[HiloDevice]: updated_devices = [] for reading in readings: - if device := self.find_device(reading.device_id): + device_identifier = reading.device_id + if device_identifier is None: + device_identifier = reading.hilo_id + if device := self.find_device(device_identifier): device.update_readings(reading) LOG.debug(f"{device} Received {reading}") if device not in updated_devices: @@ -62,8 +65,10 @@ def _map_readings_to_devices( ) return updated_devices - def find_device(self, id: int) -> HiloDevice: - return next((d for d in self.devices if d.id == id), None) # type: ignore + def find_device(self, device_identifier: int | str) -> HiloDevice: + if isinstance(device_identifier, int): + return next((d for d in self.devices if d.id == device_identifier), None) + return next((d for d in self.devices if d.hilo_id == device_identifier), None) def generate_device(self, device: dict) -> HiloDevice: device["location_id"] = self.location_id @@ -114,6 +119,8 @@ async def async_init(self) -> None: self.location_id = location_ids[0] self.location_hilo_id = location_ids[1] await self.update() + #TODO AA - trouver ou placer ça pour que ça fasse du sens, pas sûre que c'est ici values = await self._api.call_get_location_query(self.location_hilo_id) - mapper = GraphqlValueMapper() - mapper.map_values(values) + mapper = GraphqlValueMapper(self._api) + readings = mapper.map_values(values) + self._map_readings_to_devices(readings) From 7a62d8601081fae6c10c29743b5320e7a214d19a Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Thu, 6 Mar 2025 22:19:54 -0500 Subject: [PATCH 09/12] refacto + add les types d'appareil --- pyhilo/device/graphql_value_mapper.py | 205 +++++++++++++++----------- 1 file changed, 122 insertions(+), 83 deletions(-) diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index f54aaa0..c55a89e 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Union +from typing import Any, Dict, Generator, Union from pyhilo.device import DeviceReading from datetime import datetime, timezone @@ -21,10 +21,25 @@ def map_values(self, values: Dict[str, Any]) -> list[DeviceReading]: return readings def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: + attributes: list[Dict[str, Any]] = self._map_basic_device(device) match device["deviceType"]: case "Tstat": - attributes = self._map_thermostats(device) + attributes.extend(self._build_thermostat(device)) return self._map_to_device_reading(attributes) + # case "CCE": # Water Heater + # attributes.extend(self._build_water_heater(device)) + # return self._map_to_device_reading(attributes) + # case "CCR": # ChargeController + # attributes.extend(self._build_charge_controller(device)) + # return self._map_to_device_reading(attributes) + # case "HeatingFloor": + # case "LowVoltageTstat": + # case "ChargingPoint": + # case "Meter": + # case "Hub": # Gateway + # case "ColorBulb": + # case "Dimmer": + # case "Switch": case _: # Add the default logic here if needed pass @@ -34,87 +49,111 @@ def _map_to_device_reading( ) -> list[DeviceReading]: return [DeviceReading(**attr) for attr in attributes] - def _map_thermostats(self, thermostat: Dict[str, Any]) -> list[Dict[str, Any]]: + def _build_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: return [ - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Disconnected", "null"), - "value": thermostat["connectionStatus"] == "Disconnected", - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Unpaired", "null"), - "value": False, - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("CurrentTemperature", "null"), - "value": thermostat["ambientTemperature"]["value"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("TargetTemperature", "null"), - "value": thermostat["ambientTempSetpoint"]["value"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("HeatDemand", "null"), - "value": thermostat["heatDemand"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Heating", "null"), - "value": thermostat["power"]["value"] != "0" - or thermostat["power"]["value"] is not None, - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Power", "null"), - "value": thermostat["power"]["value"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("GdState", "null"), - "value": thermostat["gDState"] == "Active", - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Version", "null"), - "value": thermostat["version"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("ZigbeeVersion", "null"), - "value": thermostat["zigbeeVersion"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("Humidity", "null"), - "value": thermostat["ambientHumidity"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts( - "ThermostatAllowedModes", "null" + self.build_attribute( + device["hiloId"], + "CurrentTemperature", + device["ambientTemperature"]["value"], + ), + self.build_attribute( + device["hiloId"], + "TargetTemperature", + device["ambientTempSetpoint"]["value"], + ), + self.build_attribute(device["hiloId"], "HeatDemand", device["heatDemand"]), + self.build_attribute( + device["hiloId"], + "Heating", + device["power"]["value"] != "0" or device["power"]["value"] is not None, + ), + self.build_attribute(device["hiloId"], "Power", device["power"]["value"]), + self.build_attribute(device["hiloId"], "Version", device["version"]), + self.build_attribute( + device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] + ), + self.build_attribute( + device["hiloId"], "Humidity", device["ambientHumidity"] + ), + self.build_attribute( + device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] + ), + self.build_attribute(device["hiloId"], "ThermostatMode", device["mode"]), + self._map_gd_state(device), + ] + + def _build_water_heater(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + attributes = self._build_charge_controller(device) + attributes.extend( + [ + self.build_attribute( + device["hiloId"], "Power", device["power"]["value"] + ), + self.build_attribute( + device["hiloId"], "GdState", device["gDState"] == "Active" + ), + self.build_attribute(device["hiloId"], "Version", device["version"]), + self.build_attribute( + device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] + ), + self.build_attribute( + device["hiloId"], "Alerts", device["ambientHumidity"] + ), + self.build_attribute( + device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] + ), + self.build_attribute( + device["hiloId"], "ThermostatMode", device["mode"] + ), + self.build_attribute( + device["hiloId"], + "Disconnected", + device["connectionStatus"] == "Disconnected", ), - "value": thermostat["allowedModes"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, - { - "hilo_id": thermostat["hiloId"], - "device_attribute": self._api.dev_atts("ThermostatMode", "null"), - "value": thermostat["mode"], - "timeStampUTC": datetime.now(timezone.utc).isoformat(), - }, + self.build_attribute(device["hiloId"], "Unpaired", False), + ] + ) + + def _build_charge_controller(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self.build_attribute(device["hiloId"], "Power", device["power"]["value"]), + self.build_attribute(device["hiloId"], "Version", device["version"]), + self.build_attribute( + device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] + ), + self._map_gd_state(device), + self._map_drms_state(device), + ] + + def _map_basic_device(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self.build_attribute(device["hiloId"], "Unpaired", False), + self.build_attribute( + device["hiloId"], + "Disconnected", + device["connectionStatus"] == "Disconnected", + ), ] + + def _map_gd_state(self, device: Dict[str, Any]) -> Dict[str, Any]: + return self.build_attribute( + device["hiloId"], "GdState", device["gDState"] == "Active" + ) + + # TODO - AA Map selon le GD STATE + def _map_drms_state(self, device: Dict[str, Any]) -> Dict[str, Any]: + return ( + self.build_attribute( + device["hiloId"], "DrmsState", device["gDState"] == "Active" + ), + ) + + def build_attribute( + self, hilo_id: str, device_attribute: str, value: Any + ) -> dict[str, Any]: + return { + "hilo_id": hilo_id, + "device_attribute": self._api.dev_atts(device_attribute, "null"), + "value": value, + "timeStampUTC": datetime.now(timezone.utc).isoformat(), + } From 37277ed67be4cef8ee95fda2d75001cab4585c09 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Sun, 9 Mar 2025 21:21:54 -0400 Subject: [PATCH 10/12] Enhance GraphQL value mapping with additional device types and attributes --- pyhilo/device/graphql_value_mapper.py | 155 ++++++++++++++++++-------- 1 file changed, 110 insertions(+), 45 deletions(-) diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index c55a89e..4eed690 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -25,30 +25,52 @@ def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: match device["deviceType"]: case "Tstat": attributes.extend(self._build_thermostat(device)) - return self._map_to_device_reading(attributes) - # case "CCE": # Water Heater - # attributes.extend(self._build_water_heater(device)) - # return self._map_to_device_reading(attributes) - # case "CCR": # ChargeController - # attributes.extend(self._build_charge_controller(device)) - # return self._map_to_device_reading(attributes) - # case "HeatingFloor": + case "CCE": # Water Heater + attributes.extend(self._build_water_heater(device)) + case "CCR": # ChargeController + attributes.extend(self._build_charge_controller(device)) + case "HeatingFloor": + attributes.extend(self._build_floor_thermostat(device)) # case "LowVoltageTstat": # case "ChargingPoint": - # case "Meter": - # case "Hub": # Gateway - # case "ColorBulb": - # case "Dimmer": - # case "Switch": + case "Meter": + attributes.extend(self._build_smart_meter(device)) + case "Hub": # Gateway + attributes.extend(self._build_gateway(device)) + case "ColorBulb": + attributes.extend(self._build_light(device)) + case "Dimmer": + attributes.extend(self._build_dimmer(device)) + case "Switch": + attributes.extend(self._build_switch(device)) case _: - # Add the default logic here if needed pass + return self._map_to_device_reading(attributes) def _map_to_device_reading( self, attributes: list[Dict[str, Any]] ) -> list[DeviceReading]: return [DeviceReading(**attr) for attr in attributes] + def _build_smart_meter(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self.build_attribute(device["hiloId"], "LastUpdate", device["LastUpdate"]), + self.build_attribute(device["hiloId"], "ZigbeeChannel", device["zigbeeChannel"]), + self.build_attribute(device["hiloId"], "Disconnected", device["IsDisconnected"]), #??? gateway value?? + self._map_power(device), + ] + + def _build_gateway(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self.build_attribute(device["hiloId"], "LastStatusTime", device["LastConnectionTime"]), + self.build_attribute(device["hiloId"], "Version", device["controllerSoftwareVersion"]), + self.build_attribute(device["hiloId"], "Disconnected", device["connectionStatus"] == 2), # Offline + self.build_attribute(device["hiloId"], "ZigbeePairingActivated", device["zigBeePairingMode"]), + self.build_attribute(device["hiloId"], "ZigbeeChannel", device["zigbeeChannel"]), + self.build_attribute(device["hiloId"], "WillBeConnectedToSmartMeter", device["willBeConnectedToSmartMeter"]), + self.build_attribute(device["hiloId"], "SmartMeterUnpaired", device["smartMeterPairingStatus"]), + ] + def _build_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: return [ self.build_attribute( @@ -62,12 +84,6 @@ def _build_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: device["ambientTempSetpoint"]["value"], ), self.build_attribute(device["hiloId"], "HeatDemand", device["heatDemand"]), - self.build_attribute( - device["hiloId"], - "Heating", - device["power"]["value"] != "0" or device["power"]["value"] is not None, - ), - self.build_attribute(device["hiloId"], "Power", device["power"]["value"]), self.build_attribute(device["hiloId"], "Version", device["version"]), self.build_attribute( device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] @@ -78,51 +94,75 @@ def _build_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: self.build_attribute( device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] ), + self._map_power(device), + self._map_heating(device), self.build_attribute(device["hiloId"], "ThermostatMode", device["mode"]), self._map_gd_state(device), ] + def _build_floor_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + attributes = self._build_thermostat(device) + return attributes.extend( + self.build_attribute( + device["hiloId"], "FloorMode", self._map_to_floor_mode(device["floorMode"]) + ), + self.build_attribute( + device["hiloId"], "FloorLimit", device["FloorLimit"]["value"] + ), + ) + def _build_water_heater(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: attributes = self._build_charge_controller(device) - attributes.extend( + return attributes.extend( [ self.build_attribute( - device["hiloId"], "Power", device["power"]["value"] + device["hiloId"], "AbnormalTemperature", device["alerts"].contains("30") # AbnormalTemperature alert ), - self.build_attribute( - device["hiloId"], "GdState", device["gDState"] == "Active" - ), - self.build_attribute(device["hiloId"], "Version", device["version"]), - self.build_attribute( - device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] - ), - self.build_attribute( - device["hiloId"], "Alerts", device["ambientHumidity"] - ), - self.build_attribute( - device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] - ), - self.build_attribute( - device["hiloId"], "ThermostatMode", device["mode"] + self.build_attribute( + device["hiloId"], "CurrentTemperature", device["ProbeTemperature"]["value"]# AbnormalTemperature alert ), - self.build_attribute( - device["hiloId"], - "Disconnected", - device["connectionStatus"] == "Disconnected", - ), - self.build_attribute(device["hiloId"], "Unpaired", False), + self._map_heating(device), ] ) def _build_charge_controller(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: return [ - self.build_attribute(device["hiloId"], "Power", device["power"]["value"]), + self._map_power(device), self.build_attribute(device["hiloId"], "Version", device["version"]), self.build_attribute( device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] ), self._map_gd_state(device), self._map_drms_state(device), + self.build_attribute( + device["hiloId"], "CcrAllowedModes", device["ccrAllowedModes"] + ), + self.build_attribute( + device["hiloId"], "CcrMode", device["ccrMode"] + ), + ] + + def _build_switch(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self._map_power(device), + self.build_attribute(device["hiloId"], "Status", device["State"]), + self.build_attribute(device["hiloId"], "OnOff", device["State"]), + ] + + def _build_dimmer(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self._map_power(device), + self.build_attribute(device["hiloId"], "Intensity", device["Level"]["value"]/100), + self.build_attribute(device["hiloId"], "OnOff", device["State"]), + ] + + def _build_light(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + return [ + self.build_attribute(device["hiloId"], "ColorTemperature", device["ColorTemperature"]["value"]), + self.build_attribute(device["hiloId"], "Intensity", device["Level"]["value"]/100), + self.build_attribute(device["hiloId"], "OnOff", device["State"]), + self.build_attribute(device["hiloId"], "Hue", device.get("Hue") or 0), + self.build_attribute(device["hiloId"], "Saturation", device.get("Saturation") or 0), ] def _map_basic_device(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: @@ -146,7 +186,23 @@ def _map_drms_state(self, device: Dict[str, Any]) -> Dict[str, Any]: self.build_attribute( device["hiloId"], "DrmsState", device["gDState"] == "Active" ), - ) + ) + + def _map_heating(self, device: Dict[str, Any]) -> Dict[str, Any]: + return self.build_attribute( + device["hiloId"], + "Heating", + device["power"]["value"] != "0" or device["power"]["value"] is not None, + ), + + def _map_power(self, device: Dict[str, Any]) -> Dict[str, Any]: + return self.build_attribute( device["hiloId"],"Power", self._powerKwToW(device["power"]["value"], device["power"]["kind"])) + + def _powerKwToW(self, power: float, power_kind: int) -> float: + if power_kind == 12: # PowerKind.KW + return power * 1000 + + return power def build_attribute( self, hilo_id: str, device_attribute: str, value: Any @@ -157,3 +213,12 @@ def build_attribute( "value": value, "timeStampUTC": datetime.now(timezone.utc).isoformat(), } + + def _map_to_floor_mode( self, floor_mode: int) -> str: + match floor_mode: + case 0: + return "Ambient" + case 1: + return "Floor" + case 2: + return "Hybrid" From c0ea3a420e68775f9cca697f99d72863f9cc6f0d Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Mon, 10 Mar 2025 21:02:47 -0400 Subject: [PATCH 11/12] add charging point --- pyhilo/device/graphql_value_mapper.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index 4eed690..d6c4ef7 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -32,7 +32,7 @@ def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: case "HeatingFloor": attributes.extend(self._build_floor_thermostat(device)) # case "LowVoltageTstat": - # case "ChargingPoint": + case "ChargingPoint": case "Meter": attributes.extend(self._build_smart_meter(device)) case "Hub": # Gateway @@ -142,6 +142,19 @@ def _build_charge_controller(self, device: Dict[str, Any]) -> list[Dict[str, Any ), ] + def _build_charging_point(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + status = device["status"] + power_attribute: dict[str, Any] = {} + if status == 1 or status == 0: # is available (1) or OutOfService (0) + power_attribute = self.build_attribute(device["hiloId"], "Power", 0) + else: + power_attribute = self._map_power(device), + + return [ + self.build_attribute(device["hiloId"], "Status", status), + power_attribute + ] + def _build_switch(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: return [ self._map_power(device), @@ -196,7 +209,7 @@ def _map_heating(self, device: Dict[str, Any]) -> Dict[str, Any]: ), def _map_power(self, device: Dict[str, Any]) -> Dict[str, Any]: - return self.build_attribute( device["hiloId"],"Power", self._powerKwToW(device["power"]["value"], device["power"]["kind"])) + return self.build_attribute(device["hiloId"], "Power", self._powerKwToW(device["power"]["value"], device["power"]["kind"])) def _powerKwToW(self, power: float, power_kind: int) -> float: if power_kind == 12: # PowerKind.KW From 4398279545213d59f5f05940e76ec9b58e8753e4 Mon Sep 17 00:00:00 2001 From: Abigail Asselin Date: Tue, 11 Mar 2025 22:42:13 -0400 Subject: [PATCH 12/12] Refactor GraphQL location query and add GraphQlHelper for improved readability --- pyhilo/api.py | 38 +-- pyhilo/device/graphql_value_mapper.py | 423 +++++++++++++++++++------- pyhilo/devices.py | 4 +- pyhilo/graphql.py | 219 +++++++++++++ 4 files changed, 542 insertions(+), 142 deletions(-) create mode 100644 pyhilo/graphql.py diff --git a/pyhilo/api.py b/pyhilo/api.py index 2dbfb6c..9562b17 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -45,6 +45,7 @@ ) from pyhilo.device import DeviceAttribute, HiloDevice, get_device_attributes from pyhilo.exceptions import InvalidCredentialsError, RequestError +from pyhilo.graphql import GraphQlHelper from pyhilo.util.state import ( StateDict, WebsocketDict, @@ -519,42 +520,7 @@ async def call_get_location_query(self, location_hilo_id: string) -> Dict[str, A headers={"Authorization": f"Bearer {access_token}"}, ) client = Client(transport=transport, fetch_schema_from_transport=True) - query = gql(""" - query getLocation($locationHiloId: String!) { - getLocation(id:$locationHiloId) { - hiloId - lastUpdate - lastUpdateVersion - devices { - ... on BasicThermostat { - deviceType - hiloId - physicalAddress - connectionStatus - ambientHumidity - gDState - version - zigbeeVersion - ambientTemperature { - value - kind - } - ambientTempSetpoint { - value - kind - } - power { - value - kind - } - heatDemand - allowedModes - mode - } - } - } - } - """) + query = gql(GraphQlHelper.query_get_location()) async with client as session: result = await session.execute( diff --git a/pyhilo/device/graphql_value_mapper.py b/pyhilo/device/graphql_value_mapper.py index d6c4ef7..1f03f49 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -26,16 +26,17 @@ def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: case "Tstat": attributes.extend(self._build_thermostat(device)) case "CCE": # Water Heater - attributes.extend(self._build_water_heater(device)) + attributes.extend(self._build_water_heater(device)) case "CCR": # ChargeController attributes.extend(self._build_charge_controller(device)) case "HeatingFloor": - attributes.extend(self._build_floor_thermostat(device)) + attributes.extend(self._build_floor_thermostat(device)) # case "LowVoltageTstat": case "ChargingPoint": + attributes.extend(self._build_charging_point(device)) case "Meter": attributes.extend(self._build_smart_meter(device)) - case "Hub": # Gateway + case "Hub": # Gateway attributes.extend(self._build_gateway(device)) case "ColorBulb": attributes.extend(self._build_light(device)) @@ -53,130 +54,296 @@ def _map_to_device_reading( return [DeviceReading(**attr) for attr in attributes] def _build_smart_meter(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self.build_attribute(device["hiloId"], "LastUpdate", device["LastUpdate"]), - self.build_attribute(device["hiloId"], "ZigbeeChannel", device["zigbeeChannel"]), - self.build_attribute(device["hiloId"], "Disconnected", device["IsDisconnected"]), #??? gateway value?? - self._map_power(device), - ] + attributes = [] + if device.get("zigbeeChannel") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "ZigbeeChannel", device["zigBeeChannel"] + ) + ) + attributes.append( + self.build_attribute( + device["hiloId"], "Disconnected", device["connectionStatus"] == 2 + ), + ) + if device.get("power") is not None: + attributes.append(self._map_power(device)) + return attributes def _build_gateway(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self.build_attribute(device["hiloId"], "LastStatusTime", device["LastConnectionTime"]), - self.build_attribute(device["hiloId"], "Version", device["controllerSoftwareVersion"]), - self.build_attribute(device["hiloId"], "Disconnected", device["connectionStatus"] == 2), # Offline - self.build_attribute(device["hiloId"], "ZigbeePairingActivated", device["zigBeePairingMode"]), - self.build_attribute(device["hiloId"], "ZigbeeChannel", device["zigbeeChannel"]), - self.build_attribute(device["hiloId"], "WillBeConnectedToSmartMeter", device["willBeConnectedToSmartMeter"]), - self.build_attribute(device["hiloId"], "SmartMeterUnpaired", device["smartMeterPairingStatus"]), - ] - - def _build_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ + attributes = [] + attributes.append( + self.build_attribute( + device["hiloId"], "LastStatusTime", device["lastConnectionTime"] + ) + ) + attributes.append( + self.build_attribute( + device["hiloId"], "Version", device["controllerSoftwareVersion"] + ) + ) + attributes.append( + self.build_attribute( + device["hiloId"], "Disconnected", device["connectionStatus"] == 2 + ) + ) # Offline + attributes.append( self.build_attribute( device["hiloId"], - "CurrentTemperature", - device["ambientTemperature"]["value"], - ), + "ZigbeePairingActivated", + device["zigBeePairingModeEnhanced"], + ) + ) + if device.get("zigBeeChannel") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "ZigbeeChannel", device["zigBeeChannel"] + ) + ) + attributes.append( self.build_attribute( device["hiloId"], - "TargetTemperature", - device["ambientTempSetpoint"]["value"], - ), - self.build_attribute(device["hiloId"], "HeatDemand", device["heatDemand"]), - self.build_attribute(device["hiloId"], "Version", device["version"]), + "WillBeConnectedToSmartMeter", + device["willBeConnectedToSmartMeter"], + ) + ) + attributes.append( + self.build_attribute( + device["hiloId"], + "SmartMeterUnpaired", + device["smartMeterPairingStatus"], + ) + ) + return attributes + + def _build_thermostat( + self, device: Dict[str, Any], withDefaultMinMaxTemp: bool = True + ) -> list[Dict[str, Any]]: + attributes = [] + + if device.get("ambientTemperature") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], + "CurrentTemperature", + device["ambientTemperature"]["value"], + ) + ) + + if device.get("ambientTempSetpoint") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], + "TargetTemperature", + device["ambientTempSetpoint"]["value"], + ) + ) + + if device.get("heatDemand") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "HeatDemand", device["heatDemand"] + ) + ) + attributes.append( + self.build_attribute(device["hiloId"], "Version", device["version"]) + ) + attributes.append( self.build_attribute( device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] - ), + ) + ) + attributes.append( self.build_attribute( - device["hiloId"], "Humidity", device["ambientHumidity"] - ), + device["hiloId"], "Humidity", device.get("ambientHumidity") or 0 + ) + ) + if device.get("allowedModes") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] + ) + ) + + if device.get("power") is not None: + attributes.append(self._map_power(device)) + attributes.append(self._map_heating(device)) + attributes.append( self.build_attribute( - device["hiloId"], "ThermostatAllowedModes", device["allowedModes"] - ), - self._map_power(device), - self._map_heating(device), - self.build_attribute(device["hiloId"], "ThermostatMode", device["mode"]), - self._map_gd_state(device), - ] + device["hiloId"], + "ThermostatMode", + self._map_to_thermostat_mode(device.get("mode")), + ) + ) + attributes.append(self._map_gd_state(device)) + if withDefaultMinMaxTemp: + attributes.extend( + [ + self.build_attribute(device["hiloId"], "MaxTempSetpoint", 30), + self.build_attribute(device["hiloId"], "MinTempSetpoint", 5), + ] + ) + else: + if device.get("maxAmbientTempSetpoint") is not None: + self.build_attribute( + device["maxAmbientTempSetpoint"], + "MaxTempSetpoint", + device["maxAmbientTempSetpoint"]["value"], + ) + if device.get("minAmbientTempSetpoint") is not None: + self.build_attribute( + device["hiloId"], + "MinTempSetpoint", + device["minAmbientTempSetpoint"]["value"], + ) + + if device.get("maxAmbientTempSetpointLimit") is not None: + self.build_attribute( + device["hiloId"], + "MaxTempSetpointLimit", + device["maxAmbientTempSetpointLimit"]["value"], + ) + if device.get("minAmbientTempSetpointLimit") is not None: + self.build_attribute( + device["hiloId"], + "MinTempSetpointLimit", + device["minAmbientTempSetpointLimit"]["value"], + ) + return attributes def _build_floor_thermostat(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: attributes = self._build_thermostat(device) - return attributes.extend( - self.build_attribute( - device["hiloId"], "FloorMode", self._map_to_floor_mode(device["floorMode"]) - ), + attributes.append( self.build_attribute( - device["hiloId"], "FloorLimit", device["FloorLimit"]["value"] - ), + device["hiloId"], + "FloorMode", + self._map_to_floor_mode(device["floorMode"]), + ) ) + if device.get("floorLimit") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "FloorLimit", device["floorLimit"]["value"] + ) + ) + return attributes def _build_water_heater(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: attributes = self._build_charge_controller(device) - return attributes.extend( - [ - self.build_attribute( - device["hiloId"], "AbnormalTemperature", device["alerts"].contains("30") # AbnormalTemperature alert - ), - self.build_attribute( - device["hiloId"], "CurrentTemperature", device["ProbeTemperature"]["value"]# AbnormalTemperature alert - ), - self._map_heating(device), - ] + attributes.append( + self.build_attribute( + device["hiloId"], + "AbnormalTemperature", + device.get("alerts") is not None + and "30" in device["alerts"], # AbnormalTemperature alert + ) ) + if device.get("probeTemp") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], + "CurrentTemperature", + device["probeTemp"]["value"], # AbnormalTemperature alert + ) + ) + attributes.append(self._map_heating(device)) + return attributes def _build_charge_controller(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self._map_power(device), - self.build_attribute(device["hiloId"], "Version", device["version"]), + attributes = [] + if device.get("power") is not None: + attributes.append(self._map_power(device)) + + attributes.append( + self.build_attribute(device["hiloId"], "Version", device["version"]) + ) + attributes.append( self.build_attribute( device["hiloId"], "ZigbeeVersion", device["zigbeeVersion"] - ), - self._map_gd_state(device), - self._map_drms_state(device), - self.build_attribute( + ) + ) + attributes.append(self._map_gd_state(device)) + attributes.append(self._map_drms_state(device)) + if device.get("ccrAllowedModes") is not None: + attributes.append( + self.build_attribute( device["hiloId"], "CcrAllowedModes", device["ccrAllowedModes"] - ), + ) + ) + attributes.append( self.build_attribute( - device["hiloId"], "CcrMode", device["ccrMode"] - ), - ] + device["hiloId"], "CcrMode", self._map_to_ccr_mode(device["ccrMode"]) + ) + ) + return attributes def _build_charging_point(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: + attributes = [] status = device["status"] - power_attribute: dict[str, Any] = {} - if status == 1 or status == 0: # is available (1) or OutOfService (0) - power_attribute = self.build_attribute(device["hiloId"], "Power", 0) - else: - power_attribute = self._map_power(device), + if status in (1, 0): # is available (1) or OutOfService (0) + attributes.append(self.build_attribute(device["hiloId"], "Power", 0)) + elif device.get("power") is not None: + attributes.append((self._map_power(device))) - return [ - self.build_attribute(device["hiloId"], "Status", status), - power_attribute - ] + attributes.append(self.build_attribute(device["hiloId"], "Status", status)) + return attributes def _build_switch(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self._map_power(device), - self.build_attribute(device["hiloId"], "Status", device["State"]), - self.build_attribute(device["hiloId"], "OnOff", device["State"]), - ] + attributes = [] + if device.get("power") is not None: + attributes.append(self._map_power(device)) + attributes.append( + self.build_attribute(device["hiloId"], "Status", device["state"]) + ) + attributes.append( + self.build_attribute(device["hiloId"], "OnOff", device["state"]) + ) + return attributes def _build_dimmer(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self._map_power(device), - self.build_attribute(device["hiloId"], "Intensity", device["Level"]["value"]/100), - self.build_attribute(device["hiloId"], "OnOff", device["State"]), - ] + attributes = [] + if device.get("power") is not None: + attributes.append(self._map_power(device)) + if device.get("Level") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "Intensity", device["Level"]["value"] / 100 + ) + ) + attributes.append( + self.build_attribute(device["hiloId"], "OnOff", device["state"]) + ) + return attributes def _build_light(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: - return [ - self.build_attribute(device["hiloId"], "ColorTemperature", device["ColorTemperature"]["value"]), - self.build_attribute(device["hiloId"], "Intensity", device["Level"]["value"]/100), - self.build_attribute(device["hiloId"], "OnOff", device["State"]), - self.build_attribute(device["hiloId"], "Hue", device.get("Hue") or 0), - self.build_attribute(device["hiloId"], "Saturation", device.get("Saturation") or 0), - ] + attributes = [] + if device.get("colorTemperature") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], + "ColorTemperature", + device["colorTemperature"]["value"], + ) + ) + if device.get("level") is not None: + attributes.append( + self.build_attribute( + device["hiloId"], "Intensity", device["level"]["value"] / 100 + ) + ) + if device.get("lightType") == 1: # LightType.Color + attributes.append( + self.build_attribute(device["hiloId"], "Hue", device.get("hue") or 0) + ) + attributes.append( + self.build_attribute( + device["hiloId"], "Saturation", device.get("saturation") or 0 + ) + ) + attributes.append( + self.build_attribute(device["hiloId"], "OnOff", device["atate"]) + ) + return attributes def _map_basic_device(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: return [ @@ -193,33 +360,41 @@ def _map_gd_state(self, device: Dict[str, Any]) -> Dict[str, Any]: device["hiloId"], "GdState", device["gDState"] == "Active" ) - # TODO - AA Map selon le GD STATE def _map_drms_state(self, device: Dict[str, Any]) -> Dict[str, Any]: return ( self.build_attribute( device["hiloId"], "DrmsState", device["gDState"] == "Active" ), - ) + ) def _map_heating(self, device: Dict[str, Any]) -> Dict[str, Any]: - return self.build_attribute( - device["hiloId"], - "Heating", - device["power"]["value"] != "0" or device["power"]["value"] is not None, - ), + power = device.get("power") + if ( + power is not None + and device["power"]["value"] is not None + and device["power"]["value"] > 0 + ): + return self.build_attribute(device["hiloId"], "Heating", 1) + + return self.build_attribute(device["hiloId"], "Heating", 0) def _map_power(self, device: Dict[str, Any]) -> Dict[str, Any]: - return self.build_attribute(device["hiloId"], "Power", self._powerKwToW(device["power"]["value"], device["power"]["kind"])) + value = device["power"]["value"] if device["power"]["value"] is not None else 0 + return self.build_attribute( + device["hiloId"], + "Power", + self._power_kw_to_w(value, device["power"]["kind"]), + ) - def _powerKwToW(self, power: float, power_kind: int) -> float: - if power_kind == 12: # PowerKind.KW + def _power_kw_to_w(self, power: float, power_kind: int) -> float: + if power_kind == 12: # PowerKind.KW return power * 1000 return power def build_attribute( self, hilo_id: str, device_attribute: str, value: Any - ) -> dict[str, Any]: + ) -> Dict[str, Any]: return { "hilo_id": hilo_id, "device_attribute": self._api.dev_atts(device_attribute, "null"), @@ -227,7 +402,7 @@ def build_attribute( "timeStampUTC": datetime.now(timezone.utc).isoformat(), } - def _map_to_floor_mode( self, floor_mode: int) -> str: + def _map_to_floor_mode(self, floor_mode: int) -> str: match floor_mode: case 0: return "Ambient" @@ -235,3 +410,43 @@ def _map_to_floor_mode( self, floor_mode: int) -> str: return "Floor" case 2: return "Hybrid" + + def _map_to_thermostat_mode(self, mode: int) -> str: + match mode: + case 0: + return "Unknown" + case 1: + return "Heat" + case 2: + return "Auto" + case 3: + return "AutoHeat" + case 4: + return "EmergencyHeat" + case 5: + return "Cool" + case 6: + return "AutoCool" + case 7: + return "SouthernAway" + case 8: + return "Off" + case 9: + return "Manual" + case 10: + return "AutoBypass" + case _: + return "" + + def _map_to_ccr_mode(self, ccr_mode: int) -> str: + match ccr_mode: + case 0: + return "Unknown" + case 1: + return "Auto" + case 2: + return "Off" + case 3: + return "Manual" + case _: + return "" diff --git a/pyhilo/devices.py b/pyhilo/devices.py index 8e05686..a4e8523 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -52,7 +52,7 @@ def _map_readings_to_devices( updated_devices = [] for reading in readings: device_identifier = reading.device_id - if device_identifier is None: + if device_identifier == 0: device_identifier = reading.hilo_id if device := self.find_device(device_identifier): device.update_readings(reading) @@ -119,7 +119,7 @@ async def async_init(self) -> None: self.location_id = location_ids[0] self.location_hilo_id = location_ids[1] await self.update() - #TODO AA - trouver ou placer ça pour que ça fasse du sens, pas sûre que c'est ici + # TODO AA - trouver ou placer ça pour que ça fasse du sens, pas sûre que c'est ici values = await self._api.call_get_location_query(self.location_hilo_id) mapper = GraphqlValueMapper(self._api) readings = mapper.map_values(values) diff --git a/pyhilo/graphql.py b/pyhilo/graphql.py new file mode 100644 index 0000000..faa26d5 --- /dev/null +++ b/pyhilo/graphql.py @@ -0,0 +1,219 @@ +class GraphQlHelper: + + def query_get_location() -> str: + return """query getLocation($locationHiloId: String!) { + getLocation(id:$locationHiloId) { + hiloId + lastUpdate + lastUpdateVersion + devices { + deviceType + hiloId + physicalAddress + connectionStatus + ... on Gateway { + connectionStatus + controllerSoftwareVersion + lastConnectionTime + willBeConnectedToSmartMeter + zigBeeChannel + zigBeePairingModeEnhanced + smartMeterZigBeeChannel + smartMeterPairingStatus + } + ... on BasicSmartMeter { + deviceType + hiloId + physicalAddress + connectionStatus + zigBeeChannel + power { + value + kind + } + } + ... on LowVoltageThermostat { + coolTempSetpoint { + value + } + fanMode + fanSpeed + mode + currentState + power { + value + kind + } + ambientHumidity + gDState + ambientTemperature { + value + kind + } + ambientTempSetpoint { + value + kind + } + version + zigbeeVersion + connectionStatus + maxAmbientCoolSetPoint { + value + kind + } + minAmbientCoolSetPoint { + value + kind + } + maxAmbientTempSetpoint { + value + kind + } + minAmbientTempSetpoint { + value + kind + } + allowedModes + fanAllowedModes + } + ... on BasicSwitch { + deviceType + hiloId + physicalAddress + connectionStatus + state + power { + value + kind + } + } + ... on BasicLight { + deviceType + hiloId + physicalAddress + connectionStatus + state + hue + level + saturation + colorTemperature + lightType + } + ... on BasicEVCharger { + deviceType + hiloId + physicalAddress + connectionStatus + status + power { + value + kind + } + } + ... on BasicChargeController { + deviceType + hiloId + physicalAddress + connectionStatus + gDState + version + zigbeeVersion + state + power { + value + kind + } + } + ... on HeatingFloorThermostat { + deviceType + hiloId + physicalAddress + connectionStatus + ambientHumidity + gDState + version + zigbeeVersion + thermostatType + physicalAddress + floorMode + power { + value + kind + } + ambientTemperature { + value + kind + } + ambientTempSetpoint { + value + kind + } + maxAmbientTempSetpoint { + value + kind + } + minAmbientTempSetpoint { + value + kind + } + floorLimit { + value + } + } + ... on WaterHeater { + deviceType + hiloId + physicalAddress + connectionStatus + gDState + version + probeTemp { + value + kind + } + zigbeeVersion + state + ccrType + alerts + power { + value + kind + } + } + ... on BasicDimmer { + deviceType + hiloId + physicalAddress + connectionStatus + state + level + power { + value + kind + } + } + ... on BasicThermostat { + deviceType + hiloId + physicalAddress + connectionStatus + ambientHumidity + gDState + version + zigbeeVersion + ambientTemperature { + value + kind + } + ambientTempSetpoint { + value + kind + } + power { + value + kind + } + } + } + } + }""" \ No newline at end of file