diff --git a/pyhilo/api.py b/pyhilo/api.py index dac6b3c..444fa74 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -12,8 +12,6 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientResponseError import backoff -from gql import Client, gql -from gql.transport.aiohttp import AIOHTTPTransport from pyhilo.const import ( ANDROID_CLIENT_ENDPOINT, @@ -45,7 +43,6 @@ ) 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, @@ -513,22 +510,6 @@ 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) -> 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(GraphQlHelper.query_get_location()) - - 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 6a52e79..48b2e31 100644 --- a/pyhilo/device/graphql_value_mapper.py +++ b/pyhilo/device/graphql_value_mapper.py @@ -1,6 +1,4 @@ -from datetime import datetime, timezone -from typing import Any, Dict, Generator, Union - +from typing import Any, Dict from pyhilo.device import DeviceReading @@ -9,45 +7,50 @@ 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: + def map_query_values(self, values: Dict[str, Any]) -> list[Dict[str, Any]]: + readings: list[Dict[str, Any]] = [] + for device in values: if device.get("deviceType") is not None: reading = self._map_devices_values(device) readings.extend(reading) return readings - def _map_devices_values(self, device: Dict[str, Any]) -> list[DeviceReading]: + def map_subscription_values( + self, device: list[Dict[str, Any]] + ) -> list[Dict[str, Any]]: + readings: list[Dict[str, Any]] = [] + if device.get("deviceType") is not None: + reading = self._map_devices_values(device) + readings.extend(reading) + return readings + + def _map_devices_values(self, device: Dict[str, Any]) -> list[Dict[str, Any]]: attributes: list[Dict[str, Any]] = self._map_basic_device(device) - match device["deviceType"]: - case "Tstat": + match device["deviceType"].lower(): + case "tstat": attributes.extend(self._build_thermostat(device)) - case "CCE": # Water Heater + case "cce": # Water Heater attributes.extend(self._build_water_heater(device)) - case "CCR": # ChargeController + case "ccr": # ChargeController attributes.extend(self._build_charge_controller(device)) - case "HeatingFloor": + case "heatingfloor": attributes.extend(self._build_floor_thermostat(device)) - # case "LowVoltageTstat": - case "ChargingPoint": + # case "lowvoltagetstat": + case "chargingpoint": attributes.extend(self._build_charging_point(device)) - case "Meter": + case "meter": attributes.extend(self._build_smart_meter(device)) - case "Hub": # Gateway + case "hub": # Gateway attributes.extend(self._build_gateway(device)) - case "ColorBulb": + case "colorbulb": attributes.extend(self._build_light(device)) - case "Dimmer": + case "dimmer": attributes.extend(self._build_dimmer(device)) - case "Switch": + case "switch": attributes.extend(self._build_switch(device)) case _: pass - return self._map_to_device_reading(attributes) + return attributes def _map_to_device_reading( self, attributes: list[Dict[str, Any]] @@ -398,7 +401,8 @@ def build_attribute( ) -> Dict[str, Any]: return { "hilo_id": hilo_id, - "device_attribute": self._api.dev_atts(device_attribute, "null"), + "attribute": device_attribute, + "valueType": "null", "value": value, "timeStampUTC": datetime.now(timezone.utc).isoformat(), } diff --git a/pyhilo/devices.py b/pyhilo/devices.py index a4e8523..2b826f7 100644 --- a/pyhilo/devices.py +++ b/pyhilo/devices.py @@ -4,10 +4,9 @@ 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 +from pyhilo.device.switch import Switch class Devices: @@ -119,8 +118,3 @@ 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(self._api) - readings = mapper.map_values(values) - self._map_readings_to_devices(readings) diff --git a/pyhilo/graphql.py b/pyhilo/graphql.py index 45b2645..b403271 100644 --- a/pyhilo/graphql.py +++ b/pyhilo/graphql.py @@ -1,6 +1,34 @@ +import asyncio +from typing import Any, Dict +from gql import gql, Client +from gql.transport.aiohttp import AIOHTTPTransport +from gql.transport.websockets import WebsocketsTransport +from pyhilo.device.graphql_value_mapper import GraphqlValueMapper +from pyhilo import API +from pyhilo.devices import Devices +from typing import List, Optional + + class GraphQlHelper: - def query_get_location() -> str: - return """query getLocation($locationHiloId: String!) { + """The GraphQl Helper class.""" + + def __init__(self, api: API, devices: Devices): + self._api = api + self._devices = devices + self.access_token = "" + self.mapper: GraphqlValueMapper = GraphqlValueMapper() + + self.subscriptions: List[Optional[asyncio.Task]] = [None] + + async def async_init(self) -> None: + """Initialize the Hilo "GraphQlHelper" class.""" + self.access_token = await self._api.async_get_access_token() + self.subscriptions[0] = asyncio.create_task( + self._subscribe_to_device_updated(self._devices.location_hilo_id) + ) + await self.call_get_location_query(self._devices.location_hilo_id) + + QUERY_GET_LOCATION: str = """query getLocation($locationHiloId: String!) { getLocation(id:$locationHiloId) { hiloId lastUpdate @@ -166,7 +194,7 @@ def query_get_location() -> str: connectionStatus gDState version - probeTemp { + probeTemp { value kind } @@ -200,11 +228,11 @@ def query_get_location() -> str: gDState version zigbeeVersion - ambientTemperature { + ambientTemperature { value kind } - ambientTempSetpoint { + ambientTempSetpoint { value kind } @@ -215,4 +243,264 @@ def query_get_location() -> str: } } } - }""" + }""" + + SUBSCRIPTION_DEVICE_UPDATED: str = """subscription onAnyDeviceUpdated($locationHiloId: String!) { + onAnyDeviceUpdated(locationHiloId: $locationHiloId) { + deviceType + locationHiloId + transmissionTime + operationId + status + device { + ... on Gateway { + connectionStatus + controllerSoftwareVersion + lastConnectionTime + willBeConnectedToSmartMeter + zigBeeChannel + zigBeePairingModeEnhanced + smartMeterZigBeeChannel + smartMeterPairingStatus + } + ... on BasicSmartMeter { + deviceType + hiloId + physicalAddress + connectionStatus + zigBeeChannel + power { + value + kind + } + } + ... on LowVoltageThermostat { + deviceType + hiloId + physicalAddress + 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 + } + } + } + } +}""" + + async def call_get_location_query(self, location_hilo_id: str) -> None: + transport = AIOHTTPTransport( + url="https://platform.hiloenergie.com/api/digital-twin/v3/graphql", + headers={"Authorization": f"Bearer {self.access_token}"}, + ) + client = Client(transport=transport, fetch_schema_from_transport=True) + query = gql(self.QUERY_GET_LOCATION) + + async with client as session: + result = await session.execute( + query, variable_values={"locationHiloId": location_hilo_id} + ) + self._handle_query_result(result) + + async def _subscribe_to_device_updated(self, location_hilo_id: str) -> None: + transport = WebsocketsTransport( + url=f"wss://platform.hiloenergie.com/api/digital-twin/v3/graphql?access_token={self.access_token}" + ) + client = Client(transport=transport, fetch_schema_from_transport=True) + query = gql(self.SUBSCRIPTION_DEVICE_UPDATED) + print("Connected to device updated subscription") + try: + async with client as session: + async for result in session.subscribe( + query, variable_values={"locationHiloId": location_hilo_id} + ): + self._handle_subscription_result(result) + except asyncio.CancelledError: + print("Subscription cancelled.") + asyncio.sleep(1) + await self._subscribe_to_device_updated(location_hilo_id) + + def _handle_query_result(self, result: Dict[str, Any]) -> None: + devices_values: list[any] = result["getLocation"]["devices"] + attributes = self.mapper.map_query_values(devices_values) + self._devices.parse_values_received(attributes) + + def _handle_subscription_result(self, result: Dict[str, Any]) -> None: + devices_values: list[any] = result["onAnyDeviceUpdated"]["device"] + attributes = self.mapper.map_subscription_values(devices_values) + self._devices.parse_values_received(attributes)