Koenkk / zigbee2mqtt

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

[New device support]: EMIZB-141 Power Meter #19042

Open lSh4dowl opened 9 months ago

lSh4dowl commented 9 months ago

Link

https://www.amazon.de/Electricity-Zählerauslesung-Energieverbrauchsüberwachung-LED-Impulsen-funktioniert/dp/B0CGVB6LGC

Database entry

{"id":54,"type":"EndDevice","ieeeAddr":"0x0015bc001b100a56","nwkAddr":51420,"manufId":4117,"manufName":"frient A/S","powerSource":"Battery","modelId":"EMIZB-141","epList":[1,2],"endpoints":{"1":{"profId":49353,"epId":1,"devId":1,"inClusterList":[5,6],"outClusterList":[],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":83,"inClusterList":[0,1,3,32,1794,2817,2821],"outClusterList":[3,10,25],"clusters":{"genBasic":{"attributes":{}},"genPollCtrl":{"attributes":{"checkinInterval":14400}},"seMetering":{"attributes":{"currentSummDelivered":[0],"instantaneousDemand":354,"multiplier":1,"divisor":1000,"develcoPulseConfiguration":10000,"develcoInterfaceMode":0}}},"binds":[{"cluster":32,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2ad1c","endpointID":1},{"cluster":1794,"type":"endpoint","deviceIeeeAddress":"0x00124b0024c2ad1c","endpointID":1}],"configuredReportings":[{"cluster":1794,"attrId":1024,"minRepIntval":5,"maxRepIntval":3600,"repChange":1}],"meta":{}}},"dateCode":"2023-09-06 11:54","zclVersion":7,"interviewCompleted":true,"meta":{"configured":-570313272},"lastSeen":1695324832904,"defaultSendRequestWhen":"active","checkinInterval":3600}

Comments

It works reasonable, but shows no battery for example. I basically just copied an old Version of the previous version (https://www.zigbee2mqtt.io/devices/ZHEMI101.html) I only linked a German Amazon Page, because the Vendor doesn't have a site for the product yet (this is the old version: https://frient.com/products/electricity-meter-interface/)

External converter

const exposes = require('zigbee-herdsman-converters/lib/exposes');
const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee};
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const constants = require('zigbee-herdsman-converters/lib/constants');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const globalStore = require('zigbee-herdsman-converters/lib/store');
const utils = require('zigbee-herdsman-converters/lib/utils');
const ota = require('zigbee-herdsman-converters/lib/ota');
const e = exposes.presets;
const ea = exposes.access;

// develco specific cosntants
const manufacturerOptions = {manufacturerCode: 0x1015};

/* MOSZB-1xx - ledControl - bitmap8 - r/w
 * 0x00 Disable LED when movement is detected.
 * 0x01 Enables periodic fault flashes. These flashes are used to indicate e.g. low battery level.
 * 0x02 Enables green application defined LED. This is e.g. used to indicate motion detection.
 * Default value 0xFF ( seems to be fault + motion)
 */
const develcoLedControlMap = {
    0x00: 'off',
    0x01: 'fault_only',
    0x02: 'motion_only',
    0xFF: 'both',
};

