Skip to content

24V thermostats: mode, fan mode, humidity and cooling setpoint are received but never surfaced #998

Open

Description

@ColtHub

Summary

For 24 V thermostats (LowVoltageThermostat, e.g. a Honeywell T10 enrolled in Hilo), the climate entity is permanently stuck on hvac_modes: ['heat'] with the heating setpoint, even while the system is actively cooling. Mode, fan mode, ambient humidity and the cooling setpoint are all received from the Hilo API and stored as readings — they are simply never surfaced.

I own this hardware and can test and support the feature, which I understand has been the blocker on similar requests (#824, #473).

Environment

  • Home Assistant Core 2026.7.4 (HAOS 18.2)
  • hilo 2026.8.1 (HACS)
  • python-hilo 2026.3.5
  • Device: Honeywell T10, reported by Hilo as a low voltage thermostat (heat pump with auxiliary heat)

What happens

The Hilo app itself offers Auto / Heat / Emergency Heat / Cool / Off, three fan modes and ambient humidity. Home Assistant shows:

climate.thermostat_central
  state              : heat        # hardcoded
  hvac_modes         : ['heat']
  temperature        : 21          # heating setpoint, while cooling to 24
  current_temperature: 24
  supported_features : 1

Root cause

The data does arrive. Dumping device.readings for this thermostat gives:

['CoolTemperatureSet', 'CurrentState', 'CurrentTemperature', 'Disconnected', 'FanMode',
 'FanSpeed', 'GdState', 'Heating', 'Humidity', 'MaxCoolSetpoint', 'MaxTempSetpoint',
 'MinCoolSetpoint', 'MinTempSetpoint', 'Power', 'TargetTemperature',
 'Thermostat24VAllowedFanMode', 'Thermostat24VAllowedMode', 'Thermostat24VMode',
 'ThermostatAllowedModes', 'ThermostatMode', 'Unpaired', 'Version', 'ZigbeeVersion']

Two independent problems:

1. climate.py hardcodes heating. _attr_hvac_modes = [HVACMode.HEAT], hvac_mode returns HVACMode.HEAT, and set_hvac_mode() is an empty return.

2. Those attributes cannot be read back through get_value(). The 24 V attribute names are built by graphql_value_mapper but are absent from the static registry in pyhilo/const.py (only Humidity is present there). HiloDevice.get_value() resolves names through API.dev_atts(), which — for an unregistered name and with no value_type — returns the plain string instead of a DeviceAttribute. _get_attribute() then compares that string against reading.device_attribute objects, never matches, and returns None. The value is lost silently, with no warning.

Readings themselves are fine, because build_attribute() passes valueType, which makes dev_atts() take its fallback branch and construct a proper DeviceAttribute.

Fix and result

Reading the values directly off device.readings (matching on reading.device_attribute.hilo_attribute) and deriving the modes from Thermostat24VAllowedMode gives, on the same device, with no library change:

state              : cool
hvac_modes         : ['heat', 'off', 'cool', 'heat_cool']
fan_mode           : ON
fan_modes          : ['ON', 'AUTO', 'CIRCULATE']
current_humidity   : 53
temperature        : 24          # cooling setpoint while in cool mode
supported_features : 393

Everything is gated on the 24 V readings being present, so baseboard thermostats keep the exact previous behaviour (['heat'], TARGET_TEMPERATURE only).

I would be happy to open a PR with this if you are open to it.

Related: writes are refused on these devices

While testing the setters I found that Hilo advertises:

settable_attributes = ['Disconnected']

so HiloDevice._set_attribute() drops every command — mode, fan and temperature. I believe this is the actual cause of #937 (temperature changes working in the Hilo app but not from Home Assistant on a Sinopé 24 V), and it cannot be fixed from climate.py. I have posted the details there.

In the meantime my setters raise an explicit HomeAssistantError rather than failing silently, so the UI does not offer a control that quietly does nothing.

Activity

  1. ic-dev21 commented on Aug 2, 2026

    @ic-dev21
    Collaborator

    Duplicate of #937, I do not own any devices to test code out on this so fixing it will require outside help else I'm going in totally blind.

    24V thermostats expect a batch attribute set, no a single attribute. They need to receive setpoints for both heating and cooling, even if one or the other hasn't changed. This is the root of your issue.

  2. ic-dev21 commented on Aug 2, 2026

    @ic-dev21
    Collaborator

    @ColtHub
    Your "PUT" command must be sent to this endpoint:
    Automation/v1/api/Locations/{location_id}/Devices/BatchAttributes

    Payload should be a string dict mapping to the device's attributes list, i.e:
    {"456": {"Hue": 120.0, "Saturation": 80.0}}

    min_temp setpoint and max_temp_setpoint must be sent together.

    It's not much, but it's all I know.

  3. ColtHub commented on Aug 2, 2026

    @ColtHub
    Author

    (je passe au français, j'avais ouvert en anglais par réflexe — dis-moi si tu préfères qu'on garde l'anglais pour le tracker)

    Merci pour l'indication sur l'envoi groupé : elle était juste et c'est elle qui m'a débloqué. J'ai testé sur mon T10, et il y a un deuxième obstacle que je n'avais pas isolé.

    Ce que j'ai mesuré

    PUT Locations/{loc}/Devices/{id}/Attributes avec le lot complet :

    {"Thermostat24VMode": "HEAT", "TargetTemperature": 20.0, "CoolTemperatureSet": 22}

    Réponse :

    [{"operationId": "6078ab26-...", "status": 1, "statusReason": 0, "message": null}]

    L'API accepte et applique. Vérifié de bout en bout, trois fois :

    Test Résultat
    Consigne 21 → 20 → 21 relue par HA et affichée dans l'app Resideo
    Mode HEAT → COOL HA et Resideo suivent
    Ventilateur ON → AUTO HA et Resideo suivent

    La relecture est une vraie confirmation et non un affichage optimiste : mon code
    court-circuite set_attribute(), donc pyhilo ne met à jour aucun état local — la valeur
    revient bien de Hilo par le flux de lecture.

    Le blocage principal est côté client

    C'est le point que je n'avais pas vu et qui change le diagnostic de #937 :

    supported_attributes = [..., 'TargetTemperature', 'Thermostat24VMode', 'CoolTemperatureSet', ...]
    settable_attributes  = ['Disconnected']
    

    Hilo ne déclare rien de modifiable sur ces thermostats, alors que l'API accepte
    parfaitement l'écriture. Or HiloDevice._set_attribute() ne fait rien quand l'attribut est
    absent de settable_attributes — donc la requête n'est jamais émise, quel que soit son
    format. C'est ce qui produit le Invalid attribute DeviceAttribute(hilo_attribute='TargetTemperature', ...) for device rapporté dans #937.

    Autrement dit, les deux obstacles s'empilent : l'envoi groupé est nécessaire, comme tu le
    dis, mais tant que le garde-fou refuse, aucun envoi — groupé ou non — ne part.

    Au passage, le vocabulaire réel des modes est
    ['EMERGENCY_HEAT', 'HEAT', 'OFF', 'COOL', 'AUTO'].

    Ce que je peux proposer

    J'ai chez moi un climate.py complet qui fait lecture et écriture sur le T10, avec
    non-régression vérifiée sur mes 6 thermostats de plinthe (ils restent en ['heat'],
    supported_features: 1, sans ventilateur). Il tourne en production.

    Deux réserves avant d'ouvrir une PR, et j'aimerais ton avis :

    1. Mon _batch_put() appelle api.async_request() et api._get_url(), donc des méthodes
      privées de python-hilo. Ça convient à un correctif local, pas à une contribution propre.
      Préfères-tu que le lot soit exposé par la bibliothèque — par exemple un
      HiloDevice.set_attributes(dict) — et que l'intégration s'y branche ?
    2. Faut-il contourner settable_attributes pour ces appareils, ou plutôt corriger la façon
      dont la liste est peuplée ? Je n'ai pas assez de recul sur les autres types d'appareils
      pour trancher sans risquer une régression ailleurs.

    Dis-moi la forme qui te convient et je la prépare. Je peux tester chaque itération sur du
    vrai matériel, donc tu n'as pas à avancer à l'aveugle sur cette partie-là.

  4. ic-dev21 commented on Aug 2, 2026

    @ic-dev21
    Collaborator

    (je passe au français, j'avais ouvert en anglais par réflexe — dis-moi si tu préfères qu'on garde l'anglais pour le tracker)

    Merci pour l'indication sur l'envoi groupé : elle était juste et c'est elle qui m'a débloqué. J'ai testé sur mon T10, et il y a un deuxième obstacle que je n'avais pas isolé.

    Ce que j'ai mesuré

    PUT Locations/{loc}/Devices/{id}/Attributes avec le lot complet :

    {"Thermostat24VMode": "HEAT", "TargetTemperature": 20.0, "CoolTemperatureSet": 22}
    Réponse :

    [{"operationId": "6078ab26-...", "status": 1, "statusReason": 0, "message": null}]
    L'API accepte et applique. Vérifié de bout en bout, trois fois :

    Test Résultat
    Consigne 21 → 20 → 21 relue par HA et affichée dans l'app Resideo
    Mode HEAT → COOL HA et Resideo suivent
    Ventilateur ON → AUTO HA et Resideo suivent
    La relecture est une vraie confirmation et non un affichage optimiste : mon code court-circuite set_attribute(), donc pyhilo ne met à jour aucun état local — la valeur revient bien de Hilo par le flux de lecture.

    Le blocage principal est côté client

    C'est le point que je n'avais pas vu et qui change le diagnostic de #937 :

    supported_attributes = [..., 'TargetTemperature', 'Thermostat24VMode', 'CoolTemperatureSet', ...]
    settable_attributes  = ['Disconnected']
    

    Hilo ne déclare rien de modifiable sur ces thermostats, alors que l'API accepte parfaitement l'écriture. Or HiloDevice._set_attribute() ne fait rien quand l'attribut est absent de settable_attributes — donc la requête n'est jamais émise, quel que soit son format. C'est ce qui produit le Invalid attribute DeviceAttribute(hilo_attribute='TargetTemperature', ...) for device rapporté dans #937.

    Autrement dit, les deux obstacles s'empilent : l'envoi groupé est nécessaire, comme tu le dis, mais tant que le garde-fou refuse, aucun envoi — groupé ou non — ne part.

    Au passage, le vocabulaire réel des modes est ['EMERGENCY_HEAT', 'HEAT', 'OFF', 'COOL', 'AUTO'].

    Ce que je peux proposer

    J'ai chez moi un climate.py complet qui fait lecture et écriture sur le T10, avec non-régression vérifiée sur mes 6 thermostats de plinthe (ils restent en ['heat'], supported_features: 1, sans ventilateur). Il tourne en production.

    Deux réserves avant d'ouvrir une PR, et j'aimerais ton avis :

    1. Mon _batch_put() appelle api.async_request() et api._get_url(), donc des méthodes
      privées de python-hilo. Ça convient à un correctif local, pas à une contribution propre.
      Préfères-tu que le lot soit exposé par la bibliothèque — par exemple un
      HiloDevice.set_attributes(dict) — et que l'intégration s'y branche ?
    2. Faut-il contourner settable_attributes pour ces appareils, ou plutôt corriger la façon
      dont la liste est peuplée ? Je n'ai pas assez de recul sur les autres types d'appareils
      pour trancher sans risquer une régression ailleurs.

    Dis-moi la forme qui te convient et je la prépare. Je peux tester chaque itération sur du vrai matériel, donc tu n'as pas à avancer à l'aveugle sur cette partie-là.

    Salut, aucune préférence de mon côté pour la langue, vas-y comme tu le sens.

    J'ai des contacts dans la communauté ainsi que chez HQ qui peuvent aussi beta tester au besoin, je vais devoir m'en remettre à eux pour ce bout-là.

    Pour ce qui est de tes questions. Je pense qu'une version "propre" est préférable sur le long terme, tu n'est certainement pas le seul utilisateur qui a ces thermostats comme en témoigne l'autre issue. Je peux merger et build le release mais je vais avoir besoin de support externe pour les tests.

    1. Faudrait voir si settable attributes peut être nesté dans un if selon le devicetype. Ça fait plutôt longtemps que j'ai joué dans la portion websocket et de mon testing perso je ne vois que les attributs des thermostats Hilo pour des raisons évidentes.

    Si jamais tu veux sync up ou jaser de vive voix la communauté est pas mal active sur discord quand même. Full disclaimer, je ne suis pas programmeur donc des fois je comprends vite quand on m'explique longtemps.

    Toujours plaisant de voir la communauté open source s'entraider comme ça, merci d'avance!

  5. aboredman commented on Aug 6, 2026

    @aboredman

    J'ai un problème identique avec un Honeywell T6 et peut aider pour les tests au besoin

  6. ColtHub commented on Aug 13, 2026

    @ColtHub
    Author

    Les deux PR sont ouvertes :

    • dvd-dev/python-hilo#441 — la bibliothèque. Prête, CI vert (le job Tests y exécute vraiment la suite : 97 tests).
    • dvd-dev/hilo#1001 — l'intégration. En brouillon, elle dépend de la première.

    J'ai suivi ta préférence pour la version propre : plus aucune méthode privée de python-hilo n'est appelée depuis l'intégration. L'envoi groupé est exposé par la bibliothèque sous HiloDevice.set_attributes(dict), et climate.py ne fait plus que traduire entre les propriétés de Climate et le modèle Home Assistant — aucun nom d'attribut Hilo ne descend dans l'intégration.

    Sur ta suggestion pour settable_attributes — j'ai divergé, dis-moi si j'ai eu tort

    Tu proposais de conditionner settable_attributes selon le devicetype. J'ai pris un autre chemin et je préfère te le dire franchement.

    set_attributes() est une méthode distincte qui ne consulte pas settable_attributes du tout. _set_attribute() et son garde-fou ne sont pas touchés d'une ligne. Mon raisonnement : tu as dit devoir avancer à l'aveugle sur le matériel que tu ne possèdes pas, et toucher au garde-fou aurait mis en jeu tous les types d'appareils — lampes, interrupteurs, chauffe-eau. Là, le pire cas d'une erreur de ma part reste confiné aux thermostats 24 V.

    L'inconvénient, c'est que la cause racine reste : Hilo continue d'annoncer settable_attributes = ['Disconnected'] à tort. On contourne au lieu de corriger. Si tu préfères ta version, je la prépare — mais il faudrait alors quelqu'un pour valider qu'aucun autre type d'appareil ne régresse.

    Note aussi que j'ai utilisé PUT Locations/{loc}/Devices/{id}/Attributes, l'endpoint que j'avais mesuré comme fonctionnel, et non le BatchAttributes que tu mentionnais. Si le second est la voie officielle, je bascule volontiers.

    Validé sur matériel réel

    Honeywell T10, à côté de six thermostats de plinthe sur la même installation.

    Test Résultat
    Mode, ventilateur, humidité, consigne et bornes de froid en lecture valeurs correctes
    Consigne de froid 23 → 22 → 23 relue en 38 s
    Ventilateur ON → AUTO → ON relu en 25 s
    Les consignes survivent à un changement de ventilateur seul oui
    Mode cool → heat → cool relu en 27 s
    Les six plinthes inchangées

    Les relectures sont de vraies confirmations : set_attributes() ne met à jour aucun état local, donc les valeurs reviennent bien de Hilo.

    Un détail qui valait la peine d'être trouvé : ton appareil annonce ses modes dans l'ordre ['EMERGENCY_HEAT', 'HEAT', 'OFF', 'COOL', 'AUTO'], et EMERGENCY_HEAT comme HEAT correspondent tous deux à HVACMode.HEAT. Prendre le premier de la liste qui correspond écrivait donc EMERGENCY_HEAT pour une simple demande de chauffage — sur une thermopompe, c'est la résistance d'appoint qui s'enclenche, à plusieurs fois le coût, sans rien d'anormal à l'écran. La sélection privilégie maintenant la correspondance exacte. Il y a un test de régression dessus.

    Un défaut de l'API que je te signale au passage

    Thermostat24VAllowedMode annonce AUTO, et ton application l'offre dans son interface. Mon T10 ne le tient pas : le mode retombe sur Cool en quelques secondes, y compris quand je le sélectionne depuis l'application Hilo — donc sans mon code dans la boucle. Home Assistant n'a jamais eu le temps de le lire.

    Le vocabulaire annoncé n'est donc pas une liste de capacités fiable. Je n'ai rien câblé en dur : un seul appareil est une base trop mince. La question est dans la PR — faut-il filtrer AUTO ? Tu sauras si c'est propre au T10, à la passerelle, ou commun à tous les 24 V.

    Pour les tests

    @aboredman a un Honeywell T6 avec le même problème et propose d'aider — ça tombe bien, c'est un second modèle et tu disais avoir besoin de support externe. Le T6 devrait exposer les mêmes attributs ; s'il annonce un vocabulaire de modes différent du mien, c'est exactement ce qu'il faut savoir avant de fusionner.

    De mon côté je fais tourner ces branches en production sur ma box et je peux valider chaque itération sur du vrai matériel.

    Limites déclarées

    • hvac_action affiche idle pendant que l'appareil climatise. Il est dérivé de la seule lecture heating. L'attribut current_state porterait la vraie réponse, mais je ne l'ai jamais observé qu'à OFF — je ne voulais pas inventer une correspondance sur un seul échantillon.
    • heat_cool n'est pas testable pour la raison ci-dessus.

    Prends le temps qu'il te faut, et dis-moi si tu veux que je change quoi que ce soit dans la forme.

  7. ic-dev21 commented on Aug 13, 2026

    @ic-dev21
    Collaborator

    Je vais m'en remettre à l'expertise de @nlz242 qui a une meilleure connaissance du backend Hilo et qui aura les vraies réponses à tes questions.

  8. ColtHub commented on Sep 9, 2026

    @ColtHub
    Author

    Est-ce que le dossier a bougé depuis?

  9. ic-dev21 commented on Sep 9, 2026

    @ic-dev21
    Collaborator

    Est-ce que le dossier a bougé depuis?

    J’attends des news, mais dès que c’est testé avec les divers trucs 24v (pas juste les tiens) ça sera merge et release

  10. etiennnr commented on Oct 24, 2026

    @etiennnr

    PVI, même problème avec mon Sinopé TH6250WF. Je suis prêt à aider avec les tests au besoin 😄

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions