Koenkk / zigbee-herdsman-converters

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

Template for new device `TS0601_TZE200_wbhaespm` #7181

Open clumsy-stefan opened 6 months ago

clumsy-stefan commented 6 months ago

Below a template which is working with the above mentioned 3 phase circuit breaker with integrated leakage protection (from Ali). I also have the original documentation from the manufacturer with all the DP's and communication described (in chinese and machine translated in english).

Unfortunately my coding knowledge is limited so I only could get all values readable but not write the configurations, except the switch itself, which can be controlled with this template. Also discussed here.

Probably one of the developers will want to look through it and include it in the standard build. Let me know if I can be of further help!

Thanks, regards.

EDIT: updated version with some more DP's and all settings included... (work in progress)

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 tuya = require('zigbee-herdsman-converters/lib/tuya');
const e = exposes.presets;
const ea = exposes.access;
const extend = require('zigbee-herdsman-converters/lib/extend');
const legacy = require('zigbee-herdsman-converters/lib/legacy');
const utils = require('zigbee-herdsman-converters/lib/utils');
const { Buffer } = require('node:buffer');

const fzLocal = {
        TZE200_wbhaespm: {
        cluster: 'manuSpecificTuya',
        type: ['commandDataResponse', 'commandDataReport'],
        convert: (model, msg, publish, options, meta) => {
                const splitToAttributes = (limit) => {
                const result = {};
                const len = limit.length;
                let i = 0;
                while (i < len) {
                        const key = limit.readUInt8(i);
                        result[key] = [limit.readUInt8(i + 1), limit.readUInt16BE(i + 2)];
                        i += 4;
                }
                return result;
                };
                const lookup = { 0: 'OFF', 1: 'ON' };

                for (const dpValue of msg.data.dpValues) {
                        const value = legacy.getDataValue(dpValue);
                        const dp = dpValue.dp;
//                      meta.logger.info(`DP: ${dp} -- VALUE = ${value}`);
                        switch (dp) {
                                case 16: return {
                                        state: value ? 'ON' : 'OFF',
                                        trip: 'clear',
                                }
                                case 11: return {
                                        prepay: value ? 'ON' : 'OFF',
                                }
                                case 12: {
//                                      meta.logger.info(`DP: ${dp} -- VALUE = ${value}`);
                                        return {
//                                              clear_energy: value ? 'reset' : 'clear',
                                                clear_energy: 'clear',
                                        }
                                }
                                case 102: {
                                        meta.logger.info(`DP: ${dp} -- VALUE = ${value}`);
                                }
                                case 17: {
                                        const limit = splitToAttributes(value);
                                        meta.logger.info(`DEBUG DP #${dp} with data ${JSON.stringify(msg.data)}`);
                                        return {
                                                        'power_threshold': limit[0x03][1],
                                                        'power_breaker': lookup[limit[0x03][0]],
                                                        'leakage_threshold': limit[0x04][1],
                                                        'leakage_breaker': lookup[limit[0x04][0]],
                                                        'temperature_threshold': limit[0x05][1],
                                                        'temperature_breaker': lookup[limit[0x05][0]],
                                        };
                                }
                                case 18: {
                                        const limit = splitToAttributes(value);
                                        meta.logger.info(`DEBUG DP #${dp} with data ${JSON.stringify(msg.data)}`);
                                        return {
                                                        'over_current_threshold': limit[0x01][1],
                                                        'over_current_breaker': lookup[limit[0x01][0]],
                                                        'over_voltage_threshold': limit[0x03][1],
                                                        'over_voltage_breaker': lookup[limit[0x03][0]],
                                                        'under_voltage_threshold': limit[0x04][1],
                                                        'under_voltage_breaker': lookup[limit[0x04][0]],
                                                        'imbalance_power_threshold': limit[0x08][1],
                                                        'imbalance_power_breaker': lookup[limit[0x08][0]],
                                        };
                                }
                        }
                }
          },
         },
};

