Koenkk / zigbee-herdsman-converters

Collection of device converters to be used with zigbee-herdsman
MIT License
939 stars 3.09k forks source link

schneider_electric.js - Adding support for switch LED control #5635

Closed linkedupbits closed 1 year ago

linkedupbits commented 1 year ago

Hello, I have some Wiser iconic 41E2PBSWMZ/356PB2MBTZ switches (defined in schneider_electric.js). They have the ability to control the switch LED behaviour:

I have tested this with the Dev Console and it works to read/write the attribute:

Read result of 'clipsalWiserSwitchConfigurationClusterServer': {"SwitchIndication":2}
Wrote '{"SwitchIndication":1}' to 'clipsalWiserSwitchConfigurationClusterServer'
Read result of 'clipsalWiserSwitchConfigurationClusterServer': {"SwitchIndication":1}
Wrote '{"SwitchIndication":2}' to 'clipsalWiserSwitchConfigurationClusterServer'
Read result of 'clipsalWiserSwitchConfigurationClusterServer': {"SwitchIndication":2}

I expect this functionality will be common across all of the Wiser switches with LEDs ie the smart dimmer/ 10ax switches.

I would like to implement this, to raise a PR but I would like guidance on the best pattern to do so.

What is the pattern for where should the changes go? Should they just be in "devices/schneider_electric.js" or is this a function that should be added to converters/fromZigbee.js and converters/toZigbee.js (assuming that other brands etc. provide similar functionality to control on-switch LED behaviour). Or should there be a new schneider specific "lib" file (as say the lib/philips.js)

If I don't create a schneider specific lib, it looks like I should add new functions to:

In the devices/schneider_electric.js;

thanks for any help!

linkedupbits commented 1 year ago

@DanielNagy you might be interested in this (and could probably provide advice please!)

DanielNagy commented 1 year ago

@linkedupbits to be honest, I was looking at how to merge in my local configs for this functionality into the main z2m dev branch. I even had made some further fixes to my own local code as there are issues vs what i pasted in this gist last year https://gist.github.com/phindmarsh/84085fd0f13a79969c51e71d53197d0b?permalink_comment_id=4321210#gistcomment-4321210 But, I wanted the dimmer join issue to be resolved first, that i also mentioned in the gist. which is here https://github.com/Koenkk/zigbee2mqtt/issues/16822 Since this was merged to stable/main and subsequently Home Assistant approx 5 days ago, I'm yet to confirm the problem is resolved by installing a new dimmer. However, there is now comments that the issue still exists. Here are my current local zigbee hersman convertor files now.

However, for your reference, here is my latest local herdsman convertor files, which fixes an issue reading the value of the LED setting. As well as using the clipsal named Cluster Server, instead of elko, as it seems i seen someone merge that in.

CH2AX_SWITCH_1.js

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const utils = require('zigbee-herdsman-converters/lib/utils');
const ota = require('zigbee-herdsman-converters/lib/ota');
const e = exposes.presets;
const ea = exposes.access;

const fzLocal = {
    switchindication: {
        cluster: 'clipsalWiserSwitchConfigurationClusterServer',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const lookup = {
                0: 'Consistent with load', 
                1: 'Always on',
                2: 'Reverse with load',
                3: 'Always off' 
            };
            if ('SwitchIndication' in msg.data) {
                result.switchindication = lookup[msg.data['SwitchIndication']];
            }
            return result;
        },
    },
};

const tzLocal = {
    switchindication: {
        key: ['switchindication'],
        convertSet: async (entity, key, value, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            const lookup = {'Reverse with load': 2, 'Consistent with load': 0, 'Always off': 3, 'Always on':1};
            //value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await endpoint.write(0xFF17, {0x0000: {value: lookup[value], type: 0x30}}, {manufacturerCode: 0x105e});
            return {state: {switchindication: value}};
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            await endpoint.read(0xFF17, [0x0000], {manufacturerCode: 0x105e});
        },
    }
};

const definition = {
    zigbeeModel: ['CH2AX/SWITCH/1'],
    model: '41E2PBSWMZ/356PB2MBTZ',
    vendor: '--test--',
    description: 'Wiser 40/300-Series Module Switch 2A',
    ota: ota.zigbeeOTA,
    fromZigbee: [fz.on_off, fzLocal.switchindication],
    toZigbee: [tz.on_off, tzLocal.switchindication],
    exposes: [exposes.light(), 
        exposes.enum('switchindication', ea.ALL, ['Reverse with load', 'Consistent with load','Always off','Always on'])
            .withDescription('Led Indicator Mode')
    ],
    configure: async (device, coordinatorEndpoint, logger) => {
        const endpoint = device.getEndpoint(1);
        await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
        await reporting.onOff(endpoint);
    }
};

