Problème intégration thermostat Zigbee

Bonjour,
Après détection/intégration sans problème d’un thermostat WIFI acheté chez AliExpress j’ai commandé le même exemplaire mais en Zigbee.
L’appareil est bien détecté par l’intégration Zigbee…mais la seule entité affichée est la maj du firmware !


Le message d’Initialisation dit terminée, mais le logo de recherche continue à tourner
J’ai supprimé l’appareil et installé à nouveau, même constat.

La doc succinct ne propose que l’intégration à Tuya.
Moi qui voulait prendre un exemplaire en Zigbee je suis un peu planté…

Tu as vérifie que ton service était bien compatible avec zha ?
Sur ce site par exemple

Car si tu choisis au pif, tu peux avoir de mauvaises surprises

Ton modèle : Zigbee ID: TS0601 | _TZE204 (BSEED) Apparemment, il ne fonctionne que sous Z2M. Je ne sais pas s’il y a une solution de contournement pour ZHA

Bonjour,
il n’est pas supporter sur ZHA, il y a une demande d’ajout qui a été faite. Tu peux y trouver un quirk pour le faire fonctionner en attendant, mais la batterie ne fonctionne pas.

Je suis un peu stupide ! La version wifi de ce thermostat me convenait parfaitement et je ne me suis posé aucune question en commandant la version Zigbee. Pour moi mot magique, si c’est Zigbee ça doit marcher (et pourtant avec mes modules Z-Wave j’ai souvent bataille sur le même problème).
Au moins ce device est identifié,
Que signifie un Quirk .
Comment l’utiser pour tenter de l’utiliser même si la batterie n’est pas gérée (il peut se connecter en USB).

Je complète mon commentaire. J’ai choisi ce produit car j’ai une contrainte d’installation. Je dois remplacer des anciens thermostats qui fonctionnent à piles. Ces thermostats ne sont pas encastrés dabs des boîtes et surtout aucune arrivée 220v n’est disponible, juste la boucle sèche qui commande le commutateur de puissance. Cela limite considérablement le choix.
Avec la difficulté d’intégration du modèle Zigbee je m’interroge sur l’utilisation du modèle wifi.
J’ai une box Free Delta sauvegardée par onduleur avec un wifi qui couvre parfaitement la maison.
En cas de panne wifi je suppose que le thermostat maintient les valeurs en cours et que la seule conséquence est l’impossibilité de modifier la valeur de consigne pendant la coupure (très rare).
Je sais que wifi et domotique pas terrible, mais je suis un peu coincé.

tu peux utiliser zigbee2mqtt la il est reconnu, ZHA c’est vraiment pas terrible son seul interet son instalation simple

1 « J'aime »

c’est un convertisseur qui permet de faire fonctionner ton appareil en Zigbee.

pour utiliser un quirk, il faut ajouter ces lignes dans ton configuration.yaml:

zha:
  enable_quirks: true
  custom_quirks_path: /config/zha_quirks/

tu crées un dossier zha_quirks dans le dossier /config ( si tu utilises file editor, il est nommé homeassistant/ ).

tu copie le quirk , tout ce code :

import logging
from typing import Optional, Union

import zigpy.types as t
from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TuyaManufCluster,
    TuyaManufClusterAttributes,
    TuyaPowerConfigurationCluster,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaTimePayload,
    TuyaUserInterfaceCluster,
)
from zhaquirks.tuya.mcu import EnchantedDevice
from zigpy.profiles import zha
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
    AnalogOutput,
    Basic,
    Groups,
    OnOff,
    Ota,
    Scenes,
    Time,
)
from zigpy.zcl.clusters.hvac import Thermostat

_LOGGER = logging.getLogger(__name__)

ZWT198_TARGET_TEMP_ATTR = 0x0202  # target room temp (degree)
ZWT198_TEMPERATURE_ATTR = 0x0203  # current room temp (degree/10)
ZWT198_MODE_ATTR = 0x0404  # [0] schedule [1] manual
ZWT198_SYSTEM_MODE_ATTR = 0x0101  # device [0] off [1] on
ZWT198_HEAT_STATE_ATTR = 0x0465  # [0] heating icon off [1] heating icon on
ZWT198_CHILD_LOCK_ATTR = 0x0109  # [0] unlocked [1] locked
ZWT198_TEMP_CALIBRATION_ATTR = 0x0213  # temperature calibration (degree)
ZWT198ManufClusterSelf = {}


class CustomTuyaOnOff(LocalDataCluster, OnOff):
    """Custom Tuya OnOff cluster."""

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.thermostat_onoff_bus.add_listener(self)

    # pylint: disable=R0201
    def map_attribute(self, attribute, value):
        """Map standardized attribute value to dict of manufacturer values."""
        return {}

    async def write_attributes(self, attributes, manufacturer=None):
        """Implement writeable attributes."""

        records = self._write_attr_records(attributes)

        if not records:
            return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]

        manufacturer_attrs = {}
        for record in records:
            attr_name = self.attributes[record.attrid].name
            new_attrs = self.map_attribute(attr_name, record.value.value)

            _LOGGER.debug(
                "[0x%04x:%s:0x%04x] Mapping standard %s (0x%04x) "
                "with value %s to custom %s",
                self.endpoint.device.nwk,
                self.endpoint.endpoint_id,
                self.cluster_id,
                attr_name,
                record.attrid,
                repr(record.value.value),
                repr(new_attrs),
            )

            manufacturer_attrs.update(new_attrs)

        if not manufacturer_attrs:
            return [
                [
                    foundation.WriteAttributesStatusRecord(
                        foundation.Status.FAILURE, r.attrid
                    )
                    for r in records
                ]
            ]

        await ZWT198ManufClusterSelf[
            self.endpoint.device.ieee
        ].endpoint.tuya_manufacturer.write_attributes(
            manufacturer_attrs, manufacturer=manufacturer
        )

        return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        if command_id in (0x0000, 0x0001, 0x0002):

            if command_id == 0x0000:
                value = False
            elif command_id == 0x0001:
                value = True
            else:
                attrid = self.attributes_by_name["on_off"].id
                success, _ = await self.read_attributes(
                    (attrid,), manufacturer=manufacturer
                )
                try:
                    value = success[attrid]
                except KeyError:
                    return foundation.Status.FAILURE
                value = not value

            (res,) = await self.write_attributes(
                {"on_off": value},
                manufacturer=manufacturer,
            )
            return [command_id, res[0].status]

        return [command_id, foundation.Status.UNSUP_CLUSTER_COMMAND]


