Aide quirk personnalisé

Hello tout le monde,

Je suis déjà passé par la case présentation, mais c’est la première fois que je viens demander un peu d’aide.

Mon système est composé d’équipements zigbee. J’étais d’abord sous zigbee2mqtt mais trop instable et surtout niveau RAM trop limite. Je suis donc passé sous zha. Mais un équipement (OWON pc321) n’est pas reconnu correctement.

J’ai beaucoup parcouru les docs, et les différents exemple de quirk.

Je suis parvenu à en faire un qui fonctionne. J’accède à tous les attributs et j’arrive à les lire manuellement.

Par contre, si je fais une inclusion sans mon quirk, 2 capteurs sont détectés, et donc utilisable comme entité. Voilà une image.

Lorsque j’utilise mon quirk personnalisé, je n’ai rien du tout. Et là je cale. J’ai tenté beaucoup de choses mais ça ne fonctionne pas.



Pour l’instant, j’utilise zha toolkit, ce qui avec un script me permets de récupérer des valeurs, mais c’est pas le top.

Si quelqu’un a une idée, je suis preneur :blush:.

Voilà mon quirk :

import logging
import zigpy.types as t

from zigpy.profiles import zha
from zigpy.quirks import CustomCluster, CustomDevice
from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef
from zigpy.zcl.clusters.general import (
    Basic,
    Identify,
    )
from zigpy.zcl.clusters.smartenergy import (
    Metering, 
    )
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SKIP_CONFIGURATION,
    )
    
from zhaquirks import Bus, LocalDataCluster


_LOGGER = logging.getLogger(__name__)

""" definitions cluster """

class SimpleMetering(CustomCluster):
    cluster_id = 0x0702
    name = "Owon Simple Metering dbl"
    ep_attribute = "simple_metering"

    POWER_L = 0x2000

    def __init__(self, *args, **kwargs):
        """Init electrical measurement cluster."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.power_bus.add_listener(self)

    def power_reported(self, value):
        """Report consumption."""
        self._update_attribute(self.POWER_L, value)
        
    attributes: dict[int, ZCLAttributeDef] = {
        0x0000: ZCLAttributeDef("current_summ_delivered", type=t.uint48_t, access="r", mandatory=True),
        0x0017: ZCLAttributeDef("Inlet_temperature", type=t.uint24_t, access="r", mandatory=True),
        0x0200: ZCLAttributeDef("Status", type=t.bitmap8, access="r", mandatory=True),
        0x0300: ZCLAttributeDef("unit_of_measure", type=t.enum8, access="r", mandatory=True),
        0x0301: ZCLAttributeDef("multiplier", type=t.uint24_t, access="r", mandatory=True),
        0x0302: ZCLAttributeDef("divisor", type=t.uint24_t, access="r", mandatory=True),
        0x0303: ZCLAttributeDef("summation_formatting", type=t.bitmap8, access="r", mandatory=True),
        0x0304: ZCLAttributeDef("demand_formatting", type=t.bitmap8, access="r", mandatory=True),
        0x0306: ZCLAttributeDef("metering_device_type", type=t.bitmap8, access="r", mandatory=True),
        0x0400: ZCLAttributeDef("Instantaneous_Demand", type=t.uint24_t, access="rw", mandatory=True),
        0x1000: ZCLAttributeDef("report_map", type=t.bitmap8, access="rw", mandatory=True, is_manufacturer_specific=True),
        0x2000: ZCLAttributeDef("L1_phase_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2001: ZCLAttributeDef("L2_phase_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2002: ZCLAttributeDef("L3_phase_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2100: ZCLAttributeDef("L1_phase_reactive_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2101: ZCLAttributeDef("L2_phase_reactive_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2102: ZCLAttributeDef("L3_phase_reactive_power", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x2103: ZCLAttributeDef("reactive_power_summation_of_the_3_phases", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3000: ZCLAttributeDef("L1_phase_voltage", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3001: ZCLAttributeDef("L2_phase_voltage", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3002: ZCLAttributeDef("L3_phase_voltage", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3100: ZCLAttributeDef("L1_phase_current", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3101: ZCLAttributeDef("L2_phase_current", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3102: ZCLAttributeDef("L3_phase_current", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3103: ZCLAttributeDef("current_summation_of_the_3_phases", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x3104: ZCLAttributeDef("leakage_current", type=t.uint24_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4000: ZCLAttributeDef("L1_phase_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4001: ZCLAttributeDef("L2_phase_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4002: ZCLAttributeDef("L3_phase_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4100: ZCLAttributeDef("L1_phase_reactive_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4101: ZCLAttributeDef("L2_phase_reactive_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4102: ZCLAttributeDef("L3_phase_reactive_energy_consumption", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x4103: ZCLAttributeDef("reactive_energy_summation_of_the_3_phases", type=t.uint48_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x5000: ZCLAttributeDef("the_latest_historical_record_time", type=t.uint32_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x5001: ZCLAttributeDef("the_oldest_historical_recorded_time", type=t.uint32_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x5002: ZCLAttributeDef("set_minimum_cycle_for_report", type=t.uint32_t, access="rw", mandatory=True, is_manufacturer_specific=True),
        0x5003: ZCLAttributeDef("set_maximum_cycle_for_report", type=t.uint32_t, access="rw", mandatory=True, is_manufacturer_specific=True),
        0x5004: ZCLAttributeDef("sent_historical_record_state", type=t.uint8_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x5005: ZCLAttributeDef("frequency", type=t.uint8_t, access="r", mandatory=True, is_manufacturer_specific=True),
        0x5006: ZCLAttributeDef("the_accumulative_threshold_of_energy", type=t.uint8_t, access="rw", mandatory=True, is_manufacturer_specific=True),
        0x5007: ZCLAttributeDef("report_mode", type=t.uint8_t, access="rw", mandatory=True, is_manufacturer_specific=True),
        0x5008: ZCLAttributeDef("Set_Z_percent_change_in_power", type=t.uint8_t, access="rw", mandatory=True, is_manufacturer_specific=True),
    } 
    expose = [
        0x2000, 0x2001, 0x2002
    ]


    server_commands: dict[int, ZCLCommandDef] = {
        0x20: ZCLCommandDef("get_history_record", {}, is_manufacturer_specific=True),
        0x21: ZCLCommandDef("stop_sending_historical_record", {}, is_manufacturer_specific=True),
    }
    
    client_commands: dict[int, ZCLCommandDef] = {
        0x20: ZCLCommandDef("sent_historical_record", {}, is_manufacturer_specific=True),
    }
    


class ClearMetering(CustomCluster):
    cluster_id = 0xFFE0
    ep_attribute = "clear_metering"

    attributes: dict[int, ZCLAttributeDef] = {}
    
    server_commands: dict[int, ZCLCommandDef] = {
        0x00: ZCLCommandDef("clear_measurement_data", {}, is_manufacturer_specific=True),
    }
    client_commands: dict[int, ZCLCommandDef] = {}
    

""" nouveau device Owon """

class Owon(CustomDevice):
    
    def __init__(self, *args, **kwargs):
        """Init."""
        self.voltage_bus = Bus()
        self.consumption_bus = Bus()
        self.power_bus = Bus()
        super().__init__(*args, **kwargs)
        
    signature = {
        #MODELS_INFO: [("Owon", "PC321")],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=13
            # device_version=1
            # input_clusters=[0, 3, 1794]
            # output_clusters=[3]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.CONSUMPTION_AWARENESS_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Metering.cluster_id,
                    ],
                OUTPUT_CLUSTERS: [Identify.cluster_id],
            },
        },
        "manufacturer": "OWON Technology Inc.",
    }
    replacement = {
        #SKIP_CONFIGURATION: True,
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=13
            # device_version=1
            # input_clusters=[0, 3, 1794]
            # output_clusters=[3]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.CONSUMPTION_AWARENESS_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    SimpleMetering,
                    ClearMetering,
                ],
                OUTPUT_CLUSTERS: [Identify.cluster_id],
            },
        },
    }

Bonne soirée

Hello

Bizarre ca , car zigbee2mqtt consomme presque que rien , tu l’avais installé sur quoi
Pi/Odroid/pc ?

Quel est ta clé zigbee ?

Combien en ram ??

Car quand je regarde sur z2m ton matos est ok

Hello, effectivement sur zigbee2mqtt c’était bien reconnu.

J’avais à peu près 10% de ram utilisée en plus.

Je suis sur raspberry pi 3 b+, 1Go de ram.
Et sous haos.
J’utilise un conbee 2.

Ca fonctionnait très bien mais j’ai mis un équipement dans mon atelier, j’avais trop de déconnexion et malgré un routeur ça n’a rien changé (j’ai testé différentes config). Sous zha ça fonctionne nickel et plus réactif.

je considére que maintenant 1G de ram pour HA ou autre domo est à la la ramasse, de nos jours
la je tourne avec 52 devices sous z2m ma ram est de 1.2%

Mais bon si cela fonctionne avec zha c’est le principal :wink:

J’utilise un conbee 2.
Bonne cle sauf la porté, j’ai changé la mienne a cause de cela
Par une sonoff version P ( antenne dessus )

C’est sur que c’est limite.
Je me suis lancé dans la domotique fin d’année dernière (avant tout pour mon chauffage et ma piscine), donc je voulais limiter le budget.
Côté raspberry, les prix sont pas encore top.

Quand je continuerai, faudra forcément faire évoluer mon système.

Bonjour,

Il n’y a que 0, 3 et 1794 en input cluster sur cet équipement ?

1794 c’est le cluster metering dans lequel sont déjà définis des attributs que vous redéfinissez.
Je pense donc qu’il faudrait surcharger la classe metering au lieu de CustomCluster

Si on regarde le code de ZHA, il faut effectivement que le cluster metering soit utilisé par l’équipement pour que ces entités apparaissent :

Ainsi par défaut, le cluster 1794 match avec le cluster metering et les entités apparaissent.
Avec votre quirk, vous définissez un customCluster qui ne match pas dans le code de ZHA avec des entités à créer

Un truc plus crade serait dans votre code de changer :

ep_attribute = "simple_metering"

Par

ep_attribute = "smartenergy_metering"

Ça devrait le faire mais c’est pas la bonne méthode (ça pourrait faire crasher ZHA si ca lui plait pas).

Si jamais vous arrivez à un quirk qui fonctionne bien, il serait interessant de faire un PR pour que tout le monde en profite :wink:

Je n’ai pas regardé tous les infos en détails, mais voici ma démarche quand je fais un quirk:

  • J’essaie d’étendre le quirk existant. Ici il n’y en a pas, mais je regarderai de près les infos données pour l’appareil
    Zigbee info: Y-a-t-il un Quirk indiqué?
    Gérer l’appareil Zigbee > SIGNATURE:
  • « Scan » de l’objet avec zha_toolkit.scan_device pour en savoir plus.

Voici ce que j’avais fait pour un lixee/zlinky: Local ZHA quirk for Lixee v12 · GitHub .

On voit que le « DEVICE_TYPE »’ est zha.DeviceType.METER_INTERFACE ce qui éventuellement être mieux pour votre objet.

J’ai tenté, mais cela ne donne rien. Ca me donne le même résultat.

J’ai tenté de changer le device_type, mais le quirk n’est pas appliqué.

Le seul moyen pour l’instant d’appliquer le quirk, c’est ce que j’ai écrit.

Merci pour vos réponses.

Avec ça, je comprends mieux comment sont exposées les entités.

D’après la doc OWON que j’ai trouvé, c’est bien le cluster 1794. Mais pour zha c’est le metering.
Hors dans le cas de mon équipement, il faudrait bien plus d’entités (au minimum la puissance pour les phases L1 L2 et L3).

Mais si je comprends bien, il faudrait ajouter des infos dans le fichier sensor.py.
Pas simple je pense.

Si j’arrive à quelque chose qui fonctionne bien, je le ferai. Je suis pour le partage :slightly_smiling_face:
C’est d’ailleurs grace au partage que j’ai bien avancé pour le moment.

En attendant, avec zha_toolkit j’arrive à lire mes valeurs, mais c’est pas encore l’idéal.

Pour info, la signature c’est ce qui permet de faire correspondre le quirk à l’objet et cela doit correspondre exactement à ce que l’objet est.

Le replacement c’est ce qu’on souhaite comme comportement pour l’objet. C’est là que le DEVICE_TYPE peut être changé.

Je suggère aussi de regarder ceci:

La classe ZLinkyTICMetering hérite de CustomCluster ET de Metering.

Et j’ai l’impression que le ‹ @MULTI_MATCH › suppose que le cluster hérite de Metering:

Ce MULTI_MATCH fait partie d’un mécanisme qui oriente le choix de la classe/sensor à utiliser dans HA.

Je sais bien.
J’ai modifié le DEVICE_TYPE dans le replacement mais sans succès.

J’ai pas encore tenté cette classe, mais si je suis bien, le MULTI_MATCH sera toujours le même au final.
Ce que je voudrais n’existe pas. Il faudrait créer de nouveaux sensors qui correspondrait à cet équipement.

Je vais me pencher là-dessus, et voir si je peux soumettre un ajout.

J’avais donc compris que 2 entités n’apparaissent pas quand votre quirk est pris en compte, et que le quirk n’est pas du tout pris en compte en changeant le device_type.

Concernant ce premier point: il y a plusieurs « MULTI_MATCH » pour des fonctionnalités différentes, donc il est normalement possible de retrouver les entités qui étaient disponibles sans utilisation d’un quirk, tout en ajoutant d’autres éléments.
Le problème concernant l’ajout de fonctionnalités qui n’ont pas encore d’équivalent dans ZHA reste toutefois entier.

L’extrait suivant suggère que l’ajout d’entités de mesure électrique est prévu:

Concernant la modification de « replacement ». La prise en compte du quirk ne dépend que de la signature. Un quirk est pris en compte lorsqu’il apparaît dans « Zigbee Info ». Dans le cas contraire, il y a à mon avis un problème ailleurs - par exemple une erreur dans le python (import d’une constante, etc).

Exemple d’un Zigbee Info avec à la fin la référence au quirk:
image

Hello,

Merci pour ces explications.
J’essai d’avancer un peu.
J’ai changé mon quirk pour passer sur le DEVICE_TYPE Electrical_Measurement. Ca fonctionne aussi mais sans remonter d’entités.

Voila comment j’ai avancé (j’ai simplifié pour n’avoir pour l’instant que l’essentiel):

import zigpy.types as t

from zigpy.profiles import zha
from zigpy.quirks import CustomCluster, CustomDevice
from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef
from zigpy.zcl.clusters.general import (
    Basic,
    Identify,
    )
from zigpy.zcl.clusters.smartenergy import (
    Metering, 
    )
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SKIP_CONFIGURATION,
    )
    
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zhaquirks import LocalDataCluster

class OwonPowerMeasurement(LocalDataCluster, ElectricalMeasurement):
    """Custom class for power, voltage and current measurement."""

    cluster_id = 0x0702

    POWER_ONE_ID = 0x2000
    POWER_TWO_ID = 0x2001
    POWER_THREE_ID = 0x2002

    MULTIPLIER = 0x0301
    DIVISOR = 0x0302
    _CONSTANT_ATTRIBUTES = {MULTIPLIER: 1, DIVISOR: 1000}


    def power_one_reported(self, value):
        """Power L1 reported."""
        self._update_attribute(self.POWER_ONE_ID, value)

    def power_two_reported(self, value):
        """Power L2 reported."""
        self._update_attribute(self.POWER_TWO_ID, value)

    def power_three_reported(self, value):
        """Power L3 reported."""
        self._update_attribute(self.POWER_THREE_ID, value)

    ep_attribute = "electrical_measurement"
    attributes = {
        0x0000: ("current_summ_delivered", t.uint48_t, True),
        0x2000: ("L1_phase_power", t.uint24_t, True),
        0x2001: ("L2_phase_power", t.uint24_t, True),
        0x2002: ("L3_phase_power", t.uint24_t, True),
        0x0400: ("Instantaneous_Demand", t.uint24_t, True),
    }

    server_commands: dict[int, ZCLCommandDef] = {
        0x20: ZCLCommandDef("get_history_record", {}, is_manufacturer_specific=True),
        0x21: ZCLCommandDef("stop_sending_historical_record", {}, is_manufacturer_specific=True),
    }
    
    client_commands: dict[int, ZCLCommandDef] = {
        0x20: ZCLCommandDef("sent_historical_record", {}, is_manufacturer_specific=True),
    }
    


class OwonClearMetering(CustomCluster):
    cluster_id = 0xFFE0
    ep_attribute = "clear_metering"

    attributes: dict[int, ZCLAttributeDef] = {}
    
    server_commands: dict[int, ZCLCommandDef] = {
        0x00: ZCLCommandDef("clear_measurement_data", {}, is_manufacturer_specific=True),
    }
    client_commands: dict[int, ZCLCommandDef] = {}
    

""" New Device Owon PC321 """

class Owon(CustomDevice):
    
    signature = {
        #MODELS_INFO: [("Owon", "PC321")],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=13
            # device_version=1
            # input_clusters=[0, 3, 1794]
            # output_clusters=[3]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.CONSUMPTION_AWARENESS_DEVICE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Metering.cluster_id,
                    ],
                OUTPUT_CLUSTERS: [Identify.cluster_id],
            },
        },
        "manufacturer": "OWON Technology Inc.",
    }
    replacement = {
        #SKIP_CONFIGURATION: True,
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=83
            # device_version=1
            # input_clusters=[0, 3, 1794, 65504]
            # output_clusters=[3]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    OwonPowerMeasurement,
                    OwonClearMetering,
                ],
                OUTPUT_CLUSTERS: [Identify.cluster_id],
            },
        },
    }

La où je bloque c’est pour passer des arguments.
Il y a bien :

class ElectricalMeasurementClusterHandler(ClusterHandler):
    """Cluster handler that polls active power level."""

    CLUSTER_HANDLER_NAME = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT

    class MeasurementType(enum.IntFlag):
        """Measurement types."""

        ACTIVE_MEASUREMENT = 1
        REACTIVE_MEASUREMENT = 2
        APPARENT_MEASUREMENT = 4
        PHASE_A_MEASUREMENT = 8
        PHASE_B_MEASUREMENT = 16
        PHASE_C_MEASUREMENT = 32
        DC_MEASUREMENT = 64
        HARMONICS_MEASUREMENT = 128
        POWER_QUALITY_MEASUREMENT = 256

Donc à priori il serait possible de remonter les infos de 3 phases différentes mais j’ai pas encore trouver.

Ce que vous pouvez faire, si vous parlez anglais, c’est de mettre votre quirk dans l’issue sur le GitHub de ZHA. Un dev devrait le voir et vous aider à le finaliser