Skip to content

[Device Support Request] Tuya Smart Air Detector 6 in 1 #1563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
julianjwong opened this issue May 14, 2022 · 47 comments
Closed

[Device Support Request] Tuya Smart Air Detector 6 in 1 #1563

julianjwong opened this issue May 14, 2022 · 47 comments
Labels
device support request This requests support for a new device Tuya Request/PR regarding a Tuya device

Comments

@julianjwong
Copy link

Is your feature request related to a problem? Please describe.
Current device support for the Tuya Air Detector 6 in 1 is limited to only temperature and VOC sensors. Other sensors such as PM2.5 and CO2 are missing.
This is the device
https://zigbee.blakadder.com/Tuya_DCR-KQG.html
Supposedly working on Z2M

Describe the solution you'd like
Would be great if other sensor entities could be added especially the PM2.5 sensor

Device signature - this can be acquired by clicking on the "Zigbee Device Signature" button in the device settings
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0100",
      "in_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0x0402",
        "0x0405",
        "0x040d",
        "0x042b",
        "0x042e",
        "0xef00"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": 41440,
      "device_type": "0x0061",
      "in_clusters": [],
      "out_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZE200_dwcarsat",
  "model": "TS0601",
  "class": "zhaquirks.tuya.air.ts0601_air_quality.TuyaCO2SensorGPP"
}
Diagnostic information - this can be acquired by clicking on the "Download Diagnostics" button in the device settings
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2022.5.4",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.9.9",
    "docker": true,
    "arch": "x86_64",
    "timezone": "Australia/Sydney",
    "os_name": "Linux",
    "os_version": "5.10.108",
    "supervisor": "2022.05.1",
    "host_os": "Home Assistant OS 7.6",
    "docker_version": "20.10.9",
    "chassis": "vm",
    "run_as_root": true
  },
  "custom_components": {
    "nest_protect": {
      "version": "0.3.7",
      "requirements": []
    },
    "samsungtv_tizen": {
      "version": "1.5.9",
      "requirements": [
        "websocket-client==0.56.0",
        "wakeonlan==1.1.6",
        "numpy==1.21.1"
      ]
    },
    "xiaomi_miot": {
      "version": "0.6.4",
      "requirements": [
        "construct==2.10.56",
        "python-miio>=0.5.6",
        "micloud>=0.3"
      ]
    },
    "zha_toolkit": {
      "version": "v0.8.8",
      "requirements": []
    },
    "powerpal": {
      "version": "0.1.0",
      "requirements": [
        "mindmelting.powerpal==0.3.0"
      ]
    },
    "powercalc": {
      "version": "v0.19.17",
      "requirements": [
        "numpy>=1.21.1"
      ]
    },
    "localtuya": {
      "version": "3.2.1",
      "requirements": []
    },
    "xiaomi_cloud_map_extractor": {
      "version": "v2.1.5",
      "requirements": [
        "pillow",
        "pybase64",
        "python-miio",
        "requests",
        "pycryptodome"
      ]
    },
    "hacs": {
      "version": "1.24.5",
      "requirements": [
        "aiogithubapi>=21.11.0"
      ]
    },
    "echonetlite": {
      "version": "3.4.9",
      "requirements": [
        "pychonet==2.2.1",
        "aio-udp-server"
      ]
    },
    "bureau_of_meteorology": {
      "version": "1.1.2",
      "requirements": []
    },
    "philips_airpurifier_coap": {
      "version": "0.10.6",
      "requirements": [
        "aioairctrl==0.2.3"
      ]
    },
    "ble_monitor": {
      "version": "8.6.5",
      "requirements": [
        "pycryptodomex>=3.14.1",
        "janus>=1.0.0",
        "aioblescan>=0.2.12",
        "btsocket>=0.2.0",
        "pyric>=0.1.6.3"
      ]
    },
    "wiz_light": {
      "version": "0.4.5",
      "requirements": [
        "pywizlight==0.4.15"
      ]
    },
    "hnap_device": {
      "version": "0.2.0",
      "requirements": [
        "hnap>=1.0.0a3,<2.0.0"
      ]
    },
    "philips_airpurifier": {
      "version": "1.0.1",
      "requirements": [
        "pycryptodome"
      ]
    },
    "triones": {
      "version": "0.0.1",
      "requirements": [
        "bleak==0.13.0"
      ]
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "config_flow": true,
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "requirements": [
      "bellows==0.29.0",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.73",
      "zigpy-deconz==0.16.0",
      "zigpy==0.45.1",
      "zigpy-xbee==0.14.0",
      "zigpy-zigate==0.7.4",
      "zigpy-znp==0.7.0"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*zigstar*",
        "known_devices": [
          "ZigStar Coordinators"
        ]
      },
      {
        "vid": "1CF1",
        "pid": "0030",
        "description": "*conbee*",
        "known_devices": [
          "Conbee II"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8A2A",
        "description": "*zigbee*",
        "known_devices": [
          "Nortek HUSBZB-1"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8B34",
        "description": "*bv 2010/10*",
        "known_devices": [
          "Bitron Video AV2010/10"
        ]
      }
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga"
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      }
    ],
    "after_dependencies": [
      "usb",
      "zeroconf"
    ],
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 21487,
    "manufacturer": "_TZE200_dwcarsat",
    "model": "TS0601",
    "name": "_TZE200_dwcarsat TS0601",
    "quirk_applied": true,
    "quirk_class": "zhaquirks.tuya.air.ts0601_air_quality.TuyaCO2SensorGPP",
    "manufacturer_code": 4417,
    "power_source": "Mains",
    "lqi": 47,
    "rssi": null,
    "last_seen": "2022-05-14T10:51:31",
    "available": true,
    "device_type": "Router",
    "signature": {
      "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
      "endpoints": {
        "1": {
          "profile_id": 260,
          "device_type": "0x0100",
          "in_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0x0402",
            "0x0405",
            "0x040d",
            "0x042b",
            "0x042e",
            "0xef00"
          ],
          "out_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "242": {
          "profile_id": 41440,
          "device_type": "0x0061",
          "in_clusters": [],
          "out_clusters": [
            "0x0021"
          ]
        }
      }
    },
    "entities": [
      {
        "entity_id": "sensor.air_quality_sensor_temperature",
        "name": "_TZE200_dwcarsat TS0601"
      },
      {
        "entity_id": "sensor.tze200_dwcarsat_ts0601_0ca13a38_carbon_dioxide_concentration",
        "name": "_TZE200_dwcarsat TS0601"
      },
      {
        "entity_id": "sensor.tze200_dwcarsat_ts0601_0ca13a38_formaldehyde_concentration",
        "name": "_TZE200_dwcarsat TS0601"
      },
      {
        "entity_id": "sensor.tze200_dwcarsat_ts0601_0ca13a38_humidity",
        "name": "_TZE200_dwcarsat TS0601"
      },
      {
        "entity_id": "sensor.air_quality_sensor_voc_level",
        "name": "_TZE200_dwcarsat TS0601"
      }
    ],
    "neighbors": [
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Parent",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xDBB0",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "42"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x5B3B",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "81"
      },
      {
        "device_type": "Coordinator",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x0000",
        "permit_joining": "Unknown",
        "depth": "0",
        "lqi": "60"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x2567",
        "permit_joining": "Unknown",
        "depth": "2",
        "lqi": "85"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xE219",
        "permit_joining": "Unknown",
        "depth": "3",
        "lqi": "66"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x6FBD",
        "permit_joining": "Unknown",
        "depth": "2",
        "lqi": "69"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xEE9C",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "12"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x6CA6",
        "permit_joining": "Unknown",
        "depth": "2",
        "lqi": "60"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xA873",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "33"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x22B2",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "27"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x2ADC",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "33"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xBDDE",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "60"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x882D",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "24"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x4C1E",
        "permit_joining": "Unknown",
        "depth": "1",
        "lqi": "30"
      }
    ],
    "endpoint_names": [
      {
        "name": "ON_OFF_LIGHT"
      },
      {
        "name": "unknown 97 device_type of 0xa1e0 profile id"
      }
    ],
    "user_given_name": "Air quality sensor",
    "device_reg_id": "7746e4f45890650bf6cf8d5f6db31638",
    "area_id": "toy_room"
  }
}
Additional logs
Paste any additional debug logs here.

