Skip to content
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

Add VOC/CO/NO cluster #1287

Closed
wants to merge 4 commits into from
Closed

Conversation

RobinFrcd
Copy link

Hi,
So the VOC/NO/CO Clusters are not defined in Zigbee, but are in Matter.

As said in Koenkk/zigbee2mqtt#23339 I just added these new clusters, is that fine for you ? @Koenkk

Thanks !

@Koenkk
Copy link
Owner

Koenkk commented Jan 6, 2025

Did you already see any Zigbee devices which use these?

@RobinFrcd
Copy link
Author

@Koenkk
Copy link
Owner

Koenkk commented Jan 6, 2025

Since it's not an official Zigbee cluster, I would then use the custom cluster API, example

@RobinFrcd
Copy link
Author

RobinFrcd commented Jan 6, 2025

Alright, thanks for your answer !
But just to understand, did anything change since Koenkk/zigbee2mqtt#23339 ? It was also for a custom sensor with Matter's clusters.

PS: Found a sensor with VOC https://www.ikea.com/us/en/p/vindstyrka-air-quality-sensor-smart-30498239/
EDIT: Ok, my bad, for Ikea's I guess it's handled here: https://github.com/Koenkk/zigbee-herdsman-converters/blob/40e72bc2515deb67ef35dddab04437342b7b3eb9/src/lib/ikea.ts#L800

@Koenkk
Copy link
Owner

Koenkk commented Jan 7, 2025

I think we did not support the custom clusters at that time.

@RobinFrcd
Copy link
Author

Alright, understood, thanks ! Will try to work on adding a new device/clusters and submit a new PR soon then !

@RobinFrcd RobinFrcd closed this Jan 7, 2025
@RobinFrcd
Copy link
Author

Hey again
So I've started working on it, should it look something like this ? I've tried to look for a simple example in the examples' folder but couldn't find any. Any chance you could add a simple example to follow ?

Thank you very much !

const {
    temperature, 
    humidity, 
    pm25, 
    co2,
    numeric,
    deviceAddCustomCluster
} = require('zigbee-herdsman-converters/lib/modernExtend');
const {Zcl} = require('zigbee-herdsman');

const addCustomClusters = () => [
    deviceAddCustomCluster('vocMeasurement', {
        ID: 0xFC01,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        }
    }),
    deviceAddCustomCluster('pm1Measurement', {
        ID: 0xFC02,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        }
    }),
    deviceAddCustomCluster('pm4Measurement', {
        ID: 0xFC03,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        }
    }),
    deviceAddCustomCluster('pm10Measurement', {
        ID: 0xFC04,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        }
    }),
    deviceAddCustomCluster('noxMeasurement', {
        ID: 0xFC05,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        }
    })
];

const definition = {
    zigbeeModel: ['SEN66'],
    model: 'SEN66',
    vendor: 'Sensirion',
    description: 'Custom multi-sensor device with VOC, PM1, PM4, PM10, and NOx measurements',
    extend: [
        temperature(), 
        humidity(), 
        pm25(), 
        co2(),
        ...addCustomClusters(),
        numeric({
            name: 'voc',
            cluster: 'vocMeasurement',
            attribute: 'measuredValue',
            unit: 'ppb',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured VOC value'
        }),
        numeric({
            name: 'pm1',
            cluster: 'pm1Measurement',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM1 value'
        }),
        numeric({
            name: 'pm4',
            cluster: 'pm4Measurement',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM4 value'
        }),
        numeric({
            name: 'pm10',
            cluster: 'pm10Measurement',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM10 value'
        }),
        numeric({
            name: 'nox',
            cluster: 'noxMeasurement',
            attribute: 'measuredValue',
            unit: 'ppb',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured NOx value'
        })
    ],
    configure: async (device, coordinatorEndpoint, logger) => {
        const endpoint = device.getEndpoint(1);
        const clusters = [0xFC01, 0xFC02, 0xFC03, 0xFC04, 0xFC05];
        
        for (const cluster of clusters) {
            try {
                await endpoint.bind(cluster, coordinatorEndpoint);
                await endpoint.configureReporting(cluster, [{
                    attribute: 'measuredValue',
                    minimumReportInterval: 10,
                    maximumReportInterval: 3600,
                    reportableChange: 1
                }]);
            } catch (error) {
                logger.warn(`Failed to configure cluster ${cluster}: ${error}`);
            }
        }
    },
    meta: {
        configureKey: 1,
    },
};

module.exports = definition;

@Koenkk
Copy link
Owner

Koenkk commented Jan 13, 2025

This looks correct, doesn't it work?

@RobinFrcd
Copy link
Author

Sadly, no.

Is saw this popping in the logs, seems like it fails for FC02 but not FC01.