module.exports = definition;

CH10AX_SWITCH_1.js

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const utils = require('zigbee-herdsman-converters/lib/utils');
const ota = require('zigbee-herdsman-converters/lib/ota');
const e = exposes.presets;
const ea = exposes.access;

const fzLocal = {
    switchindication: {
        cluster: 'clipsalWiserSwitchConfigurationClusterServer',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const lookup = {
                0: 'Consistent with load', 
                1: 'Always on',
                2: 'Reverse with load',
                3: 'Always off' 
            };
            if ('SwitchIndication' in msg.data) {
                result.switchindication = lookup[msg.data['SwitchIndication']];
            }
            return result;
        },
    },
};

const tzLocal = {
    switchindication: {
        key: ['switchindication'],
        convertSet: async (entity, key, value, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            const lookup = {'Reverse with load': 2, 'Consistent with load': 0, 'Always off': 3, 'Always on':1};
            //value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await endpoint.write(0xFF17, {0x0000: {value: lookup[value], type: 0x30}}, {manufacturerCode: 0x105e});
            return {state: {switchindication: value}};
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            await endpoint.read(0xFF17, [0x0000], {manufacturerCode: 0x105e});
        },
    }
};

const definition = {
    zigbeeModel: ['CH10AX/SWITCH/1'],
    model: '41E10PBSWMZ/PDL356PB10MBTW',
    vendor: '--test--',
    description: 'Wiser 40/300-Series Module Switch 10A with ControlLink',
    ota: ota.zigbeeOTA,
    fromZigbee: [fz.on_off, fzLocal.switchindication],
    toZigbee: [tz.on_off, tzLocal.switchindication],
    exposes: [exposes.light(), 
        exposes.enum('switchindication', ea.ALL, ['Reverse with load', 'Consistent with load','Always off','Always on'])
            .withDescription('Led Indicator Mode')
    ],
    configure: async (device, coordinatorEndpoint, logger) => {
        const endpoint = device.getEndpoint(1);
        await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
        await reporting.onOff(endpoint);
    }
};

module.exports = definition;

CH_DIMMER_1.js

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const utils = require('zigbee-herdsman-converters/lib/utils');
const ota = require('zigbee-herdsman-converters/lib/ota');
const e = exposes.presets;
const ea = exposes.access;

const fzLocal = {
    switchindication: {
        cluster: 'clipsalWiserSwitchConfigurationClusterServer',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const lookup = {
                0: 'Consistent with load', 
                1: 'Always on',
                2: 'Reverse with load',
                3: 'Always off' 
            };
            if ('SwitchIndication' in msg.data) {
                result.switchindication = lookup[msg.data['SwitchIndication']];
            }
            return result;
        },
    },
    level_config: {
        cluster: 'genLevelCtrl',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const lookup = {
                255: 'previous', 
                254: 'maximum'
            };
            if ('onLevel' in msg.data) {
                result.onLevel = lookup[msg.data['onLevel']];
            }
            return result;
        }
    }
};

const tzLocal = {
    switchindication: {
        key: ['switchindication'],
        convertSet: async (entity, key, value, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            const lookup = {'Reverse with load': 2, 'Consistent with load': 0, 'Always off': 3, 'Always on':1};
            //value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await endpoint.write(0xFF17, {0x0000: {value: lookup[value], type: 0x30}}, {manufacturerCode: 0x105e});
            return {state: {switchindication: value}};
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = entity.getDevice().getEndpoint(21);
            await endpoint.read(0xFF17, [0x0000], {manufacturerCode: 0x105e});
        },
    },
    level_config: {
        key: ['onLevel'],
        convertSet: async (entity, key, value, meta) => { 

            const lookup = {'previous': 255, 'maximum': 254};
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));

            await entity.write('genLevelCtrl', {onLevel: lookup[value]}, utils.getOptions(meta.mapped, entity));
            return {state: {onLevel: value}};
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = entity.getDevice().getEndpoint(3);
            await endpoint.read('genLevelCtrl', [0x11]);
        },

    }
};

const definition = {
    zigbeeModel: ['CH/DIMMER/1'],
    model: '41EPBDWCLMZ/354PBDMBTZ',
    vendor: '--test--',
    description: 'Wiser 40/300-Series Module Dimmer with ControlLink',
    ota: ota.zigbeeOTA,
    fromZigbee: [fz.on_off, fz.brightness, fzLocal.switchindication, fzLocal.level_config, fz.lighting_ballast_configuration],
    toZigbee: [tz.light_onoff_brightness, tzLocal.switchindication, tzLocal.level_config, tz.ballast_config],
    exposes: [e.light_brightness(),
        exposes.enum('switchindication', ea.ALL, ['Reverse with load', 'Consistent with load','Always off','Always on'])
            .withDescription('Led Indicator Mode'),
        exposes.numeric('ballast_minimum_level', ea.ALL).withValueMin(1).withValueMax(254)
            .withDescription('Specifies the minimum light output of the ballast'),
        exposes.numeric('ballast_maximum_level', ea.ALL).withValueMin(1).withValueMax(254)
            .withDescription('Specifies the maximum light output of the ballast'),
        exposes.enum('onLevel', ea.ALL, ['previous', 'maximum'])
            .withDescription('Startup Brightness Level'),
        ],
    configure: async (device, coordinatorEndpoint, logger) => {
        const endpoint = device.getEndpoint(3);
        await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'genLevelCtrl', 'lightingBallastCfg']);
        await reporting.onOff(endpoint);
        await reporting.brightness(endpoint);
    }
};

module.exports = definition;
DanielNagy commented 1 year ago

Also. To answer your question, it seems there is already tz and fz sections defined at the top of devices/schneider_electric.js for other functionality related to other Schneider devices.

This is what prompted me to fix my own local config files of the issue it had.

It should be almost copy and paste all the changes into that one file.

However, as you asked, is this the preferred practice the devs want.

DanielNagy commented 1 year ago

Just saw the merge, thanks for the tag.

You should update all 3 devices whilst you're at it, not just the 2ax, but the 10ax, and the dimmer as well to have the led control.

linkedupbits commented 1 year ago

You should update all 3 devices whilst you're at it, not just the 2ax, but the 10ax, and the dimmer as well to have the led control.

Do you have a 10AX and a dimmer? I haven't installed the dimmer yet (I had previously borrowed an evaluation rig for z2m compatibility testing).

I am a little reluctant to commit without testing - but I can create a branch with the changes you could pull locally to test the changes and then I can update the PR if they work?

DanielNagy commented 1 year ago

All 3 device local config files I provided above I currently use locally with no problem.

As I use home assistant with z2m docker/supervisor, it's a little difficult for me to test branches against my "production" home. 😂

linkedupbits commented 1 year ago

I understand - I was very wary touching my home prod Z2M instance too! I run HAOS in a bhyve vm on Truenas, so I need to run Z2M in a separate BSD jail since bhyve doesn't support USB passthrough from the host. The only changes I needed to make were dropping in a local zigbee-herdsman-converters tgz file and referencing the local version in the packages, so I figured it would be easy to revert....

DanielNagy commented 1 year ago

Yea... don't want to upset the WAF (wife acceptance factor). 😂.

DanielNagy commented 1 year ago

Oh and get yourself a Ethernet/wifi based zigbee dongle, no need to worry about usb pass through 👍.

linkedupbits commented 1 year ago

Updated PR for the other two (fingers crossed!)

DanielNagy commented 1 year ago

Hey @linkedupbits,

Just had a chance to go over all the merged code for the Led Indication. Great work!

You mentioned you didnt have a dimmer (41EPBDWCLMZ/354PBDMBTZ), but there is one more extra functionality (for that device only) that can be rolled into the code around the "Startup Brightness Level" (level_config/onLevel) if you feel the courage to also merge that in! :)

linkedupbits commented 1 year ago

I have just fitted the dimmer - so I can confirm the LED control works for the dimmer in Z2M) Ah - I see it your earlier code.

Happy to raise the PR - I can test it now ;)

DanielNagy commented 1 year ago

@linkedupbits Yep - it was there all along infront of your eyes :)

@koenkk I noticed you reverted {'Reverse with load': 2, 'Consistent with load': 0, 'Always off': 3, 'Always on':1} to {'reverse_with_load': 2, 'consistent_with_load': 0, 'always_off': 3, 'always_on': 1}; As this is what is shown in the web gui, what are the reasons we don't have visually appealing "Reverse with load" gui buttons, but rather "reverse_with_load". Is there a way we can alias, or format gui buttons nicer?

DanielNagy commented 1 year ago

@linkedupbits Out of interest - When you joined the Dimmer, did you have any issues post joining? ie, did you have to restart z2m? or did it work perfectly like the 2ax/10ax?
I ask because I havent been able to test the dimmer join issue fix that has now been rolled into stable, with details here - https://github.com/Koenkk/zigbee2mqtt/issues/16822

Koenkk commented 1 year ago

@DanielNagy the z2m policy is to have all enum values as snakecase since typos are easily made when having white space chars. Maybe we can automatically compute a more friendly name (remove `` and capitalise first letter). You can create a feature request for that here: https://github.com/nurikk/zigbee2mqtt-frontend/issues

DanielNagy commented 1 year ago

@Koenkk certainly makes sense for a programming standpoint for enums, but I think for the web frontend, an automatic capitalise and replace _ with space, as you suggested would be a good idea.

Raised #1714