Additional context
Add any other context or screenshots about the feature request here.

@javicalle
Copy link
Collaborator

javicalle commented May 15, 2022

The device has been tunned in zigbee-herdsman-converters library:

This could be a quirk for the device:

ts0601_air_quality.py
"""Tuya Air Quality sensor."""

from typing import Dict

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time

from zigpy.zcl.clusters.measurement import PM25

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)

from zhaquirks.tuya import DPToAttributeMapping, TuyaLocalCluster, TuyaNewManufCluster

from zhaquirks.tuya.air import (
    TuyaAirQualityCO2,
    TuyaAirQualityFormaldehyde,
    TuyaAirQualityHumidity,
    TuyaAirQualityTemperature,
    TuyaAirQualityVOC,
    TuyaCO2ManufCluster,
)


class TuyaCO2Sensor(CustomDevice):
    """Tuya Air quality device."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_8ygsuhe1", "TS0601"),
            ("_TZE200_yvx5lh6k", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }


class TuyaCO2SensorGPP(CustomDevice):
    """Tuya Air quality device with GPP."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_ryfmq5rl", "TS0601"),
            ("_TZE200_yvx5lh6k", "TS0601"),
            ("_TZE200_dwcarsat", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
                # input_clusters=[]
                # output_clusters=[33]
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }


class TuyaAirQualityPM25(PM25, TuyaLocalCluster):
    """Tuya PM25 concentration measurement."""
    # ug/m^3  https://github.com/Koenkk/zigbee2mqtt/issues/11033#issuecomment-1124056947


class TuyaCO2ManufCluster_2(TuyaNewManufCluster):
    """Tuya with Air quality device v2 with data points."""

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        2: DPToAttributeMapping(
            TuyaAirQualityPM25.ep_attribute,
            "measured_value",
            lambda x: None if (value == 0xaaac or value == 0xaaab) else x,
        ),
        18: DPToAttributeMapping(
            TuyaAirQualityTemperature.ep_attribute, "measured_value", lambda x: x * 10
        ),
        19: DPToAttributeMapping(
            TuyaAirQualityHumidity.ep_attribute, "measured_value", lambda x: x * 10
        ),
        20: DPToAttributeMapping(
            TuyaAirQualityFormaldehyde.ep_attribute,
            "measured_value",
            lambda x: x * 1e-6,
        ),
        21: DPToAttributeMapping(
            TuyaAirQualityVOC.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        22: DPToAttributeMapping(
            TuyaAirQualityCO2.ep_attribute,
            "measured_value",
            lambda x: x * 1e-6,
        ),
    }

    data_point_handlers = {
        2: "_dp_2_attr_update",
        18: "_dp_2_attr_update",
        19: "_dp_2_attr_update",
        20: "_dp_2_attr_update",
        21: "_dp_2_attr_update",
        22: "_dp_2_attr_update",
    }


