Skip to content

Endpoint /devices obselète #564

Closed
Closed

Description

@AbigailAsselin-Hilo

Helloo

Context

La route /api/Locations/{LocationId}/Devices est obselete. Toute l'information des appareils passe par le DeviceHub, avec un SubscribeToLocation

Information initiales de la liste d'appareil d'une location

DeviceListInitialValuesReceived

Un array des appareils de la location.
L'objet devrait avoir le même format que celui dans le endpoint qui est obselete

[
  {
    "id": 0,
    "assetId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "hiloId": "string",
    "identifier": "string",
    "gatewayId": 0,
    "gatewayExternalId": "string",
    "gatewayAssetId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "string",
    "type": "Hub",
    "groupId": 0,
    "category": "Heating",
    "icon": "string",
    "loadConnected": 0,
    "modelNumber": "string",
    "locationId": 0,
    "parameters": "string",
    "externalGroup": "string",
    "provider": 1,
    "providerData": "string",
    "isFavorite": true,
    "eTag": "string",
    "supportedAttributesList": [
      "None"
    ],
    "settableAttributesList": [
      "None"
    ],
    "supportedParametersList": [
      "None"
    ]
  }
]

Pour les mises à jour de la liste des appareils (Ajout et supression)

DeviceAdded

Les informations du device qui à été ajouté (même objet que la liste ci-haut)

  {
    "id": 0,
    "assetId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "hiloId": "string",
    "identifier": "string",
    "gatewayId": 0,
    "gatewayExternalId": "string",
    "gatewayAssetId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "string",
    "type": "Hub",
    "groupId": 0,
    "category": "Heating",
    "icon": "string",
    "loadConnected": 0,
    "modelNumber": "string",
    "locationId": 0,
    "parameters": "string",
    "externalGroup": "string",
    "provider": 1,
    "providerData": "string",
    "isFavorite": true,
    "eTag": "string",
    "supportedAttributesList": [
      "None"
    ],
    "settableAttributesList": [
      "None"
    ],
    "supportedParametersList": [
      "None"
    ]
  }

DeviceDeleted

Les ids (deviceId et HiloId) du device qui a été supprimé.

{
  "id": 0,
  "hiloId": "string"
}

Information initiale et mise à jour des valeurs des appareils

DevicesValuesReceived ET GatewayValuesReceived

Les valeurs des appareils ET du gateway, ils sont séparés dans 2 messages mais le format est le même

[
  {
    "deviceId": 0,
    "hiloId": "string",
    "locationId": 0,
    "locationHiloId": "string",
    "timeStampUTC": "2025-02-03T19:06:34.202Z",
    "attribute": "None",
    "value": "string",
    "valueType": "OnOff",
    "operationId": "string"
  }
]

Donc :
Lors d'un SubscribeToLocation, on devrait recevoir 3 messages avec les informations nécessaires pour afficher les états/valeurs des appareils:

  • DeviceListInitialValuesReceived
  • DevicesValuesReceived
  • GatewayValuesReceived

Ensuite, le DeviceHub met à jour les informations reçus avec ces messages:

  • DeviceAdded
  • DeviceDeleted
  • DevicesValuesReceived
  • GatewayValuesReceived

DeviceListInitialValuesReceived, DeviceAdded ET DeviceDeleted servent à bartir la liste d'appareils d'une location
DevicesValuesReceived ET GatewayValuesReceived font juste mettre à jour les états/valeurs des ces appareils.

Activity

  1. ic-dev21 commented on Feb 3, 2025

    @ic-dev21
    Collaborator

    Salut, pour info supplémentaire, si quelqu'un avait envie d'y travailler, il y a déjà un callback pour une portion du /devicehub qui est gérée:

    @callback
    async def on_websocket_event(self, event: WebsocketEvent) -> None:
    """Define a callback for receiving a websocket event."""
    async_dispatcher_send(self._hass, DISPATCHER_TOPIC_WEBSOCKET_EVENT, event)
    if event.event_type == "COMPLETE":
    cb = self.invocations.get(event.invocation)
    if cb:
    async_call_later(self._hass, 3, cb(event.invocation))
    elif event.target == "Heartbeat":
    self.validate_heartbeat(event)
    elif event.target == "DevicesValuesReceived":
    # When receiving attribute values for unknown devices, assume
    # we have refresh the device list.
    new_devices = any(
    self.devices.find_device(item["deviceId"]) is None
    for item in event.arguments[0]
    )
    if new_devices:
    LOG.warning(
    "Device list appears to be desynchronized, forcing a refresh thru the API..."
    )
    await self.devices.update()
    updated_devices = self.devices.parse_values_received(event.arguments[0])
    # NOTE(dvd): If we don't do this, we need to wait until the coordinator
    # runs (scan_interval) to have updated data in the dashboard.
    for device in updated_devices:
    async_dispatcher_send(
    self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
    )
    elif event.target == "DeviceListInitialValuesReceived":
    # This websocket event only happens after calling SubscribeToLocation.
    # This triggers an update without throwing an exception
    new_devices = await self.devices.update_devicelist_from_signalr(
    event.arguments[0]
    )
    elif event.target == "DeviceListUpdatedValuesReceived":
    # This message only contains display information, such as the Device's name (as set in the app), it's groupid, icon, etc.
    # Updating the device name causes issues in the integration, it detects it as a new device and creates a new entity.
    # Ignore this call, for now... (update_devicelist_from_signalr does work, but causes the issue above)
    # await self.devices.update_devicelist_from_signalr(event.arguments[0])
    LOG.debug(
    "Received 'DeviceListUpdatedValuesReceived' message, not implemented yet."
    )
    elif event.target == "DevicesListChanged":
    # This message only contains the location_id and is used to inform us that devices have been removed from the location.
    # Device deletion is not implemented yet, so we just log the message for now.
    LOG.debug("Received 'DevicesListChanged' message, not implemented yet.")
    elif event.target == "DeviceAdded":
    # Same structure as DeviceList* but only one device instead of a list
    devices = []
    devices.append(event.arguments[0])
    new_devices = await self.devices.update_devicelist_from_signalr(devices)
    elif event.target == "DeviceDeleted":
    # Device deletion is not implemented yet, so we just log the message for now.
    LOG.debug("Received 'DeviceDeleted' message, not implemented yet.")
    elif event.target == "GatewayValuesReceived":
    # Gateway deviceId hardcoded to 1 as it is not returned by Gateways/Info.
    # First time we encounter a GatewayValueReceived event, update device with proper deviceid.
    gateway = self.devices.find_device(1)
    if gateway:
    gateway.id = event.arguments[0][0]["deviceId"]
    LOG.debug(f"Updated Gateway's deviceId from default 1 to {gateway.id}")
    updated_devices = self.devices.parse_values_received(event.arguments[0])
    # NOTE(dvd): If we don't do this, we need to wait until the coordinator
    # runs (scan_interval) to have updated data in the dashboard.
    for device in updated_devices:
    async_dispatcher_send(
    self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
    )
    else:
    LOG.warning(f"Unhandled websocket event: {event}")

    Dans la version avec challenge sensor via signalR/websocket j'ai séparé le /devicehub du /challengehub, c'est un work in progress donc prière de pas trop rire de moi:

    def register_websocket_listener(self, listener):
    """Register a listener for websocket events."""
    LOG.debug(f"Registering websocket listener: {listener.__class__.__name__}")
    self._websocket_listeners.append(listener)
    async def _handle_websocket_message(self, event):
    """Process websocket messages and notify listeners."""
    LOG.debug(f"Received websocket message type: {event}")
    target = event.target
    LOG.debug(f"handle_websocket_message_target {target}")
    msg_data = event
    LOG.debug(f"handle_websocket_message_ msg_data {msg_data}")
    if target == "ChallengeListInitialValuesReceived":
    msg_type = "challenge_list_initial"
    elif target == "ChallengeAdded":
    msg_type = "challenge_added"
    elif target == "ChallengeDetailsUpdated":
    msg_type = "challenge_details_update"
    elif target == "ChallengeConsumptionUpdatedValuesReceived":
    msg_type = "challenge_details_update"
    elif target == "ChallengeDetailsUpdatedValuesReceived":
    msg_type = "challenge_details_update"
    elif target == "ChallengeDetailsInitialValuesReceived":
    msg_type = "challenge_details_update"
    elif target == "ChallengeListUpdatedValuesReceived":
    msg_type = "challenge_details_update"
    # ic-dev21 Notify listeners
    for listener in self._websocket_listeners:
    handler_name = f"handle_{msg_type}"
    if hasattr(listener, handler_name):
    handler = getattr(listener, handler_name)
    try:
    # ic-dev21 Extract the arguments from the WebsocketEvent object
    if isinstance(msg_data, WebsocketEvent):
    arguments = msg_data.arguments
    if arguments: # ic-dev21 check if there are arguments
    await handler(arguments[0])
    else:
    LOG.warning(f"Received empty arguments for {msg_type}")
    else:
    await handler(msg_data)
    except Exception as e:
    LOG.error(f"Error in websocket handler {handler_name}: {e}")
    async def _handle_challenge_events(self, event: WebsocketEvent) -> None:
    """Handle all challenge-related websocket events."""
    if event.target == "ChallengeDetailsInitialValuesRecei0ved":
    challenge = event.arguments[0]
    LOG.debug(f"ChallengeDetailsInitialValuesReceived, challenge = {challenge}")
    self.challenge_id = challenge.get("id")
    elif event.target == "ChallengeDetailsUpdatedValuesReceived":
    LOG.debug("ChallengeDetailsUpdatedValuesReceived")
    elif event.target == "ChallengeListUpdatedValuesReceived":
    LOG.debug("ChallengeListUpdatedValuesReceived")
    self.challenge_phase = event.arguments[0][0]["currentPhase"]
    elif event.target == "ChallengeAdded":
    LOG.debug("ChallengeAdded")
    challenge = event.arguments[0]
    self.challenge_id = challenge.get("id")
    await self.subscribe_to_challenge(1, self.challenge_id)
    elif event.target == "ChallengeListInitialValuesReceived":
    LOG.debug("ChallengeListInitialValuesReceived")
    challenges = event.arguments[0]
    for challenge in challenges:
    challenge_id = challenge.get("id")
    self.challenge_phase = challenge.get("currentPhase")
    self.challenge_id = challenge.get("id")
    await self.subscribe_to_challenge(1, challenge_id)
    async def _handle_device_events(self, event: WebsocketEvent) -> None:
    """Handle all device-related websocket events."""
    if event.target == "DevicesValuesReceived":
    new_devices = any(
    self.devices.find_device(item["deviceId"]) is None
    for item in event.arguments[0]
    )
    if new_devices:
    LOG.warning(
    "Device list appears to be desynchronized, forcing a refresh thru the API..."
    )
    await self.devices.update()
    updated_devices = self.devices.parse_values_received(event.arguments[0])
    for device in updated_devices:
    async_dispatcher_send(
    self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
    )
    elif event.target == "DeviceListInitialValuesReceived":
    await self.devices.update_devicelist_from_signalr(event.arguments[0])
    elif event.target == "DeviceListUpdatedValuesReceived":
    LOG.debug(
    "Received 'DeviceListUpdatedValuesReceived' message, not implemented yet."
    )
    elif event.target == "DevicesListChanged":
    LOG.debug("Received 'DevicesListChanged' message, not implemented yet.")
    elif event.target == "DeviceAdded":
    devices = [event.arguments[0]]
    await self.devices.update_devicelist_from_signalr(devices)
    elif event.target == "DeviceDeleted":
    LOG.debug("Received 'DeviceDeleted' message, not implemented yet.")
    elif event.target == "GatewayValuesReceived":
    gateway = self.devices.find_device(1)
    if gateway:
    gateway.id = event.arguments[0][0]["deviceId"]
    LOG.debug(f"Updated Gateway's deviceId from default 1 to {gateway.id}")
    updated_devices = self.devices.parse_values_received(event.arguments[0])
    for device in updated_devices:
    async_dispatcher_send(
    self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
    )
    @callback
    async def on_websocket_event(self, event: WebsocketEvent) -> None:
    """Define a callback for receiving a websocket event."""
    async_dispatcher_send(self._hass, DISPATCHER_TOPIC_WEBSOCKET_EVENT, event)
    if event.event_type == "COMPLETE":
    cb = self.invocations.get(event.invocation)
    if cb:
    async_call_later(self._hass, 3, cb(event.invocation))
    elif event.target == "Heartbeat":
    self.validate_heartbeat(event)
    elif "Challenge" in event.target:
    await self._handle_challenge_events(event)
    await self._handle_websocket_message(event)
    elif "Device" in event.target or event.target == "GatewayValuesReceived":
    await self._handle_device_events(event)
    else:
    LOG.warning(f"Unhandled websocket event: {event}")

  2. AbigailAsselin-Hilo commented on Feb 20, 2025

    @AbigailAsselin-Hilo
    ContributorAuthor

    Salut @ic-dev21, j'ai une petite question sur la façon que l'intégration Hilo obtient le locationId de l'utilsateur pour faire les appels. Vous utilisez bien le GetLocations pour avoir le locationId? J'ai rechercher dans le code mais j'ai pas trouvé.

    Merci

  3. fersingb commented on Feb 20, 2025

    @fersingb
    Collaborator

    Bonjour @AbigailAsselin-Hilo,

    Après un check rapide dans le code, un appel semble être fait vers https://api.hiloenergie.com/Automation/v1/api/Locations

    Bonne journée

  4. ic-dev21 commented on Feb 20, 2025

    @ic-dev21
    Collaborator

    Salut @ic-dev21, j'ai une petite question sur la façon que l'intégration Hilo obtient le locationId de l'utilsateur pour faire les appels. Vous utilisez bien le GetLocations pour avoir le locationId? J'ai rechercher dans le code mais j'ai pas trouvé.

    Merci

    https://github.com/dvd-dev/python-hilo/blob/cd9f9b0fdf3a41b8713379fd18154e7dca47bf58/pyhilo/api.py#L497-L501

  5. ic-dev21 commented on Mar 1, 2025

    @ic-dev21
    Collaborator

    Salut @AbigailAsselin-Hilo, si ça peut t'aider et en aider d'autres, ma compréhension du flow, ce n'est pas mon code mais je suis relativement confiant que je suis dedans.

    Pour la connexion actuellement, on passe par la fonction suivante:

    https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/api.py#L495-L499

        async def get_location_id(self) -> int:
            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"])
    

    URL utilisé:
    https://api.hiloenergie.com/Automation/v1/api/Locations/

    Les contantes pour le bâtir sont ici:
    https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/const.py#L26-L34

    # API constants
    API_HOSTNAME: Final = "api.hiloenergie.com"
    API_END: Final = "v1/api"
    API_AUTOMATION_ENDPOINT: Final = f"/Automation/{API_END}"
    API_CHALLENGE_ENDPOINT: Final = f"/challenge/{API_END}"
    API_GD_SERVICE_ENDPOINT: Final = f"/GDService/{API_END}"
    API_NOTIFICATIONS_ENDPOINT: Final = "/Notifications"
    API_EVENTS_ENDPOINT: Final = "/Notifications"
    API_REGISTRATION_ENDPOINT: Final = f"{API_NOTIFICATIONS_ENDPOINT}/Registrations"
    

    Une fois le LocationId obtenu, on fait un POST pour obtenir le wss URL et notre accesstoken, on save en YAML:
    https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/websocket.py#L458-L488

        async def _negotiate(self, config: WebsocketConfig) -> Tuple[str, str]:
            """Negotiate websocket connection and get URL and token.
            Args:
                config: The websocket configuration to negotiate
            Returns:
                Tuple containing the websocket URL and access token
            """
            LOG.debug(f"Getting websocket url for {config.endpoint}")
            url = f"{config.endpoint}/negotiate"
            LOG.debug(f"Negotiate URL is {url}")
    
    
            resp = await self.async_request("post", url)
            ws_url = resp.get("url")
            ws_token = resp.get("accessToken")
    
    
            # Save state
            state_key = (
                "websocketDevices"
                if config.endpoint == AUTOMATION_DEVICEHUB_ENDPOINT
                else "websocketChallenges"
            )
            await self._set_state(
                self._state_yaml,
                state_key,
                {
                    "url": ws_url,
                    "token": ws_token,
                },
            )
    
    
            return ws_url, ws_token
    

    On peut maintenant. bâtir le wss URL complet:
    https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/websocket.py#L490-L529

    Une fois qu'on l'a, pour le /devicehub, on fait un subscribetolocation:

    @callback
    async def subscribe_to_location(self, inv_id: int) -> None:
    """Sends the json payload to receive updates from the location."""
    LOG.debug(f"Subscribing to location {self.devices.location_id}")
    await self._api.websocket_devices.async_invoke(
    [self.devices.location_id], "SubscribeToLocation", inv_id
    )

    Les reading sont traitées ici quand ils rentrent:

    async def _handle_device_events(self, event: WebsocketEvent) -> None:
    """Handle all device-related websocket events."""
    if event.target == "DevicesValuesReceived":
    new_devices = any(
    self.devices.find_device(item["deviceId"]) is None
    for item in event.arguments[0]
    )
    if new_devices:
    LOG.warning(
    "Device list appears to be desynchronized, forcing a refresh thru the API..."
    )
    await self.devices.update()
    updated_devices = self.devices.parse_values_received(event.arguments[0])
    for device in updated_devices:
    async_dispatcher_send(
    self._hass, SIGNAL_UPDATE_ENTITY.format(device.id)
    )

    Et sont parsées ici:
    https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/websocket.py#L224-L240

    Ma compréhension est possiblement pas parfaite, mais voilà.

  6. AbigailAsselin-Hilo commented on Mar 2, 2025

    @AbigailAsselin-Hilo
    ContributorAuthor

    Ouais ça semble faire du sens! Juste pour le parse je crois que c'est lui ici https://github.com/dvd-dev/python-hilo/blob/60e529797369ff402ad0c312a7bf8f65ca5351ad/pyhilo/devices.py#L38 par contre

  7. ic-dev21 commented on Apr 9, 2025

    @ic-dev21
    Collaborator

    Closed by v2025.4.1

  8. SimonCoulombe commented on May 20, 2025

    @SimonCoulombe

    Salut! Merci encore de maintenir hilo, je viens de réaliser que ça faisait 2 semaines que j'avais pas de data d'énergie, maintenant je sais pourquoi.

    Question de noob, je fais comment pour faire une à jour?

    J'ai fait "redownload" dans HACS:

    Image

    dans les device and entities ça m'a dit do you want to configure hilo, j'ai dit oui, là il se plaint que je peux juste avoir une configuration..

    Image

    il y a sûrement un bouton simple qqpart qui m'échappe

    mon setup actuel:

    Image

  9. ic-dev21 commented on May 20, 2025

    @ic-dev21
    Collaborator

    Salut! Merci encore de maintenir hilo, je viens de réaliser que ça faisait 2 semaines que j'avais pas de data d'énergie, maintenant je sais pourquoi.

    Question de noob, je fais comment pour faire une à jour?

    J'ai fait "redownload" dans HACS:

    Image

    dans les device and entities ça m'a dit do you want to configure hilo, j'ai dit oui, là il se plaint que je peux juste avoir une configuration..

    Image

    il y a sûrement un bouton simple qqpart qui m'échappe

    mon setup actuel:

    Image

    Normalement tu fais simplement redownload et tu restart HA, rien d'autre à faire. Il est possible que tu doives deleter le fichier hilo_state.yaml sous /config, j'ai changé la librairie YAML car l'ancienne causait des problèmes de fichier corrompus.

  10. SimonCoulombe commented on May 20, 2025

    @SimonCoulombe

    bin oui toi, j'avais pas vu--- mais c'est bien écrit là. Tout est revenu, merci encore.

    Image

  11. added theissue type on Mar 12, 2026
  12. added a commit that references this issue on Mar 16, 2026
  13. ic-dev21 commented on Mar 16, 2026

    @ic-dev21
    Collaborator

    Fixed by #897 pour de vrai, ce coup-là.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    breakingBreaking ChangesdependenciesPull requests that update a dependency file

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions