Koenkk / zigbee2mqtt

Zigbee 🐝 to MQTT bridge 🌉, get rid of your proprietary Zigbee bridges 🔨
https://www.zigbee2mqtt.io
GNU General Public License v3.0
11.49k stars 1.62k forks source link

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

Open lordlightman opened 1 month ago

lordlightman commented 1 month ago

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

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;
ogvalt commented 1 month ago

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?

lordlightman commented 1 month ago

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.

ogvalt commented 1 month ago

@lordlightman what you scraped together is quite impressive!

lordlightman commented 1 month ago

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.

ogvalt commented 1 month ago

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

Koenkk commented 1 month ago

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

lordlightman commented 1 month ago

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.

hallard commented 3 days ago

@lordlightman just to let you know I added this converter (using GUI) with latest Z2M (1.39.0-1) but converter is not starting firing an error

[2024-07-02 00:25:01] info:     z2m: Logging to console, file (filename: log.log)
[2024-07-02 00:25:01] error:    z2m: Failed to load external converter file blabla

[2024-07-02 00:25:01] error:    z2m: Probably there is a syntax error in the file or the external converter is not compatible with the current Zigbee2MQTT version

anyway, not a big deal but could be great to see this device integrated to Z2M :-)

BTW, not related I also added this device to Tuya Smart Home (with Tuya ZigBee HUB) and I don't have any option (in Tuya App) to set threshold for over voltage or over current, very strange because it is the same device of the picture. So if someone knows how to set these option with native app, let me know please.

ogvalt commented 3 days ago

I also have no luck using over voltage/current protection via zha. it seems that device do not expose corresponding endpoints.

so I would say it's kinda misleading marketing from their side, because i specifically chose this device over cheaper one

hallard commented 3 days ago

@ogvalt did you install the external decoder in ZHA? also did you try with official tuya app just to be sure?

Anyway I bought them also for this feature and I'm unable to have the menu below with official app

image

ogvalt commented 3 days ago

@hallard I haven't tried and I'm unable to, because my setup is custom. Also no external decoder.

My observations are based on observations from home assistant logs. I found zha device descriptor that matched with endpoints that device was broadcasting. Maybe they yet not exposed this features on firmware level?

hallard commented 3 days ago

@ogvalt got it, I think to have it on HA you need the custom decoder of this issue (if your device can handle it of course), may explain why you don't have it, worth trying maybe :-)

@Koenkk, you confirm this change is not merged due to lack of PR?

Koenkk commented 2 days ago

you confirm this change is not merged due to lack of PR?

yes