class ZWT198ManufCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of thermostatic valves."""

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        global ZWT198ManufClusterSelf
        ZWT198ManufClusterSelf[self.endpoint.device.ieee] = self

    set_time_offset = 1970
    
    server_commands = {
        0x0000: foundation.ZCLCommandDef(
            "set_data",
            {"param": TuyaManufCluster.Command},
            False,
            is_manufacturer_specific=False,
        ),
        0x0010: foundation.ZCLCommandDef(
            "mcu_version_req",
            {"param": t.uint16_t},
            False,
            is_manufacturer_specific=True,
        ),
        0x0024: foundation.ZCLCommandDef(
            "set_time",
            {"param": TuyaTimePayload},
            False,
            is_manufacturer_specific=False,
        ),
    }    

    attributes = TuyaManufClusterAttributes.attributes.copy()
    attributes.update(
        {
            ZWT198_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
            ZWT198_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
            ZWT198_MODE_ATTR: ("mode", t.uint8_t, True),
            ZWT198_SYSTEM_MODE_ATTR: ("system_mode", t.uint8_t, True),
            ZWT198_HEAT_STATE_ATTR: ("heat_state", t.uint8_t, True),
            ZWT198_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
            ZWT198_TEMP_CALIBRATION_ATTR: ("temperature_calibration", t.int32s, True),
        }
    )

    DIRECT_MAPPED_ATTRS = {
        ZWT198_TEMPERATURE_ATTR: (
            "local_temperature",
            lambda value: value * 10,
        ),
        ZWT198_TARGET_TEMP_ATTR: (
            "occupied_heating_setpoint",
            lambda value: value * 10,
        ),
        ZWT198_TEMP_CALIBRATION_ATTR: (
            "local_temperature_calibration",
            lambda value: value * 10,
        ),
    }

    def _update_attribute(self, attrid, value):
        """Override default _update_attribute."""
        super()._update_attribute(attrid, value)
        #if attrid in self.DIRECT_MAPPED_ATTRS:
        if attrid in self.DIRECT_MAPPED_ATTRS and value < 60000:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                self.DIRECT_MAPPED_ATTRS[attrid][0],
                value
                if self.DIRECT_MAPPED_ATTRS[attrid][1] is None
                else self.DIRECT_MAPPED_ATTRS[attrid][1](value),
            )

        if attrid == ZWT198_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)
            self.endpoint.device.thermostat_onoff_bus.listener_event(
                "child_lock_change", value
            )
        elif attrid == ZWT198_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("mode_change", value)
        elif attrid == ZWT198_TEMP_CALIBRATION_ATTR:
            self.endpoint.device.ZWT198TempCalibration_bus.listener_event(
                "set_value", value / 10
            )
        elif attrid == ZWT198_HEAT_STATE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("state_change", value)
        elif attrid == ZWT198_SYSTEM_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "system_mode_change", value
            )


class ZWT198Thermostat(TuyaThermostatCluster):
    """Thermostat cluster for thermostatic valves."""

    _CONSTANT_ATTRIBUTES = {
        0x0015: 500,    # MIN HEAT SETPOINT
        0x0016: 3100,   # MAX HEAT SETPOINT
        0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only,
    }

    DIRECT_MAPPING_ATTRS = {
        "local_temperature_calibration": (
            ZWT198_TEMP_CALIBRATION_ATTR,
            lambda value: round(value / 10),
        ),
        "occupied_heating_setpoint": (
            ZWT198_TARGET_TEMP_ATTR,
            lambda value: round(value / 10),
        ),
    }

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.thermostat_bus.add_listener(self)

    def map_attribute(self, attribute, value):
        """Map standardized attribute value to dict of manufacturer values."""

        if attribute in self.DIRECT_MAPPING_ATTRS:
            return {
                self.DIRECT_MAPPING_ATTRS[attribute][0]: value
                if self.DIRECT_MAPPING_ATTRS[attribute][1] is None
                else self.DIRECT_MAPPING_ATTRS[attribute][1](value)
            }

        if attribute == "operation_preset":
            if value == 1:
                return {ZWT198_MODE_ATTR: 0}
            if value == 2:
                return {ZWT198_MODE_ATTR: 1}

        if attribute in ("programing_oper_mode", "occupancy"):
            if attribute == "occupancy":
                occupancy = value
                oper_mode = self._attr_cache.get(
                    self.attributes_by_name["programing_oper_mode"].id,
                    self.ProgrammingOperationMode.Simple,
                )
            else:
                occupancy = self._attr_cache.get(
                    self.attributes_by_name["occupancy"].id, self.Occupancy.Occupied
                )
                oper_mode = value
            if occupancy == self.Occupancy.Occupied:
                if oper_mode == self.ProgrammingOperationMode.Schedule_programming_mode:
                    return {ZWT198_MODE_ATTR: 0}
                if oper_mode == self.ProgrammingOperationMode.Simple:
                    return {ZWT198_MODE_ATTR: 1}
                self.error("Unsupported value for ProgrammingOperationMode")
            else:
                self.error("Unsupported value for Occupancy")

        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                mode = 0
            else:
                mode = 1
            return {ZWT198_SYSTEM_MODE_ATTR: mode}

    def mode_change(self, value):
        """Preset Mode change."""
        if value == 0:
            operation_preset = self.Preset.Schedule
            prog_mode = self.ProgrammingOperationMode.Schedule_programming_mode
            occupancy = self.Occupancy.Occupied
        else:
            operation_preset = self.Preset.Manual
            prog_mode = self.ProgrammingOperationMode.Simple
            occupancy = self.Occupancy.Occupied

        self._update_attribute(
            self.attributes_by_name["programing_oper_mode"].id, prog_mode
        )
        self._update_attribute(
            self.attributes_by_name["occupancy"].id, occupancy
        )
        self._update_attribute(
            self.attributes_by_name["operation_preset"].id, operation_preset
        )

    def system_mode_change(self, value):
        """System Mode change."""
        if value == 0:
            mode = self.SystemMode.Off
        else:
            mode = self.SystemMode.Heat
        self._update_attribute(
            self.attributes_by_name["system_mode"].id, mode
        )


class ZWT198UserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""

    _CHILD_LOCK_ATTR = ZWT198_CHILD_LOCK_ATTR


class ZWT198ChildLock(CustomTuyaOnOff):
    """On/Off cluster for the child lock function of the electric heating thermostats."""

    def child_lock_change(self, value):
        """Child lock change."""
        self._update_attribute(self.attributes_by_name["on_off"].id, value)

    def map_attribute(self, attribute, value):
        """Map standardized attribute value to dict of manufacturer values."""
        if attribute == "on_off":
            return {ZWT198_CHILD_LOCK_ATTR: value}


class ZWT198TempCalibration(LocalDataCluster, AnalogOutput):
    """Analog output for Temp Calibration."""

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.ZWT198TempCalibration_bus.add_listener(self)
        self._update_attribute(
            self.attributes_by_name["description"].id, "Temperature Calibration"
        )
        self._update_attribute(self.attributes_by_name["max_present_value"].id, 10)
        self._update_attribute(self.attributes_by_name["min_present_value"].id, -10)
        self._update_attribute(self.attributes_by_name["resolution"].id, 0.1)
        self._update_attribute(self.attributes_by_name["application_type"].id, 13 << 16)
        self._update_attribute(self.attributes_by_name["engineering_units"].id, 62)

    def set_value(self, value):
        """Set value."""
        self._update_attribute(self.attributes_by_name["present_value"].id, value)

    def get_value(self):
        """Get value."""
        return self._attr_cache.get(self.attributes_by_name["present_value"].id)

    async def write_attributes(self, attributes, manufacturer=None):
        """Override the default Cluster write_attributes."""
        for attrid, value in attributes.items():
            if isinstance(attrid, str):
                attrid = self.attributes_by_name[attrid].id
            if attrid not in self.attributes:
                self.error("%d is not a valid attribute id", attrid)
                continue
            self._update_attribute(attrid, value)

            await ZWT198ManufClusterSelf[
                self.endpoint.device.ieee
            ].endpoint.tuya_manufacturer.write_attributes(
                {ZWT198_TEMP_CALIBRATION_ATTR: value * 10},
                manufacturer=None,
            )
        return ([foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],)


class ZWT198(EnchantedDevice, TuyaThermostat):
    """ZWT198 Thermostatic radiator valve."""

    def __init__(self, *args, **kwargs):
        """Init device."""
        self.thermostat_onoff_bus = Bus()
        self.ZWT198TempCalibration_bus = Bus()
        super().__init__(*args, **kwargs)

    signature = {
        MODELS_INFO: [
            ("_TZE204_xnbkhhdr", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaManufClusterAttributes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    ZWT198ManufCluster,
                    ZWT198Thermostat,
                    ZWT198UserInterface,
                    TuyaPowerConfigurationCluster,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
                INPUT_CLUSTERS: [ZWT198ChildLock],
                OUTPUT_CLUSTERS: [],
            },
            3: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.CONSUMPTION_AWARENESS_DEVICE,
                INPUT_CLUSTERS: [ZWT198TempCalibration],
                OUTPUT_CLUSTERS: [],
            },
        }
    }

Tu créer un fichier texte et colle le code du quirk et sauvegarde. Tu renome ce fichier en ts06001.py.
ce fichier ts06001.py, tu le colle dans le dossier zha_quirks.

tu vérifie ta configuration et redémarre HA. Il faut peut être supprimer ton thermostats et le reappairer pour qu’il charge bien le quirk.

Mais en Wifi, en général ça passe par un cloud et si problème avec les serveurs, ça ne fonctionne pas.
Comme tu le dit problème de wifi aussi, fonctionne plus.
Le zigbee c’est du local ( pas de cloud ) et si tu n’as pas internet, ça fonctionne quand même. Après pareil, si ton HA est planté, ça ne fonctionne pas.

SUPER !Ca marche impeccable. Le thermostat est bien intégré, je récupére la température ambiante et celle de consigne.
Ta procédure était super claire…et pour moi remplie de découverte ! C’est la première fois que je modifier le fichier yaml.
Angoisse…et quelque interrogation (comment créer le folder ?) mais le pas à pas était si précis que je suis arrivé au bout !
En dehors du fait d’avoir pu intégrer le thermostat, cette manip m’a enfin permis de franchir l’étape clé pour beaucoup de nouveaux utilisateurs de HA : celle de de plonger dans les modifications de configuration !

2 « J'aime »

Bonjour,

J’ai exactement le même soucis avec un thermostat acheté sur Aliexpress

J’ai installé File editor pour accéder au fichier de configuration, j’ai créé un dossier zha_quirks puis ajouter le fichier ts06001.py

D’ailleurs pourquoi le fichier s’appelle ts06001.py et pas ts0601.py ?
Je redémarre HA, refait un appareillage et la rien n’a changé.

Si je regarde dans le fichier diagnostics. Je vois que le quirks n’est pas appliquer !

J’ai essayé plusieurs tentative mais rien, changé le nom du fichier en ts0601.py mais rien …;
Avez vous une idée svp ?

Bonjour,
car ts0601.py est le nom d’origine du quirk , pour contourner il faut un nom différent.
Ensuite ce quirk est pour le model :

    signature = {
        MODELS_INFO: [
            ("_TZE204_xnbkhhdr", "TS0601"),
        ],

qui n’est pas le votre:
image

Changer le modèle dans le quirk et retester.
C’est le souci pour du Tuya, ça change constamment de numéro de modèle… Ce qui n’est pas le cas chez d’autre marque. Tuya c’est du low cost .

Bonjour,
Merci pour votre réponse.
J’ai effectué les modifs dans le fichier ts06001.py comme indiqué ci-dessus.

Cependant le quirk n’est pas reconnu. :frowning:

Avez vous une autre piste svp ?
Votre remarque pour le Tuyau est bien enregistré :slight_smile:

Merci d’avance

Tu as bien mis le fichier dans /config/zha_quirks/ ?
sous file editor /config = homeassistant/ .

oui voici le chemin d’accès
image

Et mon fichier de configuration.yaml