// develco specific convertors
const develco = {
    configure: {
        read_sw_hw_version: async (device, logger) => {
            for (const ep of device.endpoints) {
                if (ep.supportsInputCluster('genBasic')) {
                    try {
                        const data = await ep.read('genBasic', ['develcoPrimarySwVersion', 'develcoPrimaryHwVersion'],
                            manufacturerOptions);

                        if (data.hasOwnProperty('develcoPrimarySwVersion')) {
                            device.softwareBuildID = data.develcoPrimarySwVersion.join('.');
                        }

                        if (data.hasOwnProperty('develcoPrimaryHwVersion')) {
                            device.hardwareVersion = data.develcoPrimaryHwVersion.join('.');
                        }
                    } catch (error) {/* catch timeouts of sleeping devices */}
                    break;
                }
            }
        },
    },
    fz: {
        // Some Develco devices report strange values sometimes
        // https://github.com/Koenkk/zigbee2mqtt/issues/13329
        electrical_measurement: {
            ...fz.electrical_measurement,
            convert: (model, msg, publish, options, meta) => {
                if (msg.data.rmsVoltage !== 0xFFFF && msg.data.rmsCurrent !== 0xFFFF && msg.data.activePower !== -0x8000) {
                    return fz.electrical_measurement.convert(model, msg, publish, options, meta);
                }
            },
        },
        device_temperature: {
            ...fz.device_temperature,
            convert: (model, msg, publish, options, meta) => {
                if (msg.data.currentTemperature !== -0x8000) {
                    return fz.device_temperature.convert(model, msg, publish, options, meta);
                }
            },
        },
        temperature: {
            ...fz.temperature,
            convert: (model, msg, publish, options, meta) => {
                if (msg.data.measuredValue !== -0x8000 && msg.data.measuredValue !== 0xFFFF) {
                    return fz.temperature.convert(model, msg, publish, options, meta);
                }
            },
        },
        metering: {
            ...fz.metering,
            convert: (model, msg, publish, options, meta) => {
                if (msg.data.instantaneousDemand !== -0x800000) {
                    return fz.metering.convert(model, msg, publish, options, meta);
                }
            },
        },
        pulse_configuration: {
            cluster: 'seMetering',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                const result = {};
                if (msg.data.hasOwnProperty('develcoPulseConfiguration')) {
                    result[utils.postfixWithEndpointName('pulse_configuration', msg, model, meta)] =
                        msg.data['develcoPulseConfiguration'];
                }

                return result;
            },
        },
        interface_mode: {
            cluster: 'seMetering',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                const result = {};
                if (msg.data.hasOwnProperty('develcoInterfaceMode')) {
                    result[utils.postfixWithEndpointName('interface_mode', msg, model, meta)] =
                        constants.develcoInterfaceMode.hasOwnProperty(msg.data['develcoInterfaceMode']) ?
                            constants.develcoInterfaceMode[msg.data['develcoInterfaceMode']] :
                            msg.data['develcoInterfaceMode'];
                }
                if (msg.data.hasOwnProperty('status')) {
                    result['battery_low'] = (msg.data.status & 2) > 0;
                    result['check_meter'] = (msg.data.status & 1) > 0;
                }

                return result;
            },
        },
        fault_status: {
            cluster: 'genBinaryInput',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                const result = {};
                if (msg.data.hasOwnProperty('reliability')) {
                    const lookup = {0: 'no_fault_detected', 7: 'unreliable_other', 8: 'process_error'};
                    result.reliability = lookup[msg.data['reliability']];
                }
                if (msg.data.hasOwnProperty('statusFlags')) {
                    result.fault = (msg.data['statusFlags']===1);
                }
                return result;
            },
        },
        voc: {
            cluster: 'develcoSpecificAirQuality',
            type: ['attributeReport', 'readResponse'],
            options: [exposes.options.precision('voc'), exposes.options.calibration('voc')],
            convert: (model, msg, publish, options, meta) => {
                // from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
                // "The mean molar mass of this mixture is 110 g/mol and hence,
                // 1 ppb TVOC corresponds to 4.5 μg/m3."
                const vocPpb = parseFloat(msg.data['measuredValue']);
                const voc = vocPpb * 4.5;
                const vocProperty = utils.postfixWithEndpointName('voc', msg, model, meta);

                // from aqszb-110-technical-manual-air-quality-sensor-04-08-20.pdf page 6, section 2.2 voc
                // this contains a ppb to level mapping table.
                let airQuality;
                const airQualityProperty = utils.postfixWithEndpointName('air_quality', msg, model, meta);
                if (vocPpb <= 65) {
                    airQuality = 'excellent';
                } else if (vocPpb <= 220) {
                    airQuality = 'good';
                } else if (vocPpb <= 660) {
                    airQuality = 'moderate';
                } else if (vocPpb <= 2200) {
                    airQuality = 'poor';
                } else if (vocPpb <= 5500) {
                    airQuality = 'unhealthy';
                } else if (vocPpb > 5500) {
                    airQuality = 'out_of_range';
                } else {
                    airQuality = 'unknown';
                }
                return {[vocProperty]: utils.calibrateAndPrecisionRoundOptions(voc, options, 'voc'), [airQualityProperty]: airQuality};
            },
        },
        voc_battery: {
            cluster: 'genPowerCfg',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                /*
                 * Per the technical documentation for AQSZB-110:
                 * To detect low battery the system can monitor the "BatteryVoltage" by setting up a reporting interval of every 12 hour.
                 * When a voltage of 2.5V is measured the battery should be replaced.
                 * Low batt LED indication–RED LED will blink twice every 60 second.
                 */
                const result = fz.battery.convert(model, msg, publish, options, meta);
                result.battery_low = (result.voltage <= 2500);
                return result;
            },
        },
        led_control: {
            cluster: 'genBasic',
            type: ['attributeReport', 'readResponse'],
            options: [],
            convert: (model, msg, publish, options, meta) => {
                const state = {};

                if (msg.data.hasOwnProperty('develcoLedControl')) {
                    state['led_control'] = develcoLedControlMap[msg.data['develcoLedControl']];
                }

                return state;
            },
        },
        ias_occupancy_timeout: {
            cluster: 'ssIasZone',
            type: ['attributeReport', 'readResponse'],
            options: [],
            convert: (model, msg, publish, options, meta) => {
                const state = {};

                if (msg.data.hasOwnProperty('develcoAlarmOffDelay')) {
                    state['occupancy_timeout'] = msg.data['develcoAlarmOffDelay'];
                }

                return state;
            },
        },
        input: {
            cluster: 'genBinaryInput',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg, publish, options, meta) => {
                const result = {};
                if (msg.data.hasOwnProperty('presentValue')) {
                    const value = msg.data['presentValue'];
                    result[utils.postfixWithEndpointName('input', msg, model, meta)] = value == 1;
                }
                return result;
            },
        },
    },
    tz: {
        pulse_configuration: {
            key: ['pulse_configuration'],
            convertSet: async (entity, key, value, meta) => {
                await entity.write('seMetering', {'develcoPulseConfiguration': value}, manufacturerOptions);
                return {readAfterWriteTime: 200, state: {'pulse_configuration': value}};
            },
            convertGet: async (entity, key, meta) => {
                await entity.read('seMetering', ['develcoPulseConfiguration'], manufacturerOptions);
            },
        },
        interface_mode: {
            key: ['interface_mode'],
            convertSet: async (entity, key, value, meta) => {
                const payload = {'develcoInterfaceMode': utils.getKey(constants.develcoInterfaceMode, value, undefined, Number)};
                await entity.write('seMetering', payload, manufacturerOptions);
                return {readAfterWriteTime: 200, state: {'interface_mode': value}};
            },
            convertGet: async (entity, key, meta) => {
                await entity.read('seMetering', ['develcoInterfaceMode'], manufacturerOptions);
            },
        },
        current_summation: {
            key: ['current_summation'],
            convertSet: async (entity, key, value, meta) => {
                await entity.write('seMetering', {'develcoCurrentSummation': value}, manufacturerOptions);
                return {state: {'current_summation': value}};
            },
        },
        led_control: {
            key: ['led_control'],
            convertSet: async (entity, key, value, meta) => {
                const ledControl = utils.getKey(develcoLedControlMap, value, value, Number);
                await entity.write('genBasic', {'develcoLedControl': ledControl}, manufacturerOptions);
                return {state: {led_control: value}};
            },
            convertGet: async (entity, key, meta) => {
                await entity.read('genBasic', ['develcoLedControl'], manufacturerOptions);
            },
        },
        ias_occupancy_timeout: {
            key: ['occupancy_timeout'],
            convertSet: async (entity, key, value, meta) => {
                let timeoutValue = value;
                if (timeoutValue < 20) {
                    meta.logger.warn(`Minimum occupancy_timeout is 20, using 20 instead of ${timeoutValue}!`);
                    timeoutValue = 20;
                }
                await entity.write('ssIasZone', {'develcoAlarmOffDelay': timeoutValue}, manufacturerOptions);
                return {state: {occupancy_timeout: timeoutValue}};
            },
            convertGet: async (entity, key, meta) => {
                await entity.read('ssIasZone', ['develcoAlarmOffDelay'], manufacturerOptions);
            },
        },
        input: {
            key: ['input'],
            convertGet: async (entity, key, meta) => {
                await entity.read('genBinaryInput', ['presentValue']);
            },
        },
    },
};

const definition = {
    zigbeeModel: ['EMIZB-141'], // The model ID from: Device with modelID 'lumi.sens' is not supported.
    model: 'EMIZB-141', // Vendor model number, look on the device for a model number
    vendor: 'frient A/S', // Vendor of the device (only used for documentation and startup logging)
    description: 'frient Powermeter', // Description of the device, copy from vendor site. (only used for documentation and startup logging)
    fromZigbee: [develco.fz.metering, develco.fz.pulse_configuration, develco.fz.interface_mode],
        toZigbee: [develco.tz.pulse_configuration, develco.tz.interface_mode, develco.tz.current_summation],
        endpoint: (device) => {
            return {'default': 2};
        },
        configure: async (device, coordinatorEndpoint, logger) => {
            const endpoint = device.getEndpoint(2);
            await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']);
            await reporting.instantaneousDemand(endpoint);
            await reporting.readMeteringMultiplierDivisor(endpoint);
        },
        exposes: [
            e.power(),
            e.energy(),
            e.battery_low(),
            exposes.numeric('pulse_configuration', ea.ALL).withValueMin(0).withValueMax(65535)
                .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'),
            exposes.enum('interface_mode', ea.ALL,
                ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0'])
                .withDescription('Operating mode/probe'),
            exposes.numeric('current_summation', ea.SET)
                .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0)
                .withValueMax(268435455),
            exposes.binary('check_meter', ea.STATE, true, false)
                .withDescription('Is true if communication problem with meter is experienced'),
        ],
};

module.exports = definition;

Supported color modes

No response

Color temperature range

No response

Koenkk commented 9 months ago

Could you make a PR to add support for this device?

lSh4dowl commented 9 months ago

I am still testing and experiencing some weirdness, where I see less usage than in reality, however, I don’t know if it is due to my power meter or the device, since I have no other reader device to compare to.

lSh4dowl commented 9 months ago

I could need some help with this :)

Readings are definitely not accurate when set to the right frequency, also another problem I have noticed is that I now have a small solar system and it seems to add the generated overflow energy that goes out to the total.

j91321 commented 9 months ago

I've encountered the same problem, it's mentioned even on develco website that this is not compatible with PV systems, it doesn't happen to 100% of electrometers, but some of them are configured to give impulse also on energy export and you have no way of distinguishing import from export (if you have on-grid setup for solar)

lSh4dowl commented 9 months ago

my system seems to have 2 IR Lights, so I wonder if one is dedicated for export and one for import, might test this out by just covering one up.

j91321 commented 9 months ago

Many systems have multiple lights, my also has 2, but one shows imp./kWh the other imp./kvarh. Best to find and consult manual.

lSh4dowl commented 9 months ago

That would be a bummer, since to my knowledge this is the only zigbee power meter, that’s why I bought it and this makes it practically useless :/ Probably will have to send it back if this is the case.

j91321 commented 9 months ago

There are other power meters like TuYa TS0601 although the installation is definitely more complex than the LED one. Based on my research so far this would also work better with PV because, it should be able to determine the import/export direction depending on where the clamps are installed.

KaytheDays commented 9 months ago

Will this device be officially supported ? I am considering it, as it seems the device with the most simple installation out there … https://www.develcoproducts.com/de/produkte/zaehlermodule/modul-fuer-stromzaehler-2-led/#EMI2LEDAngaben

LifeInZeitnot commented 9 months ago

Hello, i'm new to Zigbee2mqtt, how can i import the config shown by lSh4dowl ?

petosiso commented 9 months ago

I had same problem yesterday, this worked for me:

Via the web terminal, I could create my converter file per the Z2M guide and place it in /config/zigbee2mqtt/.js (that's what the guide means by "This file has to be created next to the configuration.yaml", because the configuration.yaml file for Z2M is in /config/zigbee2mqtt, at least in HassOS).

Then, to tell Z2M to use the converter file, go to the Z2M UI, then click "Settings" > "External Converters". Enter the filename of the converter file you just created ("SPP02GIP.js" in my case), and click "Submit", then "Restart".

The external converter file should now be in use by Z2M, which you can verify in the logs per the guide linked in the OP.

AlmightyCZ commented 8 months ago

@petosiso Can you confirm, that the device is working correctly when you don't have a solar system?

penroseg commented 8 months ago

Hi @petosiso. I can confirm it's working well on a non-solar system meter. Have had it running for about 3 weeks (using the custom converter shared by @lSh4dowl. Including showing up in Energy Dashboard.

There are issues with some fields (Battery mainly) and the Summation resets to Zero no matter what is entered. I was hoping to use that latter field to input my starting Meter number and have it increase from there but from reading back it appears that was an issue with the v1 meter also. Maybe when 'proper' support comes to Z2M it will work but for the moment it is more than enough for me.

I've also set up Utility meters for Hour, Day & Month so I can see a visual. I used a version of this article here to set up my Day, Night & Peak rates. https://community.home-assistant.io/t/stuck-setting-up-peak-off-peak-electricity-tariff/419894/5

image

image

image

petosiso commented 8 months ago

@AlmightyCZ yes, I can also confirm that it works. Exactly as @penroseg described, image image

jllarraz commented 7 months ago

Does anyone knows when is this to be added to the list of supported devices?

AlmightyCZ commented 7 months ago

It's now supported in 1.34.0(https://github.com/Koenkk/zigbee-herdsman-converters/pull/6582 + https://www.zigbee2mqtt.io/devices/EMIZB-141.html)

cawith commented 7 months ago

Great news that it is now supported in 1.34.0, but for some reason I don't get an energy reading. Does anybody face the same issue or have a fix? I tried reconfiguring.

Skærmbillede 2023-12-02 kl  11 55 09
ywaf commented 7 months ago

Great news that it is now supported in 1.34.0, but for some reason I don't get an energy reading. Does anybody face the same issue or have a fix? I tried reconfiguring. Skærmbillede 2023-12-02 kl 11 55 09

you have to set the pulse setting which isnt there in the official build, im currently working on a pr, or use the custom external converter

alerich-x commented 7 months ago

I've encountered the same problem, it's mentioned even on develco website that this is not compatible with PV systems, it doesn't happen to 100% of electrometers, but some of them are configured to give impulse also on energy export and you have no way of distinguishing import from export (if you have on-grid setup for solar)

So this is a hardware issue and the device support in Z2MQTT doesn't make any difference?

akostamo commented 7 months ago

This custom converter works so much better than what is currently in the release version, so eagerly waiting this to be the officially released!

dawiinci commented 7 months ago

Great news that it is now supported in 1.34.0, but for some reason I don't get an energy reading. Does anybody face the same issue or have a fix? I tried reconfiguring.

you have to set the pulse setting which isnt there in the official build, im currently working on a pr, or use the custom external converter

That is good news. What are your plans on the PR? I am considering using the external converter to make energy work, but this might interfere with the PR, right?

ywaf commented 7 months ago

Great news that it is now supported in 1.34.0, but for some reason I don't get an energy reading. Does anybody face the same issue or have a fix? I tried reconfiguring.

you have to set the pulse setting which isnt there in the official build, im currently working on a pr, or use the custom external converter

That is good news. What are your plans on the PR? I am considering using the external converter to make energy work, but this might interfere with the PR, right?

nah just remove the custom converter if u wanna go back to normal converter

dawiinci commented 6 months ago

In the external converter I am missing the battery indicator in %. Is there a way to add it?

AlmightyCZ commented 6 months ago

In the external converter I am missing the battery indicator in %. Is there a way to add it?

The converter in release is reporting battery percentage, but it's probably not working. Despite showing 100% consistently, my meter stopped updating (after 60 days of operation), indicating battery depletion. After replacing the batteries, it started working correctly again.

AlmightyCZ commented 6 months ago

Also the energy reporting has stoped working (as someone has already mentioned here) so I'm going back to the original external converter for now.

fumantsu commented 6 months ago

I will agree with the use of external converter. It is much more complete. I managed to set the current summation but to be fair not sure what should be. Is it the "current value" in kWh that I see in the power box? (for the Czech people I have PRE).

nivek1612 commented 6 months ago

Used the external converter mentioned above. Running for 5 days and the tracking of my usage matches the electricity meter perfectly.

jllarraz commented 5 months ago

I have used the external converter but the values (energy and power) seems a bit odd to me, can someone point me in the right direction { "battery": 60, "battery_low": false, "energy": 4295032.83, "interface_mode": 65534, "linkquality": 255, "power": -8388608, "pulse_configuration": 1000, "voltage": 2700, "check_meter": null, "current_summation": null }

akostamo commented 5 months ago

I have used the external converter but the values (energy and power) seems a bit odd to me, can someone point me in the right direction { "battery": 60, "battery_low": false, "energy": 4295032.83, "interface_mode": 65534, "linkquality": 255, "power": -8388608, "pulse_configuration": 1000, "voltage": 2700, "check_meter": null, "current_summation": null }

Check from your energy meter, there should be pulse rate mentioned near the pulse led. Default is 1000 per kwh like in my example picture attached. Just adjust the pulse configuration.

Screenshot_2024-01-25-19-48-50-19_ab7988c7b00b15bc78ec5a428c58236f

jllarraz commented 5 months ago

I have used the external converter but the values (energy and power) seems a bit odd to me, can someone point me in the right direction { "battery": 60, "battery_low": false, "energy": 4295032.83, "interface_mode": 65534, "linkquality": 255, "power": -8388608, "pulse_configuration": 1000, "voltage": 2700, "check_meter": null, "current_summation": null }

Check from your energy meter, there should be pulse rate mentioned near the pulse led. Default is 1000 per kwh like in my example picture attached. Just adjust the pulse configuration.

Screenshot_2024-01-25-19-48-50-19_ab7988c7b00b15bc78ec5a428c58236f

I checked and is 1000 imp/kwh so wondering if I have to do something else

nivek1612 commented 5 months ago

Your power reading is negative which is impossible unless you are exporting energy to the grid. Even if you are thats a huge number.

This is what mine looks like and its tracking the usage very accurately.

{ "battery": 100, "battery_low": false, "energy": 660.83, "interface_mode": "electricity", "linkquality": 48, "power": 3234, "pulse_configuration": 1000, "voltage": 3100, "check_meter": null, "current_summation": null }

maybe do a reset of the frient and check you put the sensor very accurately over the pulse LED.

reset procedure is here https://help.frient.com/en/frient/electricity-meter-interface-2-led/t/cllmabm0o17vmkv0unrk3y34j

jllarraz commented 5 months ago

Your power reading is negative which is impossible unless you are exporting energy to the grid. Even if you are thats a huge number.

This is what mine looks like and its tracking the usage very accurately.

{ "battery": 100, "battery_low": false, "energy": 660.83, "interface_mode": "electricity", "linkquality": 48, "power": 3234, "pulse_configuration": 1000, "voltage": 3100, "check_meter": null, "current_summation": null }

maybe do a reset of the frient and check you put the sensor very accurately over the pulse LED.

reset procedure is here https://help.frient.com/en/frient/electricity-meter-interface-2-led/t/cllmabm0o17vmkv0unrk3y34j

thanks, reset it to factory seems to have worked { "battery": 60, "battery_low": null, "current_summation": null, "energy": 65.54, "interface_mode": "electricity", "linkquality": 255, "power": 286, "pulse_configuration": 1000, "voltage": 2700, "check_meter": null }

PaJaSoft commented 5 months ago

Guys, is there a plan to create a new PR for this external converter to be a standard part of z2m converters?

In the default released version of z2m the EMIZB-141 is supported (recognized) but I'm lacking almost every setting (LED pulses EE meter) including resetting kWh summary and setting pulses value per kWh (in my case I need 500 pulses/kWh, factory default is 1000).

I've been using this external converted for awhile and it seems reliable and accurate as far as I can compare the measurement and stability.

So I'm asking is there something known what is blocking the release now? If necessary I would help with the PR.

akostamo commented 5 months ago

I think It's the lack of battery indication is this to be pulled into the official one, but.. no-one has time to get it right. :(

jllarraz commented 5 months ago

If the only thing missing is the battery level, I modified the converter to add the battery level. Here you have the new version emizb_141.txt please rename ".txt" to ".js" if you are going to use the file straight away

const exposes = require('zigbee-herdsman-converters/lib/exposes'); const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee}; const tz = require('zigbee-herdsman-converters/converters/toZigbee'); const constants = require('zigbee-herdsman-converters/lib/constants'); const reporting = require('zigbee-herdsman-converters/lib/reporting'); const globalStore = require('zigbee-herdsman-converters/lib/store'); const utils = require('zigbee-herdsman-converters/lib/utils'); const ota = require('zigbee-herdsman-converters/lib/ota'); const e = exposes.presets; const ea = exposes.access;

// develco specific cosntants const manufacturerOptions = {manufacturerCode: 0x1015};

/* MOSZB-1xx - ledControl - bitmap8 - r/w

// develco specific convertors const develco = { configure: { read_sw_hw_version: async (device, logger) => { for (const ep of device.endpoints) { if (ep.supportsInputCluster('genBasic')) { try { const data = await ep.read('genBasic', ['develcoPrimarySwVersion', 'develcoPrimaryHwVersion'], manufacturerOptions);

                    if (data.hasOwnProperty('develcoPrimarySwVersion')) {
                        device.softwareBuildID = data.develcoPrimarySwVersion.join('.');
                    }

                    if (data.hasOwnProperty('develcoPrimaryHwVersion')) {
                        device.hardwareVersion = data.develcoPrimaryHwVersion.join('.');
                    }
                } catch (error) {/* catch timeouts of sleeping devices */}
                break;
            }
        }
    },
},
fz: {
    // Some Develco devices report strange values sometimes
    // https://github.com/Koenkk/zigbee2mqtt/issues/13329
    electrical_measurement: {
        ...fz.electrical_measurement,
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.rmsVoltage !== 0xFFFF && msg.data.rmsCurrent !== 0xFFFF && msg.data.activePower !== -0x8000) {
                return fz.electrical_measurement.convert(model, msg, publish, options, meta);
            }
        },
    },
    device_temperature: {
        ...fz.device_temperature,
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.currentTemperature !== -0x8000) {
                return fz.device_temperature.convert(model, msg, publish, options, meta);
            }
        },
    },
    temperature: {
        ...fz.temperature,
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.measuredValue !== -0x8000 && msg.data.measuredValue !== 0xFFFF) {
                return fz.temperature.convert(model, msg, publish, options, meta);
            }
        },
    },
    metering: {
        ...fz.metering,
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.instantaneousDemand !== -0x800000) {
                return fz.metering.convert(model, msg, publish, options, meta);
            }
        },
    },
    pulse_configuration: {
        cluster: 'seMetering',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data.hasOwnProperty('develcoPulseConfiguration')) {
                result[utils.postfixWithEndpointName('pulse_configuration', msg, model, meta)] =
                    msg.data['develcoPulseConfiguration'];
            }

            return result;
        },
    },
    interface_mode: {
        cluster: 'seMetering',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data.hasOwnProperty('develcoInterfaceMode')) {
                result[utils.postfixWithEndpointName('interface_mode', msg, model, meta)] =
                    constants.develcoInterfaceMode.hasOwnProperty(msg.data['develcoInterfaceMode']) ?
                        constants.develcoInterfaceMode[msg.data['develcoInterfaceMode']] :
                        msg.data['develcoInterfaceMode'];
            }
            if (msg.data.hasOwnProperty('status')) {
                result['battery_low'] = (msg.data.status & 2) > 0;
                result['check_meter'] = (msg.data.status & 1) > 0;
            }

            return result;
        },
    },
    fault_status: {
        cluster: 'genBinaryInput',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data.hasOwnProperty('reliability')) {
                const lookup = {0: 'no_fault_detected', 7: 'unreliable_other', 8: 'process_error'};
                result.reliability = lookup[msg.data['reliability']];
            }
            if (msg.data.hasOwnProperty('statusFlags')) {
                result.fault = (msg.data['statusFlags']===1);
            }
            return result;
        },
    },
    voc: {
        cluster: 'develcoSpecificAirQuality',
        type: ['attributeReport', 'readResponse'],
        options: [exposes.options.precision('voc'), exposes.options.calibration('voc')],
        convert: (model, msg, publish, options, meta) => {
            // from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
            // "The mean molar mass of this mixture is 110 g/mol and hence,
            // 1 ppb TVOC corresponds to 4.5 μg/m3."
            const vocPpb = parseFloat(msg.data['measuredValue']);
            const voc = vocPpb * 4.5;
            const vocProperty = utils.postfixWithEndpointName('voc', msg, model, meta);

            // from aqszb-110-technical-manual-air-quality-sensor-04-08-20.pdf page 6, section 2.2 voc
            // this contains a ppb to level mapping table.
            let airQuality;
            const airQualityProperty = utils.postfixWithEndpointName('air_quality', msg, model, meta);
            if (vocPpb <= 65) {
                airQuality = 'excellent';
            } else if (vocPpb <= 220) {
                airQuality = 'good';
            } else if (vocPpb <= 660) {
                airQuality = 'moderate';
            } else if (vocPpb <= 2200) {
                airQuality = 'poor';
            } else if (vocPpb <= 5500) {
                airQuality = 'unhealthy';
            } else if (vocPpb > 5500) {
                airQuality = 'out_of_range';
            } else {
                airQuality = 'unknown';
            }
            return {[vocProperty]: utils.calibrateAndPrecisionRoundOptions(voc, options, 'voc'), [airQualityProperty]: airQuality};
        },
    },
    voc_battery: {
        cluster: 'genPowerCfg',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            /*
             * Per the technical documentation for AQSZB-110:
             * To detect low battery the system can monitor the "BatteryVoltage" by setting up a reporting interval of every 12 hour.
             * When a voltage of 2.5V is measured the battery should be replaced.
             * Low batt LED indication–RED LED will blink twice every 60 second.
             */
            const result = fz.battery.convert(model, msg, publish, options, meta);
            result.battery_low = (result.voltage <= 2500);
            return result;
        },
    },
    led_control: {
        cluster: 'genBasic',
        type: ['attributeReport', 'readResponse'],
        options: [],
        convert: (model, msg, publish, options, meta) => {
            const state = {};

            if (msg.data.hasOwnProperty('develcoLedControl')) {
                state['led_control'] = develcoLedControlMap[msg.data['develcoLedControl']];
            }

            return state;
        },
    },
    ias_occupancy_timeout: {
        cluster: 'ssIasZone',
        type: ['attributeReport', 'readResponse'],
        options: [],
        convert: (model, msg, publish, options, meta) => {
            const state = {};

            if (msg.data.hasOwnProperty('develcoAlarmOffDelay')) {
                state['occupancy_timeout'] = msg.data['develcoAlarmOffDelay'];
            }

            return state;
        },
    },
    input: {
        cluster: 'genBinaryInput',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data.hasOwnProperty('presentValue')) {
                const value = msg.data['presentValue'];
                result[utils.postfixWithEndpointName('input', msg, model, meta)] = value == 1;
            }
            return result;
        },
    },
},
tz: {
    pulse_configuration: {
        key: ['pulse_configuration'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('seMetering', {'develcoPulseConfiguration': value}, manufacturerOptions);
            return {readAfterWriteTime: 200, state: {'pulse_configuration': value}};
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('seMetering', ['develcoPulseConfiguration'], manufacturerOptions);
        },
    },
    interface_mode: {
        key: ['interface_mode'],
        convertSet: async (entity, key, value, meta) => {
            const payload = {'develcoInterfaceMode': utils.getKey(constants.develcoInterfaceMode, value, undefined, Number)};
            await entity.write('seMetering', payload, manufacturerOptions);
            return {readAfterWriteTime: 200, state: {'interface_mode': value}};
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('seMetering', ['develcoInterfaceMode'], manufacturerOptions);
        },
    },
    current_summation: {
        key: ['current_summation'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('seMetering', {'develcoCurrentSummation': value}, manufacturerOptions);
            return {state: {'current_summation': value}};
        },
    },
    led_control: {
        key: ['led_control'],
        convertSet: async (entity, key, value, meta) => {
            const ledControl = utils.getKey(develcoLedControlMap, value, value, Number);
            await entity.write('genBasic', {'develcoLedControl': ledControl}, manufacturerOptions);
            return {state: {led_control: value}};
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genBasic', ['develcoLedControl'], manufacturerOptions);
        },
    },
    ias_occupancy_timeout: {
        key: ['occupancy_timeout'],
        convertSet: async (entity, key, value, meta) => {
            let timeoutValue = value;
            if (timeoutValue < 20) {
                meta.logger.warn(`Minimum occupancy_timeout is 20, using 20 instead of ${timeoutValue}!`);
                timeoutValue = 20;
            }
            await entity.write('ssIasZone', {'develcoAlarmOffDelay': timeoutValue}, manufacturerOptions);
            return {state: {occupancy_timeout: timeoutValue}};
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('ssIasZone', ['develcoAlarmOffDelay'], manufacturerOptions);
        },
    },
    input: {
        key: ['input'],
        convertGet: async (entity, key, meta) => {
            await entity.read('genBinaryInput', ['presentValue']);
        },
    },
},

};

const definition = { zigbeeModel: ['EMIZB-141'], // The model ID from: Device with modelID 'lumi.sens' is not supported. model: 'EMIZB-141', // Vendor model number, look on the device for a model number vendor: 'frient A/S', // Vendor of the device (only used for documentation and startup logging) description: 'frient Powermeter', // Description of the device, copy from vendor site. (only used for documentation and startup logging) fromZigbee: [develco.fz.metering, develco.fz.pulse_configuration, develco.fz.interface_mode, develco.fz.battery], toZigbee: [develco.tz.pulse_configuration, develco.tz.interface_mode, develco.tz.current_summation], endpoint: (device) => { return {'default': 2}; }, configure: async (device, coordinatorEndpoint, logger) => { const endpoint = device.getEndpoint(2); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.instantaneousDemand(endpoint); await reporting.readMeteringMultiplierDivisor(endpoint); }, exposes: [ e.power(), e.energy(), e.battery_low(), e.battery(), exposes.numeric('pulse_configuration', ea.ALL).withValueMin(0).withValueMax(65535) .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'), exposes.enum('interface_mode', ea.ALL, ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0']) .withDescription('Operating mode/probe'), exposes.numeric('current_summation', ea.SET) .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0) .withValueMax(268435455), exposes.binary('check_meter', ea.STATE, true, false) .withDescription('Is true if communication problem with meter is experienced'), ], };

module.exports = definition;

skonteron commented 5 months ago

Maybe someone here can help. I have created the file emizb_141.js in the directory /homeassistant/zigbee2mqtt. Then I integrated the emizb_141.js file using Zigbee2MQTT -> Settings -> external converter.

2024-02-09_16h39_22 2024-02-09_16h38_55

Unfortunately, I still do not get any energy displayed. After inserting the json file I reset the frient.

2024-02-09_16h39_58 2024-02-09_16h40_08

What am I doing wrong?

jllarraz commented 5 months ago

Did you click on submit?

PaJaSoft commented 5 months ago

Did you restart z2m instance?

skonteron commented 5 months ago

I have restarted both z2m and HA. After the restart my js file was still there

nivek1612 commented 5 months ago

Try the original convertor at the start of this thread

skonteron commented 5 months ago

Perfect :) with the orginal convertor it's working. I get this now. I think thats it. Thank you so much for your help

"battery": 100, "battery_low": false, "energy": 3.18, "linkquality": 15, "power": 452, "voltage": 3000, "check_meter": null, "current_summation": 138329565, "interface_mode": "electricity", "pulse_configuration": null

PaJaSoft commented 5 months ago

@skonteron Check if your elmeter uses 1000 impulses/1kWh ("pulse_configuration": null) or set it up correctly...

skonteron commented 5 months ago

Thx for the advice. It should be 10.000. I will try

penroseg commented 5 months ago

@jllarraz that new version with the Battery didn't work for me unfortunately. I did get Battery level displayed but anything after that in the convertor file is no longer being made available. I have been using the original convertor since October and those additional fields were available so I think there may be a bug in the code somewhere but I'm not able to correct it. I did have a quick try at it but got an invalid convertor message in Z2M so gave up before breaking my integration totally.

I can see those are in your latest version too but for some reason are not now being integrated.

exposes: [ e.power(), e.energy(), e.battery_low(), e.battery(), exposes.numeric('_pulse_configuration_', ea.ALL).withValueMin(0).withValueMax(65535) .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'), exposes.enum(_'interface_mode_', ea.ALL, ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0']) .withDescription('Operating mode/probe'), exposes.numeric('current_summation', ea.SET) .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0) .withValueMax(268435455), exposes.binary('check_meter', ea.STATE, true, false) .withDescription('Is true if communication problem with meter is experienced'), ],

Using the newer connector the shorter list is exposed. image

Using the original connector the longer list is available, including the Pulse Configuration entity. image

jllarraz commented 5 months ago

Apologies for that @penroseg I left a bit of code on that adapter that shouldnt be there.

this is the screenshoot of my Adapter

Screenshot 2024-02-10 at 20 30 32

The modification of the original adapter (the first one on this thread) is as simple as changing this

exposes: [ e.power(), e.energy(), e.battery_low(), exposes.numeric('pulse_configuration', ea.ALL).withValueMin(0).withValueMax(65535) .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'), exposes.enum('interface_mode', ea.ALL, ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0']) .withDescription('Operating mode/probe'), exposes.numeric('current_summation', ea.SET) .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0) .withValueMax(268435455), exposes.binary('check_meter', ea.STATE, true, false) .withDescription('Is true if communication problem with meter is experienced'), ] to this (just add e.battery()) exposes: [ e.power(), e.energy(), e.battery_low(), e.battery(), exposes.numeric('pulse_configuration', ea.ALL).withValueMin(0).withValueMax(65535) .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'), exposes.enum('interface_mode', ea.ALL, ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0']) .withDescription('Operating mode/probe'), exposes.numeric('current_summation', ea.SET) .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0) .withValueMax(268435455), exposes.binary('check_meter', ea.STATE, true, false) .withDescription('Is true if communication problem with meter is experienced'), ]

And this is the file (hopefully this time is right for everyone) emizb_141.txt

Again just change "txt" to "js"

PaJaSoft commented 5 months ago

@jllarraz Your last version is working fine however I still can't see a value of reported battery in percents. There is still NULL although the uninterrupted run of the whole system (-> Frient ZED, Coordinator and z2m) is 24h+ long for sure. I'm not sure if the issue is on Frient side (it didn't report this value yet) or in the implementation of messages handling.

jllarraz commented 5 months ago

@PaJaSoft you should be able to see the logs of the communication and filter by "Frient" or whatever name you gave it

in my case it looks like Info 2024-02-12 12:54:51MQTT publish: topic 'zigbee2mqtt/Energy Metter Frient', payload '{"battery":60,"battery_low":false,"check_meter":null,"current_summation":3176500,"energy":308.92,"interface_mode":"electricity","linkquality":255,"power":1715,"pulse_configuration":1000,"voltage":2600}'

PaJaSoft commented 5 months ago

@jllarraz Yea, I know how to search for this however I would rather looking how looks the LOG message when ZED reports a battery level to coordinator. I mean sth similar like for Energy: debug 2024-02-12 14:17:12: Received Zigbee message from 'XXX', type 'attributeReport', cluster 'seMetering', data '{"currentSummDelivered":[0,224482],"instantaneousDemand":804}' from endpoint 2 with groupID 0

penroseg commented 5 months ago

@jllarraz Thats newest version is working perfectly. Many thanks for your help. Am seeing battery percentage straight away and all the original fields are still there. Much appreciated. !!!

nivek1612 commented 5 months ago

just switched to the newer convertor. Seems to work ok. But my battery is still reading 100% but the device is new so maybe that's accurate but I would have expected it to read around 98% or similar after 8 weeks.

{ "battery": 100, "battery_low": false, "energy": 1462.53, "interface_mode": "electricity", "linkquality": 51, "power": 1553, "pulse_configuration": 1000, "voltage": 3100, "check_meter": null, "current_summation": null }