zigbee2mqtt icon indicating copy to clipboard operation
zigbee2mqtt copied to clipboard

[New device support]: TOMZN TOB9Z-VAP Smart circuit breaker

Open lordlightman opened this issue 9 months ago • 7 comments

Link

https://www.aliexpress.com/item/1005006177598520.html

Database entry

{"id":18,"type":"Router","ieeeAddr":"top_secret","nwkAddr":"top_secret","manufId":4098,"manufName":"_TZ3000_303avxxt","powerSource":"Mains (single phase)","modelId":"TS011F","epList":[1,242],"endpoints":{"1":{"profId":260,"epId":1,"devId":266,"inClusterList":[0,3,4,5,6,1794,2820,1026,57344,57345],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"65503":"\u0000\u0000\u0000\u0000\u0005\u001b\t�-\u0013\u001c\t�-\u0012!\t�-\u0018","65506":31,"65508":0,"65534":0,"modelId":"TS011F","manufacturerName":"_TZ3000_303avxxt","stackVersion":0,"dateCode":"","zclVersion":3,"appVersion":69,"powerSource":1}},"genOnOff":{"attributes":{"32768":0,"onOff":1,"onTime":0,"offWaitTime":0,"tuyaBacklightMode":1,"moesStartUpOnOff":2,"tuyaBacklightSwitch":1}},"manuSpecificTuya_3":{"attributes":{"53248":0,"53249":0,"53250":0,"53251":0,"53252":0,"53253":0,"powerOnBehavior":2,"switchType":0}},"haElectricalMeasurement":{"attributes":{"acCurrentDivisor":1000,"acCurrentMultiplier":1,"rmsVoltage":229,"rmsCurrent":0,"activePower":0}},"seMetering":{"attributes":{"divisor":100,"multiplier":1,"currentSummDelivered":[0,0]}},"manuSpecificBosch":{"attributes":{"53251":"AAAA"}},"msTemperatureMeasurement":{"attributes":{"measuredValue":0}}},"binds":[{"cluster":1026,"type":"endpoint","deviceIeeeAddress":"top_secret","endpointID":1},{"cluster":6,"type":"endpoint","deviceIeeeAddress":"top_secret","endpointID":1},{"cluster":2820,"type":"endpoint","deviceIeeeAddress":"top_secret","endpointID":1},{"cluster":1794,"type":"endpoint","deviceIeeeAddress":"top_secret","endpointID":1}],"configuredReportings":[{"cluster":2820,"attrId":1285,"minRepIntval":5,"maxRepIntval":3600,"repChange":1,"manufacturerCode":null},{"cluster":2820,"attrId":1288,"minRepIntval":5,"maxRepIntval":3600,"repChange":10,"manufacturerCode":null},{"cluster":2820,"attrId":1291,"minRepIntval":5,"maxRepIntval":3600,"repChange":1,"manufacturerCode":null},{"cluster":1794,"attrId":0,"minRepIntval":5,"maxRepIntval":3600,"repChange":[1,1],"manufacturerCode":null}],"meta":{}},"242":{"profId":41440,"epId":242,"devId":97,"inClusterList":[],"outClusterList":[33],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":69,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"meta":{"configured":332242049},"lastSeen":1715873912168}

Comments

Hello. Please, add support for TOMZN TOB9Z-VAP Smart circuit breaker.

I was able to put together an external converter for this device, see external definition section.

Observations

  • unlike similar models from EARU EAKCB-T-M-Z and Tongou TO-Q-SY2-163JZT, this circuit breaker does not measure temperature, nor does it provide temp breaker and threshold (also no power breaker and threshold)
  • energy reading is reset to 0 after the circuit breaker is removed from Z2M and then added again

Here is an image of the device from the official product page:

TOMZN TOB9Z-VAP Smart circuit breaker

External definition

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

const fzLocal = {
    TS011F_electrical_measurement: {
        ...fz.electrical_measurement,
        convert: async (model, msg, publish, options, meta) => {
            const result = await fz.electrical_measurement.convert(model, msg, publish, options, meta) ?? {};
            const lookup = {power: 'activePower', current: 'rmsCurrent', voltage: 'rmsVoltage'};

            // Wait 5 seconds before reporting a 0 value as this could be an invalid measurement.
            // https://github.com/Koenkk/zigbee2mqtt/issues/16709#issuecomment-1509599046
            if (result) {
                for (const key of ['power', 'current', 'voltage']) {
                    if (key in result) {
                        const value = result[key];
                        clearTimeout(globalStore.getValue(msg.endpoint, key));
                        if (value === 0) {
                            const configuredReporting = msg.endpoint.configuredReportings.find((c) =>
                                c.cluster.name === 'haElectricalMeasurement' && c.attribute.name === lookup[key]);
                            const time = ((configuredReporting ? configuredReporting.minimumReportInterval : 5) * 2) + 1;
                            globalStore.putValue(msg.endpoint, key, setTimeout(() => {
                                const payload = {[key]: value};
                                // Device takes a lot of time to report power 0 in some cases. When current == 0 we can assume power == 0
                                // https://github.com/Koenkk/zigbee2mqtt/discussions/19680#discussioncomment-7868445
                                if (key === 'current') {
                                    payload.power = 0;
                                }
                                publish(payload);
                            }, time * 1000));
                            delete result[key];
                        }
                    }
                }
            }

            // Device takes a lot of time to report power 0 in some cases. When the state is OFF we can assume power == 0
            // https://github.com/Koenkk/zigbee2mqtt/discussions/19680#discussioncomment-7868445
            if (meta.state.state === 'OFF') {
                result.power = 0;
            }

            return result;
        }
    },
    TS011F_threshold: {
        cluster: 'manuSpecificTuya_3',
        type: 'raw',
        convert: (model, msg, publish, options, meta) => {
            const splitToAttributes = (value) => {
                const result = {};
                const len = value.length;
                let i = 0;
                while (i < len) {
                    const key = value.readUInt8(i);
                    result[key] = [value.readUInt8(i+1), value.readUInt16BE(i+2)];
                    i += 4;
                }
                return result;
            };
            const lookup = {0: 'OFF', 1: 'ON'};
            const command = msg.data[2];
            const data = msg.data.slice(3);
            if (command == 0xE7) {
                const value = splitToAttributes(data);
                return {
                    'over_current_threshold': value[0x01][1],
                    'over_current_breaker': lookup[value[0x01][0]],
                    'over_voltage_threshold': value[0x03][1],
                    'over_voltage_breaker': lookup[value[0x03][0]],
                    'under_voltage_threshold': value[0x04][1],
                    'under_voltage_breaker': lookup[value[0x04][0]],
                };
            }
        }
    }
};

const tzLocal = {
    TS011F_threshold: {
        key: [
            'over_current_threshold', 'over_current_breaker', 'over_voltage_threshold', 'over_voltage_breaker',
            'under_voltage_threshold', 'under_voltage_breaker',
        ],
        convertSet: async (entity, key, value, meta) => {
            const onOffLookup = {'on': 1, 'off': 0};
            switch (key) {
            case 'over_current_threshold': {
                const state = meta.state['over_current_breaker'];
                const buf = Buffer.from([1, utils.getFromLookup(state, onOffLookup), 0, utils.toNumber(value, 'over_current_threshold')]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            case 'over_current_breaker': {
                const threshold = meta.state['over_current_threshold'];
                const number = utils.toNumber(threshold, 'over_current_threshold');
                const buf = Buffer.from([1, utils.getFromLookup(value, onOffLookup), 0, number]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            case 'over_voltage_threshold': {
                const state = meta.state['over_voltage_breaker'];
                const buf = Buffer.from([3, utils.getFromLookup(state, onOffLookup), 0, utils.toNumber(value, 'over_voltage_breaker')]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            case 'over_voltage_breaker': {
                const threshold = meta.state['over_voltage_threshold'];
                const number = utils.toNumber(threshold, 'over_voltage_threshold');
                const buf = Buffer.from([3, utils.getFromLookup(value, onOffLookup), 0, number]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            case 'under_voltage_threshold': {
                const state = meta.state['under_voltage_breaker'];
                const buf = Buffer.from([4, utils.getFromLookup(state, onOffLookup), 0, utils.toNumber(value, 'under_voltage_threshold')]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            case 'under_voltage_breaker': {
                const threshold = meta.state['under_voltage_threshold'];
                const number = utils.toNumber(threshold, 'under_voltage_breaker');
                const buf = Buffer.from([4, utils.getFromLookup(value, onOffLookup), 0, number]);
                await entity.command('manuSpecificTuya_3', 'setOptions3', {data: buf});
                break;
            }
            default: // Unknown key
                logger.warning(`Unhandled key ${key}`, NS);
            }
        }
    }
};

const definition = {
    fingerprint: tuya.fingerprint('TS011F', ['_TZ3000_303avxxt']),
    model: 'TS011F_with_threshold',
    description: 'Din rail switch with power monitoring and threshold settings',
    vendor: 'TuYa',
    ota: ota.zigbeeOTA,
    extend: [tuya.modernExtend.tuyaOnOff({
        electricalMeasurements: true, electricalMeasurementsFzConverter: fzLocal.TS011F_electrical_measurement,
        powerOutageMemory: true, indicatorMode: true,
    })],
    fromZigbee: [fz.temperature, fzLocal.TS011F_threshold],
    toZigbee: [tzLocal.TS011F_threshold],
    exposes: [
        e.numeric('over_current_threshold', ea.STATE_SET).withValueMin(1).withValueMax(64).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(265).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'),
    ],
    configure: async (device, coordinatorEndpoint) => {
        await tuya.configureMagicPacket(device, coordinatorEndpoint);
        const endpoint = device.getEndpoint(1);
        endpoint.command('genBasic', 'tuyaSetup', {});
        await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'haElectricalMeasurement', 'seMetering']);
        await reporting.rmsVoltage(endpoint, {change: 1});
        await reporting.rmsCurrent(endpoint, {change: 10});
        await reporting.activePower(endpoint, {change: 1});
        await reporting.currentSummDelivered(endpoint);
        endpoint.saveClusterAttributeKeyValue('haElectricalMeasurement', {acCurrentDivisor: 1000, acCurrentMultiplier: 1});
        endpoint.saveClusterAttributeKeyValue('seMetering', {divisor: 100, multiplier: 1});
        device.save();
    },
    whiteLabel: [
        tuya.whitelabel('TOMZN', 'TOB9Z-VAP', 'Smart circuit breaker', ['_TZ3000_303avxxt']),
    ],
};

module.exports = definition;

lordlightman avatar May 16 '24 16:05 lordlightman

hey, @lordlightman I've bought the same device and encountered the same issue.

I'm thinking about writing zha quirk and add it to my hass, but I'm new to this. Are there any way I could adapt your code above to python zha quirk file?

ogvalt avatar May 16 '24 18:05 ogvalt

Hello @ogvalt. Sorry, I am not a developer, I barely scraped this converter together following various tutorials on how to write an external converter for Z2M.

lordlightman avatar May 16 '24 19:05 lordlightman

@lordlightman what you scraped together is quite impressive!

ogvalt avatar May 16 '24 19:05 ogvalt

Thanks @ogvalt, I just took bits and pieces from Tuya converter for similar device EARU EAKCB-T-M-Z and after some trial and error managed to make my external converter work.

lordlightman avatar May 16 '24 20:05 lordlightman

@lordlightman it seems I also successfully found a solution and everything working as I expected

ogvalt avatar May 17 '24 09:05 ogvalt

could you make a pull request to add out of the box support for this device?

Koenkk avatar May 17 '24 20:05 Koenkk

Hello @Koenkk. Sorry, I'm not a developer, I do not know how to integrate my converter into the file with all other Tuya converters.

lordlightman avatar May 19 '24 10:05 lordlightman