class TuyaCO2SensorGPP_2(CustomDevice):
    """Tuya Air quality device v2 with GPP."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_dwcarsat", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
                # input_clusters=[]
                # output_clusters=[33]
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster_2,
                    TuyaAirQualityPM25,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Conversion factors can be wrong.
PM25 has a filter value not tested before:

lambda x: None if (value === 0xaaac || value === 0xaaab) else x,

@julianjwong
Copy link
Author

Sorry looks like the PM2.5 sensor code is not right.

Logger: homeassistant.config_entries
Source: components/zha/__init__.py:99
First occurred: 12:15:54 AM (1 occurrences)
Last logged: 12:15:54 AM

Error setting up entry Sonoff Zigbee 3.0 USB Dongle Plus, s/n: 0a28bcebc512ec11892b21c7bd930c07 - ITead for zha
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/config_entries.py", line 335, in async_setup
    result = await component.async_setup_entry(hass, self)
  File "/usr/src/homeassistant/homeassistant/components/zha/__init__.py", line 99, in async_setup_entry
    setup_quirks(config)
  File "/usr/local/lib/python3.9/site-packages/zhaquirks/__init__.py", line 409, in setup
    importer.find_module(modname).load_module(modname)
  File "<frozen importlib._bootstrap_external>", line 529, in _check_name_wrapper
  File "<frozen importlib._bootstrap_external>", line 1029, in load_module
  File "<frozen importlib._bootstrap_external>", line 854, in load_module
  File "<frozen importlib._bootstrap>", line 274, in _load_module_shim
  File "<frozen importlib._bootstrap>", line 711, in _load
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 846, in exec_module
  File "<frozen importlib._bootstrap_external>", line 983, in get_code
  File "<frozen importlib._bootstrap_external>", line 913, in source_to_code
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/config/quirks/ts0601_air_quality.py", line 96
    lambda x: None if (value === 0xaaac || value === 0xaaab) else x,
                               ^
SyntaxError: invalid syntax

@javicalle
Copy link
Collaborator

javicalle commented May 15, 2022

Can you try replacing this expression?:

lambda x: None if (value == 0xaaac or value == 0xaaab) else x,

@julianjwong
Copy link
Author

Thanks but new issue

Logger: homeassistant.config_entries
Source: components/zha/__init__.py:99
First occurred: 11:49:22 AM (1 occurrences)
Last logged: 11:49:22 AM

Error setting up entry Sonoff Zigbee 3.0 USB Dongle Plus, s/n: 0a28bcebc512ec11892b21c7bd930c07 - ITead for zha
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/config_entries.py", line 335, in async_setup
    result = await component.async_setup_entry(hass, self)
  File "/usr/src/homeassistant/homeassistant/components/zha/__init__.py", line 99, in async_setup_entry
    setup_quirks(config)
  File "/usr/local/lib/python3.9/site-packages/zhaquirks/__init__.py", line 409, in setup
    importer.find_module(modname).load_module(modname)
  File "<frozen importlib._bootstrap_external>", line 529, in _check_name_wrapper
  File "<frozen importlib._bootstrap_external>", line 1029, in load_module
  File "<frozen importlib._bootstrap_external>", line 854, in load_module
  File "<frozen importlib._bootstrap>", line 274, in _load_module_shim
  File "<frozen importlib._bootstrap>", line 711, in _load
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 846, in exec_module
  File "<frozen importlib._bootstrap_external>", line 983, in get_code
  File "<frozen importlib._bootstrap_external>", line 913, in source_to_code
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/config/quirks/ts0601_air_quality.py", line 96
    lambda x: None if (value == 0xaaac || value == 0xaaab) else x,
                                        ^
SyntaxError: invalid syntax

@julianjwong
Copy link
Author

Ok so I tried replacing || with or and that seems to have gotten it out of the errors. But for some reason the device isn't using the custom quirk.
Have this in my config and I know that works hence why it was throwing errors previously

zha:
  enable_quirks: true
  custom_quirks_path: /config/quirks

@javicalle
Copy link
Collaborator

I have made a mess trying to put everything in a single file.
Could you try with the new version (from the previous comment)?

Delete any __pycache__ folder in your custom_quirks_path and restart HA.

@julianjwong
Copy link
Author

Cool thanks looks like it loaded this time as denoted by Quirk: ts0601_air_quality.TuyaCO2SensorGPP_2.
Even the previous standard quirk loaded VOC, temperature, CO2, formaldehyde, and humidity (but didn't when I first looked) but unfortunately this custom quirk doesnt add a PM2.5 sensor entity. Should it show up automatically? Or do I need to remove the device and pair it again?

@julianjwong
Copy link
Author

With the new quirk formaldehyde dropped from 360ppm to 4ppm, crazy co2 spikes may have been eliminated which is good. Just not sure if the formaldehyde reading is right

@javicalle
Copy link
Collaborator

formaldehyde dropped from 360ppm to 4ppm

You can try to change in your quirk this part:

        20: DPToAttributeMapping(
            TuyaAirQualityFormaldehyde.ep_attribute,
            "measured_value",
            lambda x: x * 1e-3,
        ),

There's any way to validate wich values are the correct ones?

The conversion factors are probably need to be revised. Tell me if there is any other value that seems strange to you.

I think that I saw that PM2.5 does not generate an entity in HA, but you should be able to query it as attributes of the TuyaAirQualityPM25 cluster.

crazy co2 spikes may have been eliminated which is good

Well, actually these spikes seems that aren't CO2 values but PM2.5 values (and had been filtered).

The modifications would be:

  • temp, hum & VOC keep unchanged
  • the previous CO2 readings (with spikes) readings were actually PM2.5 readings
  • the previous Formaldehyde readings were actually CO2 readings
  • the current Formaldehyde readings weren't previouslly readed

In Z2M there are a table with units and range values for this device:

@julianjwong
Copy link
Author

Ok here's my values vs the table. Looks like some mismatched ones.
167911691-d24b952b-6c5b-4637-991e-4fce758be1f4
Screenshot_20220516-233058_Home Assistant

@afaucogney
Copy link

Hey guys, I bought this device too, but it even does not pair ! Have you got some issue about that by your side ?

Info 2022-05-16 21:50:00Device '0xa4c13821495fcb38' joined
Info 2022-05-16 21:50:01MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0xa4c13821495fcb38","ieee_address":"0xa4c13821495fcb38"},"type":"device_joined"}'
Info 2022-05-16 21:50:01MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":{"friendly_name":"0xa4c13821495fcb38"},"type":"device_connected"}'
Info 2022-05-16 21:50:01Starting interview of '0xa4c13821495fcb38'
Info 2022-05-16 21:50:02MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0xa4c13821495fcb38","ieee_address":"0xa4c13821495fcb38","status":"started"},"type":"device_interview"}'
Info 2022-05-16 21:50:02MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":"interview_started","meta":{"friendly_name":"0xa4c13821495fcb38"},"type":"pairing"}'
Error 2022-05-16 21:52:19Failed to interview '0xa4c13821495fcb38', device has not successfully been paired

@julianjwong
Copy link
Author

Hey guys, I bought this device too, but it even does not pair ! Have you got some issue about that by your side ?

Info 2022-05-16 21:50:00Device '0xa4c13821495fcb38' joined
Info 2022-05-16 21:50:01MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0xa4c13821495fcb38","ieee_address":"0xa4c13821495fcb38"},"type":"device_joined"}'
Info 2022-05-16 21:50:01MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":{"friendly_name":"0xa4c13821495fcb38"},"type":"device_connected"}'
Info 2022-05-16 21:50:01Starting interview of '0xa4c13821495fcb38'
Info 2022-05-16 21:50:02MQTT publish: topic 'zigbee2mqtt/bridge/event', payload '{"data":{"friendly_name":"0xa4c13821495fcb38","ieee_address":"0xa4c13821495fcb38","status":"started"},"type":"device_interview"}'
Info 2022-05-16 21:50:02MQTT publish: topic 'zigbee2mqtt/bridge/log', payload '{"message":"interview_started","meta":{"friendly_name":"0xa4c13821495fcb38"},"type":"pairing"}'
Error 2022-05-16 21:52:19Failed to interview '0xa4c13821495fcb38', device has not successfully been paired

I'm using ZHA and paired ok for me? Did you try moving it closer to your coordinator?

@javicalle
Copy link
Collaborator

Apart from the temperature and humidity, the rest of the values I have no way of validating them.
Values for CO2 may be in the correct range values.

It seems that the units for VOC would not be correct (the device measures in ppm and HA shows ug/m3). Maybe needs to be adjusted here:

The rest of the measures also seem to need to be adjusted.
According to these tables:

values for Formaldehyde seems to be too high, and also with the diferent units. There is no alternative sensor in HA, so maybe a units conversion can be needed:

@julianjwong
Copy link
Author

So I have another sensor in a different room in the same house but presumably similar readings.
Here's what it shows
signal-2022-05-17-20-48-32-309
Ignore temperature and humidity values as the heater is on in that room

@Sesshoumaru-sama
Copy link

Sesshoumaru-sama commented May 17, 2022

I own the same device with the same issues (which almost feels reassuring).
Wonky values for formaldehyde and no showing up of PM2.5

Sad. Hopefully we can fine a fix and get it to the ZHA component in HA.

@javicalle
Copy link
Collaborator

Ummm, it seems that CO2 and VOC values are fine (VOC: 15µg/m3 ~ 0,012mg/m3)

The measuring range of the device seems to be 0-10µg/m3 (possibly without decimals)
HA assumes that Formaldehyde units are PPM with a multiplier of 1e6, and displays values without decimals:

Here I don't know what should be the correct solution.
We can try to do a ug/m3 --> PPM conversion in the quirk so that the information displayed in HA is correct. Or propose a change in HA so that it also accepts PPM as units of measurement for Formaldehyde 🤷🏻‍♂️

Any suggestion is welcome.

@julianjwong
Copy link
Author

I'd say that given these two sensors both have different units it's probably best to suggest a change in HA for the units?

@dmulcahey
Copy link
Collaborator

We had the same issue with VOC in HA. we can create a ppm formaldehyde entity similar to how we did for VOC

@maxperron
Copy link

#1406

I believe it's the same devices and issues

@drmulligan
Copy link

#1707 is also related.

@julianjwong
Copy link
Author

Not sure when this happened but pm2.5 is now showing for me. Has spikes to over 40000 but generally working

@vikitor87
Copy link

Today I have implemented the quirk for this sensor, and it shows everything apparently correct except PM2.5.
Captura1

In the HomeAssistant logs I get this error.

Logger: homeassistant
Source: zha_quirks/ts0601_air_quality.py:158
First occurred: 19:22:42 (1529 occurrences)
Last logged: 19:35:22

Error doing job: Task exception was never retrieved
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/zigpy_znp/zigbee/application.py", line 588, in on_af_message
    self.packet_received(
  File "/usr/local/lib/python3.10/site-packages/zigpy/application.py", line 855, in packet_received
    self.handle_message(
  File "/usr/local/lib/python3.10/site-packages/zigpy/application.py", line 361, in handle_message
    return sender.handle_message(
  File "/usr/local/lib/python3.10/site-packages/zigpy/device.py", line 370, in handle_message
    return endpoint.handle_message(
  File "/usr/local/lib/python3.10/site-packages/zigpy/endpoint.py", line 224, in handle_message
    handler(hdr, args, dst_addressing=dst_addressing)
  File "/usr/local/lib/python3.10/site-packages/zigpy/zcl/__init__.py", line 373, in handle_message
    self.handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
  File "/usr/local/lib/python3.10/site-packages/zhaquirks/tuya/__init__.py", line 1431, in handle_cluster_request
    status = getattr(self, handler_name)(*args)
  File "/usr/local/lib/python3.10/site-packages/zhaquirks/tuya/__init__.py", line 1449, in handle_get_data
    getattr(self, dp_handler)(record)
  File "/usr/local/lib/python3.10/site-packages/zhaquirks/tuya/__init__.py", line 1482, in _dp_2_attr_update
    value = dp_map.converter(value)
  File "/config/zha_quirks/ts0601_air_quality.py", line 158, in <lambda>
    lambda x: None if (value == 0xaaac or value == 0xaaab) else x,
NameError: name 'value' is not defined

Any clue to get the PM2.5 values? I have observed that in Z2M it is possible, and there is a thread about it.
Added new model Tuya Air quality sensor TS0601 _TZE200_dwcarsat
Thanks.

@javicalle
Copy link
Collaborator

Replace this:

lambda x: None if (value == 0xaaac or value == 0xaaab) else x

With:

lambda x: None if (x == 0xaaac or x == 0xaaab) else x

@vikitor87
Copy link

You've got it. Now the value appears, perfect! I think it should be divided by 10, or if the value is true (from 65 to 75 ug/m3), I have a short time to live.

@javicalle
Copy link
Collaborator

Then you can try with;

lambda x: None if (x == 0xaaac or x == 0xaaab) else x/10

@ylemoigne
Copy link

@vikitor87 Can you share the quirk file you've implemented ?

@vikitor87
Copy link

Of course! @ylemoigne . It is the same quirk file published at the beginning but modifying code line 158

ts0601_air_quality.py

"""Tuya Air Quality sensor."""

from typing import Dict

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time

from zigpy.zcl.clusters.measurement import PM25

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)

from zhaquirks.tuya import DPToAttributeMapping, TuyaLocalCluster, TuyaNewManufCluster

from zhaquirks.tuya.air import (
    TuyaAirQualityCO2,
    TuyaAirQualityFormaldehyde,
    TuyaAirQualityHumidity,
    TuyaAirQualityTemperature,
    TuyaAirQualityVOC,
    TuyaCO2ManufCluster,
)


class TuyaCO2Sensor(CustomDevice):
    """Tuya Air quality device."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_8ygsuhe1", "TS0601"),
            ("_TZE200_yvx5lh6k", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }


class TuyaCO2SensorGPP(CustomDevice):
    """Tuya Air quality device with GPP."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_ryfmq5rl", "TS0601"),
            ("_TZE200_yvx5lh6k", "TS0601"),
            ("_TZE200_dwcarsat", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
                # input_clusters=[]
                # output_clusters=[33]
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }


class TuyaAirQualityPM25(PM25, TuyaLocalCluster):
    """Tuya PM25 concentration measurement."""
    # ug/m^3  https://github.com/Koenkk/zigbee2mqtt/issues/11033#issuecomment-1124056947


class TuyaCO2ManufCluster_2(TuyaNewManufCluster):
    """Tuya with Air quality device v2 with data points."""

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        2: DPToAttributeMapping(
            TuyaAirQualityPM25.ep_attribute,
            "measured_value",
            lambda x: None if (x == 0xaaac or x == 0xaaab) else x * 0.1, #Modified "value" and divided by 10
            #lambda x: x, #Test
        ),
        18: DPToAttributeMapping(
            TuyaAirQualityTemperature.ep_attribute, "measured_value", lambda x: x * 10
        ),
        19: DPToAttributeMapping(
            TuyaAirQualityHumidity.ep_attribute, "measured_value", lambda x: x * 10
        ),
        20: DPToAttributeMapping(
            TuyaAirQualityFormaldehyde.ep_attribute,
            "measured_value",
            lambda x: x * 1e-6,
        ),
        21: DPToAttributeMapping(
            TuyaAirQualityVOC.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        22: DPToAttributeMapping(
            TuyaAirQualityCO2.ep_attribute,
            "measured_value",
            lambda x: x * 1e-6,
        ),
    }

    data_point_handlers = {
        2: "_dp_2_attr_update",
        18: "_dp_2_attr_update",
        19: "_dp_2_attr_update",
        20: "_dp_2_attr_update",
        21: "_dp_2_attr_update",
        22: "_dp_2_attr_update",
    }


class TuyaCO2SensorGPP_2(CustomDevice):
    """Tuya Air quality device v2 with GPP."""

    signature = {
        # NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)]
        # device_version=1
        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
        # input_clusters=[0, 4, 5, 61184],
        # output_clusters=[25, 10])
        MODELS_INFO: [
            ("_TZE200_dwcarsat", "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,
                    TuyaCO2ManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
                # input_clusters=[]
                # output_clusters=[33]
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaCO2ManufCluster_2,
                    TuyaAirQualityPM25,
                    TuyaAirQualityCO2,
                    TuyaAirQualityFormaldehyde,
                    TuyaAirQualityHumidity,
                    TuyaAirQualityTemperature,
                    TuyaAirQualityVOC,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

· Entities

cap1
cap2

@gudvinr
Copy link

gudvinr commented Dec 18, 2022

Excluding 0xaaac and 0xaaab is not enough, over the time you'll notice that it produces some very off singular values that will be normal on next measurement. Those values do not make any sense being way over claimed measurement range.

@ylemoigne
Copy link

@vikitor87 thanks, it works (well at least it looks better now, i don't know the accuracy of the device)

@lonnie776
Copy link

So after much head scratching and googling, I was able to get my 6 sensors to show up using ts0601_air_quality.py from above. However now I am fairly certain Formaldehyde, PM2.5, and VOC are all incorrect and will require some other scaling factor. From my brief research my values are out of the normal for indoor conditions. I have also noticed the Unit of Measure is incorrect for VOC and Formaldehyde, but this can be fixed with Customize.

Has anyone been able to get sane readings from Tuya and compared them to the values reported by Home Assistant?

@javicalle
Copy link
Collaborator

From the various reports that I have found of this type of sensor, I think that there are at least 2 different types of sensors with mixed sensors/DPs.
Look for a quirk for your exact manufacturer to be sure.

@TheJulianJES TheJulianJES added the Tuya Request/PR regarding a Tuya device label Jan 4, 2023
@RSDynamics
Copy link

Not a lot of action here but I have the TS0601 _TZE200_dwcarsat
I copied this quirk as the values seems to be mixed up.
Values seem to be at the right place now but the accuracy of the sensor is not very high.
An almost constant humidity of 105% and CO2 level of around 362ppm are just impossible.
PM25 is spiking to 4000 at random times.
Is there a way to add an offset to the values?
CO2 is about 100ppm to low and humidity is 40% to high.

@vietpv89
Copy link

RSDynamics i have same issues, humidity allways higher 100%.
Did u resolve that issue ?

@RSDynamics
Copy link

No, did not had the time yet.
What I do think it is a different issue than a offset. The humidity stays within about 1% so of the 105% so that is also not very realistic.
I think temperature is in the same component and is reliable so not sure what the issue is at the moment.

@codyrocco
Copy link

still no news?
anyway, event on z2m, co2 values seems to be...fixed, at around 350 :)))
attached are values from zha... i should be very dead now!

image

@martin3000
Copy link
Contributor

martin3000 commented Jul 15, 2023

I added this entry to my configuration.yaml:
zha:
.. custom_quirks_path: .homeassistant/custom_zha_quirks/

(with HA core)

I stored the ts0601_air_quality.py in this directory. The formaldehyde values are better now, but with 40ppm outside on the balcony still to high. I think it should not be more than 0.1ppm. Maybe the unit is µg/m³ ? 1 ⁠ppm⁠ =1240 µg/m³.
In this case the value should be divided by 1240 to convert from µg/m³ to ppm. But then in homeassistant/components/zha/sensor.py#L586-L594 , _decimals has to be increased.

Also I think that device has the VOC level in ppm, whereas HA display it in µg. Does it need conversion?

@martin3000
Copy link
Contributor

martin3000 commented Jul 16, 2023

  device unit HA unit conv. fact. healthy if
CO2 ppm ppm   <1000 ppm (outdoor: 400ppm)
Formaldehyde µg/m³ ppm /1240 <0.1 ppm
Pm2.5 µg/m³ µg/m³   <50 µg/m³
VOC ppm µg/m³ *2500 <3000 µg/m³ (outdoor 100-200 µg/m³)
Temperature °C °C    
Humidity % %    

@martin3000
Copy link
Contributor

martin3000 commented Jul 17, 2023

By comparing the values with the tuya app, I have now found the following:

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        2: DPToAttributeMapping(
            TuyaAirQualityPM25.ep_attribute, "measured_value",  lambda x: None if (x>500) else x
        ),
        18: DPToAttributeMapping(
            TuyaAirQualityTemperature.ep_attribute, "measured_value", lambda x: x * 10
        ),
        19: DPToAttributeMapping(
            TuyaAirQualityHumidity.ep_attribute, "measured_value", lambda x: x * 10
        ),
        20: DPToAttributeMapping(
           TuyaAirQualityFormaldehyde.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        21: DPToAttributeMapping(
            TuyaAirQualityVOC.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        22: DPToAttributeMapping(
            TuyaAirQualityCO2.ep_attribute, "measured_value", lambda x: x * 1e-6,
        ),
    }

The formaldehyde unit must be changed from ppm to ppb (customize: section).

@martin3000
Copy link
Contributor

VOC is displayed in the tuya app with the unit mg/m³, in the HA tuya integration it shows up as ppm. The numbers seem to be the same. If zhaquirks sends a value of 10, the tuya app shows it as 0,010 mg/m³ (would be 10 µg/m³) and the tuya HA integration shows it as 0,010 ppm (would be 25 µg/m³).

Copy link

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added the stale Issue is inactivate and might get closed soon label Jan 15, 2024
@Abdull
Copy link

Abdull commented Jan 18, 2024

/unstale

@github-actions github-actions bot removed the stale Issue is inactivate and might get closed soon label Jan 18, 2024
@frog32
Copy link
Contributor

frog32 commented Apr 5, 2024

The combined work of folks in this thread already results in a much better quirk than is currently available from zha-device-handlers. Would any of the contributors mind submitting a pull request so less tech-savvy people can use it as well and are not getting garbage?

@Eis-T
Copy link

Eis-T commented Sep 7, 2024

Hi, I'm trying to get this device to work and it turns out I don't have the skills yet to get this sensor to work in HA. Can somebody who figured this one out post a config / quirk here? First time working with quirks so I might be making obvious mistakes.

@martin3000
Copy link
Contributor

Just pair it in ZHA and it will work. You don't need any skills.

@darth-hp
Copy link

By comparing the values with the tuya app, I have now found the following:

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        2: DPToAttributeMapping(
            TuyaAirQualityPM25.ep_attribute, "measured_value",  lambda x: None if (x>500) else x
        ),
        18: DPToAttributeMapping(
            TuyaAirQualityTemperature.ep_attribute, "measured_value", lambda x: x * 10
        ),
        19: DPToAttributeMapping(
            TuyaAirQualityHumidity.ep_attribute, "measured_value", lambda x: x * 10
        ),
        20: DPToAttributeMapping(
           TuyaAirQualityFormaldehyde.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        21: DPToAttributeMapping(
            TuyaAirQualityVOC.ep_attribute, "measured_value", lambda x: x * 1e-6
        ),
        22: DPToAttributeMapping(
            TuyaAirQualityCO2.ep_attribute, "measured_value", lambda x: x * 1e-6,
        ),
    }

The formaldehyde unit must be changed from ppm to ppb (customize: section).

Hi Martin,

How do I get this to work for ZHA?

Kind regards

@TheJulianJES TheJulianJES added the device support request This requests support for a new device label Jan 26, 2025
@prairiesnpr
Copy link
Collaborator

This was added in #3651.

@martin3000
Copy link
Contributor

Every few minutes, the pm2.5 sensor sends the value 0xAAAC (43692)
For my TZE200_dwcarsat I had a custom quirks with this code:
lambda x: None if (x == 0xaaac or x == 0xaaab or x == 0xaa00 or x>5000) else x
or alternatively:
lambda x: None if (x>500) else x
This removed the strange peak values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
device support request This requests support for a new device Tuya Request/PR regarding a Tuya device
Projects
None yet
Development

No branches or pull requests