const tzLocal = {
    TZE200_wbhaespm: {
        key: ['trip', 'clear_energy', 'prepay', 'serial', 'power_threshold', 'power_breaker', 'leakage_threshold', 'leakage_breaker', 'temperature_threshold', 'temperature_breaker',
        'over_current_threshold', 'over_current_breaker', 'over_voltage_threshold', 'over_voltage_breaker', 'under_voltage_threshold', 'under_voltage_breaker', 'imbalance_power_threshold', 'imbalance_power_breaker'],
        convertSet: async (entity, key, value, meta) => {
                const onOffLookup = { 'ON': 1, 'OFF': 0 };
//              meta.logger.info(`Key: ${key} -- VALUE = ${value}`);
                switch (key) {
                case 'trip': {
                        if (value === 'trip') {
                                await tuya.sendDataPointBool(entity, 21, true);
                        };
                        return { state: { trip: 'clear' } };
                }
                case 'clear_energy': {
                        if (value === 'reset') {
                                await tuya.sendDataPointBool(entity, 12, true);
                        };
                        return { state: { clear_energy: 'clear' } };
                }
                case 'prepay': {
                        await tuya.sendDataPointBool(entity, 11, value === 'ON');
                        return { state: { prepay: value } };
                }
                case 'serial': {
                        let data = [];
                        data = data.concat(legacy.convertStringToHexArray(value));
                        await tuya.sendDataPointStringBuffer(entity, 19, data);
                        return { state: { serial: value } };
                }
                case 'power_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
//                      meta.logger.info(`DEBUG ${JSON.stringify(meta.state)}`);
                        state[0] = 3;
                        state[2] = (value & 0xff00) >> 8;
                        state[3] = value & 0xff;
                        meta.logger.info(`DEBUG2 ${JSON.stringify(state)} length: ` + state.length);
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'power_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
//                      meta.logger.info(`DEBUG ${JSON.stringify(meta.state)}`);
                        state[0] = 3;
                        state[1] = utils.getFromLookup(value, onOffLookup);
                        meta.logger.info(`DEBUG2 ${JSON.stringify(state)} length: ` + state.length);
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'leakage_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[4] = 4;
                        state[6] = (value & 0xff00) >> 8;
                        state[7] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'leakage_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[4] = 4;
                        state[5] = utils.getFromLookup(value, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'temperature_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[8] = 5;
                        state[10] = (value & 0xff00) >> 8;
                        state[11] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'temperature_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_1'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[8] = 5;
                        state[9] = utils.getFromLookup(value, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 17, state);
                        break;
                }
                case 'over_current_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[0] = 1;
                        state[2] = (value & 0xff00) >> 8;
                        state[3] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'over_current_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[0] = 1;
                        state[1] = utils.getFromLookup(state, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'over_voltage_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[4] = 3;
                        state[6] = (value & 0xff00) >> 8;
                        state[7] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'over_voltage_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[4] = 3;
                        state[5] = utils.getFromLookup(state, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'under_voltage_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[8] = 4;
                        state[10] = (value & 0xff00) >> 8;
                        state[11] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'under_voltage_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[8] = 4;
                        state[9] = utils.getFromLookup(state, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'imbalance_power_threshold': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[12] = 8;
                        state[14] = (value & 0xff00) >> 8;
                        state[15] = value & 0xff;
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                case 'imbalance_power_breaker': {
                        var state = [];
                        oldstate = meta.state['alarm_set_2'];
                        for (var item in oldstate) {
                                state.push(oldstate[item]);
                        }
                        state[12] = 8;
                        state[13] = utils.getFromLookup(state, onOffLookup);
                        await tuya.sendDataPointRaw(entity, 18, state);
                        break;
                }
                default: // Unknown key
                        meta.logger.warn(`Unhandled key ${key}`);
                }
        },
    },
};

const definition = {
        fingerprint: tuya.fingerprint('TS0601', ['_TZE200_wbhaespm']),
        model: 'TS0601',
        vendor: 'TuYa',
        description: '3 Phase breaker with leakage protection and power meter',
        extend: [],
        fromZigbee: [tuya.fz.datapoints, fzLocal.TZE200_wbhaespm],
        toZigbee: [tuya.tz.datapoints, tzLocal.TZE200_wbhaespm],
        configure: tuya.configureMagicPacket,
        exposes: [  e.switch().setAccess('state', ea.STATE_SET),
                        e.binary('prepay', ea.STATE_SET, 'ON', 'OFF').withDescription('Prepayment switch state'),
                tuya.exposes.voltageWithPhase('a'), tuya.exposes.voltageWithPhase('b'), tuya.exposes.voltageWithPhase('c'),
                tuya.exposes.powerWithPhase('a'), tuya.exposes.powerWithPhase('b'), tuya.exposes.powerWithPhase('c'),
                tuya.exposes.currentWithPhase('a'), tuya.exposes.currentWithPhase('b'), tuya.exposes.currentWithPhase('c'),
                // Change the description according to the specifications of the device
                e.energy().withDescription('Total forward active energy'), e.temperature(),
                e.binary('trip', ea.STATE_SET, 'trip', 'clear').withDescription('Trip'),
                e.binary('clear_energy', ea.STATE_SET, 'reset', 'clear').withDescription('Reset Energy Counter'),
                e.enum('fault', ea.STATE, ['clear', 'short_circuit_alarm', 'surge_alarm', 'overload_alarm',
                        'leakagecurr_alarm', 'temp_dif_fault', 'fire_alarm',
                        'high_power_alarm', 'self_test_alarm', 'ov_cr',
                        'unbalance_alarm', 'ov_vol', 'undervoltage_alarm',
                        'miss_phase_alarm', 'outage_alarm', 'magnetism_alarm',
                        'credit_alarm', 'no_balance_alarm']).withDescription('Fault status of the device (clear = nothing)'),
                e.numeric('serial', ea.STATE).withDescription('Serial'),
                e.numeric('leakage_current', ea.STATE).withDescription('Leakage current'),
                e.numeric('temperature_threshold', ea.STATE_SET).withValueMin(-40).withValueMax(200).withValueStep(1).withUnit('*C')
                        .withDescription('High temperature threshold'),
                e.binary('temperature_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('High temperature breaker'),
                e.numeric('power_threshold', ea.STATE_SET).withValueMin(1).withValueMax(30).withValueStep(1).withUnit('kW')
                        .withDescription('High power threshold'),
                e.binary('power_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('High power breaker'),
                e.numeric('leakage_threshold', ea.STATE_SET).withValueMin(1).withValueMax(1000).withValueStep(1).withUnit('mA')
                        .withDescription('Leakage-current threshold'),
                e.binary('leakage_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('Leakage-current breaker'),
                e.numeric('over_current_threshold', ea.STATE_SET).withValueMin(1).withValueMax(80).withValueStep(1).withUnit('A')
                        .withDescription('Over-current threshold'),
                e.binary('over_current_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('Over-current breaker'),
                e.numeric('over_voltage_threshold', ea.STATE_SET).withValueMin(220).withValueMax(280).withValueStep(1).withUnit('V')
                        .withDescription('Over-voltage threshold'),
                e.binary('over_voltage_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('Over-voltage breaker'),
                e.numeric('under_voltage_threshold', ea.STATE_SET).withValueMin(76).withValueMax(240).withValueStep(1).withUnit('V')
                        .withDescription('Under-voltage threshold'),
                e.binary('under_voltage_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('Under-voltage breaker'),
                e.numeric('imbalance_power_threshold', ea.STATE_SET).withValueMin(1).withValueMax(30).withValueStep(1).withUnit('kW')
                        .withDescription('Imbalance-power threshold'),
                e.binary('imbalance_power_breaker', ea.STATE_SET, 'ON', 'OFF')
                        .withDescription('Imbalance-power breaker'),
                ],
        meta: {
                tuyaDatapoints: [
                [1, 'energy', tuya.valueConverter.divideBy100],
                [6, null, tuya.valueConverter.phaseVariant2WithPhase('a')],
                [7, null, tuya.valueConverter.phaseVariant2WithPhase('b')],
                [8, null, tuya.valueConverter.phaseVariant2WithPhase('c')],
                [9, 'fault', tuya.valueConverterBasic.lookup({ 'clear': 0, 'short_circuit_alarm': 1, 'surge_alarm': 2, 'overload_alarm': 4,
                                                'leakagecurr_alarm': 8, 'temp_dif_fault': 16, 'fire_alarm': 32,
                                                'high_power_alarm': 64, 'self_test_alarm': 128, 'ov_cr': 256,
                                                'unbalance_alarm': 512, 'ov_vol': 1024, 'undervoltage_alarm': 2048,
                                                'miss_phase_alarm': 4096, 'outage_alarm': 8192, 'magnetism_alarm': 16384,
                                                'credit_alarm': 32768, 'no_balance_alarm': 65536 })],
                [11, 'prepay', tuya.valueConverter.onOff],
                [12, 'clear_energy', tuya.valueConverterBasic.lookup({ 'reset': true, 'clear': false })],
                [13, 'balance', tuya.valueConverter.raw],
                [14, 'charge', tuya.valueConverter.raw],
                [15, 'leakage_current', tuya.valueConverter.raw],
                [16, 'state', tuya.valueConverter.onOff],
                [17, 'alarm_set_1', tuya.valueConverter.raw],
                [18, 'alarm_set_2', tuya.valueConverter.raw],
                [19, 'serial', tuya.valueConverter.raw],
                [21, 'trip', tuya.valueConverterBasic.lookup({ 'trip': true, 'clear': false })],
                [101, 'countdown', tuya.valueConverter.raw],
                [102, 'temperature', tuya.valueConverter.divideBy10],
                ],
        },
};

module.exports = definition;
/*

{"1":"Total forward energy",
"6":"Phase A",
"7":"Phase B",
"8":"Phase C",
"9":"Fault",
"11":"Switch prepayment",
"12":"Clear energy",
"13":"Balance energy",
"14":"charge energy",
"15":"Leakage current",
"16":"Switch",
"17":"Alarm set1",
"18":"Alarm set2",
"19":"Breaker id",
"21":"Leakagecurr test",
"101":"Countdown",
"102":"Temp Current"}

"fault": [
"short_circuit_alarm",
"surge_alarm",
"overload_alarm",
"leakagecurr_alarm",
"temp_dif_fault",
"fire_alarm",
"high_power_alarm",
"self_test_alarm",
"ov_cr",
"unbalance_alarm",
"ov_vol",
"undervoltage_alarm",
"miss_phase_alarm",
"outage_alarm",
"magnetism_alarm",
"credit_alarm",
"no_balance_alarm"
]

"alarm_set_1": [
"3:overload_power_limit:kW",
"trip_on_off",
"limit_byte_high",
"limit_byte_low",
"4:leakage_current_limit:mA",
"trip_on_off",
"limit_byte_high",
"limit_byte_low",
"5:over_temperature_limit:C",
"trip_on_off",
"limit_byte_high",
"limit_byte_low"
]
"alarm_set_2": [
"1:overload_current_linit:A",
"trip_on_off",
"limit_byte_high",
"limit_byte_low",
"3:over_voltage_limit:V",
"trip_on_off",
"limit_byte_high",
"limit_byte_low",
"4:under_voltage_limit:V",
"trip_on_off",
"limit_byte_high",
"limit_byte_low",
"8:unbalance_power_limit:kW",
"trip_on_off",
"limit_byte_high",
"limit_byte_low"
]
Note to me:
Overload: 29kW
Leakage: 30mA
High Temp: 80C
Over Current: 63A
Over Voltage: 275V
Under Voltage: 160V
Balance: 10kW

*/
clumsy-stefan commented 5 months ago

I'm struggling to understand the structure of the converters. eg. what needs to go in lib/ , converters/ and devices/ (especially the fzLocal and tzLocal functions). Also if I should extend the /tuya.js definitions oder make a complete new one (all in device/xyz.js or split in lib/xyz.js and device/xyz.js)...

So I'm currently not able to make a PR out of this...

Really sorry, but I'm not an actual developper, but I'm happy to support wherever I can..