zigbee2mqtt  | [2025-01-13 20:39:30] info: 	z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/bridge/state', payload '{"state":"online"}'
zigbee2mqtt  | [2025-01-13 20:39:30] error: 	z2m: Failed to call 'OnEvent' 'start' (AssertionError [ERR_ASSERTION]: Custom cluster ID (64514) should match existing cluster ID (1068)
zigbee2mqtt  |     at Device.addCustomCluster (/app/node_modules/zigbee-herdsman/src/controller/model/device.ts:1271:19)
zigbee2mqtt  |     at addCluster (/app/node_modules/zigbee-herdsman-converters/src/lib/modernExtend.ts:2443:20)
zigbee2mqtt  |     at onEvent (/app/node_modules/zigbee-herdsman-converters/src/lib/modernExtend.ts:2447:86)
zigbee2mqtt  |     at Object.onEvent (/app/node_modules/zigbee-herdsman-converters/src/index.ts:214:27)
zigbee2mqtt  |     at processTicksAndRejections (node:internal/process/task_queues:95:5)
zigbee2mqtt  |     at OnEvent.callOnEvent (/app/lib/extension/onEvent.ts:50:13)
zigbee2mqtt  |     at OnEvent.start (/app/lib/extension/onEvent.ts:12:13)
zigbee2mqtt  |     at Controller.callExtensions (/app/lib/controller.ts:399:17)
zigbee2mqtt  |     at Controller.start (/app/lib/controller.ts:218:9)
zigbee2mqtt  |     at start (/app/index.js:154:5))

and everything is null in the UI
image

Thanks for your help mate !

@Koenkk
Copy link
Owner

Koenkk commented Jan 13, 2025

Try using a different cluster name, names from cluster.ts cannot be re-used (e.g.

)

@RobinFrcd
Copy link
Author

Wow, after a few changes, it finally works smoothly, thank you very much !
As this is kind of a custom project (that I will open-source soon), does it make sense to open a PR with this device ? Or is it meant to be kept as a custom external converter ?

PS: I'm also waiting ESP-IDF to add support for official clusters, but I don't think it will happen anytime soon. espressif/esp-zigbee-sdk#531

const {
    temperature, 
    humidity, 
    pm25, 
    co2,
    numeric,
    deviceAddCustomCluster
} = require('zigbee-herdsman-converters/lib/modernExtend');
const {Zcl} = require('zigbee-herdsman');
const {logger} = require('zigbee-herdsman-converters/lib/logger');

const NS = 'zhc:sensirion';

const addCustomClusters = () => [
    deviceAddCustomCluster('sen66VocMeas', {
        ID: 0xFC01,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        },
        commands: {},
        commandsResponse: {},
    }),
    deviceAddCustomCluster('sen66Pm1Meas', {
        ID: 0xFC02,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        },
        commands: {},
        commandsResponse: {},
    }),
    deviceAddCustomCluster('sen66Pm4Meas', {
        ID: 0xFC03,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        },
        commands: {},
        commandsResponse: {},
    }),
    deviceAddCustomCluster('sen66Pm10Meas', {
        ID: 0xFC04,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        },
        commands: {},
        commandsResponse: {},
    }),
    deviceAddCustomCluster('sen66NoxMeas', {
        ID: 0xFC05,
        attributes: {
            measuredValue: {ID: 0x0000, type: Zcl.DataType.SINGLE_PREC}
        },
        commands: {},
        commandsResponse: {},
    })
];

const definition = {
    zigbeeModel: ['SEN66'],
    model: 'SEN66',
    vendor: 'Sensirion',
    description: 'Custom multi-sensor device with VOC, PM1, PM4, PM10, and NOx measurements',
    extend: [
        temperature(), 
        humidity(), 
        pm25(), 
        co2(),
        ...addCustomClusters(),
        numeric({
            name: 'voc',
            cluster: 'sen66VocMeas',
            attribute: 'measuredValue',
            unit: 'ppb',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured VOC value'
        }),
        numeric({
            name: 'pm1',
            cluster: 'sen66Pm1Meas',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM1 value'
        }),
        numeric({
            name: 'pm4',
            cluster: 'sen66Pm4Meas',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM4 value'
        }),
        numeric({
            name: 'pm10',
            cluster: 'sen66Pm10Meas',
            attribute: 'measuredValue',
            unit: 'µg/m³',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured PM10 value'
        }),
        numeric({
            name: 'nox',
            cluster: 'sen66NoxMeas',
            attribute: 'measuredValue',
            unit: 'ppb',
            access: 'STATE_GET',
            reporting: {
                min: 10,
                max: 3600,
                change: 1
            },
            description: 'Measured NOx value'
        })
    ],
    configure: async (device, coordinatorEndpoint) => {
        const endpoint = device.getEndpoint(1);
        const clusters = [0xFC01, 0xFC02, 0xFC03, 0xFC04, 0xFC05];
        
        for (const cluster of clusters) {
            try {
                await endpoint.bind(cluster, coordinatorEndpoint);
                await endpoint.configureReporting(cluster, [{
                    attribute: 'measuredValue',
                    minimumReportInterval: 10,
                    maximumReportInterval: 3600,
                    reportableChange: 1
                }]);
                logger.info(`Configured cluster ${cluster} for device ${device.ieeeAddress}`, NS);
            } catch (error) {
                logger.warning(`Failed to configure cluster ${cluster}: ${error}`, NS);
            }
        }
    },
    meta: {
        configureKey: 1,
    },
};

module.exports = definition;

@Koenkk
Copy link
Owner

Koenkk commented Jan 14, 2025

If instructions how to make this device are available, it can be added to https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/src/devices/custom_devices_diy.ts

@RobinFrcd
Copy link
Author

Alright, so I just published the instructions for the device here: https://github.com/RobinFrcd/Senesp98

With Z2M custom device here: https://github.com/RobinFrcd/Senesp98/blob/master/zigbee2mqtt/sen66.js

Would it make sense to you if I add it to custom_devices_diy.ts too ? Or this project is a bit too specific ?

@Koenkk
Copy link
Owner

Koenkk commented Jan 21, 2025

Yes, feel free to make a PR to custom devices and a PR to z2m.io with the links to your GH repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants