Skip to content

Enhance features for EMIZB-151 device#10458

Merged
Koenkk merged 14 commits intoKoenkk:masterfrom
Fabiancrg:patch-1
Oct 26, 2025
Merged

Enhance features for EMIZB-151 device#10458
Koenkk merged 14 commits intoKoenkk:masterfrom
Fabiancrg:patch-1

Conversation

@Fabiancrg
Copy link
Copy Markdown
Contributor

This is related to new converter file for the existing Frient EMIZB-151 device. Please see the issue bellow with new converter and details about the update: #10457

This is related to new converter file for the existing Frient EMIZB-151 device.
Please see the issue bellow with new converter and details about the update:
Koenkk#10457
@Fabiancrg
Copy link
Copy Markdown
Contributor Author

I hope I did this correctly, I don't really want to change the description of the device but change the converter linked to it, please check #10457

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Oct 22, 2025

Could you update this PR with the ext converter?

@Fabiancrg
Copy link
Copy Markdown
Contributor Author

Here is the converter, as explained in #10457, this is an update of an existing converter, here are the updates:

  • The existing converter is only reporting, for the energy part, the total consummed energy (currentSummDelivered) as Energy without distinction between peak (currentTier1*) and off peak (currentTier2*). It's not reporting the energy returned to the grid (currentSummDelivered) (e.g.: solar production).
  • This new converter gives the total, peak and off peak for the energy consummed and returned to the grid in energy_consumed, energy_consumed_tariff1, energy_consumed_tariff2, energy_produced, energy_produced_tariff1 and energy_produced_tariff2
  • The existing converter is giving info related to phase A as power, voltage and current while for the other phases we have voltage_phase_b, current_phase_b,... so I added volatge_phase_a, current_phase_a and power_phase_a.
  • I added also power_total, it is the sum of power_phase_a, power_phase_b and power_phase_c (positive when power is consummed and negative when it is returned to the grid)
  • Existing names (power, voltage, current and energy) were kept for backward compatibility.
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 ota = require('zigbee-herdsman-converters/lib/ota');
const e = exposes.presets;
const ea = exposes.access;

const definition = {
  zigbeeModel: ['EMIZB-151'],
  model: 'EMIZB-151',
  vendor: 'Frient',
  description: 'Electricity Meter Interface 2 P1',

  fromZigbee: [
    {
      cluster: 'haElectricalMeasurement',
      type: ['attributeReport', 'readResponse'],
      convert: (model, msg, publish, options, meta) => {
        const result = {};

        // Power - Apply multiplier/divisor scaling
        const getPowerFactor = () => {
          const multiplier = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acPowerMultiplier');
          const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acPowerDivisor');
          return multiplier && divisor ? multiplier / divisor : 1;
        };
        
        const powerFactor = getPowerFactor();
        
        if ('activePower' in msg.data) {
          let power = msg.data.activePower * powerFactor;
          
          // Apply power calibration if set
          const calibration = meta.device.meta?.power_calibration || 0;
          if (calibration !== 0) {
            power = power * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.power_precision ?? 1;
          power = Number(power.toFixed(precision));
          
          result.power = power;
          result.power_phase_a = power;
        }
        if ('activePowerPhB' in msg.data) {
          let power = msg.data.activePowerPhB * powerFactor;
          
          // Apply power phase B calibration if set
          const calibration = meta.device.meta?.power_phase_b_calibration || 0;
          if (calibration !== 0) {
            power = power * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.power_phase_b_precision ?? 1;
          power = Number(power.toFixed(precision));
          
          result.power_phase_b = power;
        }
        if ('activePowerPhC' in msg.data) {
          let power = msg.data.activePowerPhC * powerFactor;
          
          // Apply power phase C calibration if set
          const calibration = meta.device.meta?.power_phase_c_calibration || 0;
          if (calibration !== 0) {
            power = power * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.power_phase_c_precision ?? 1;
          power = Number(power.toFixed(precision));
          
          result.power_phase_c = power;
        }

        // Calculate power_total using current values and previous state
        // This ensures we use values from previous messages if not in current message
        const phaseA = result.power_phase_a ?? meta.state.power_phase_a ?? 0;
        const phaseB = result.power_phase_b ?? meta.state.power_phase_b ?? 0;
        const phaseC = result.power_phase_c ?? meta.state.power_phase_c ?? 0;
        result.power_total = phaseA + phaseB + phaseC;

        // Voltage - Apply multiplier/divisor scaling
        const getVoltageFactor = () => {
          const multiplier = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acVoltageMultiplier');
          const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acVoltageDivisor');
          return multiplier && divisor ? multiplier / divisor : 0.1; // Default to /10 if not available
        };
        
        const voltageFactor = getVoltageFactor();
        
        if ('rmsVoltage' in msg.data) {
          let voltage = msg.data.rmsVoltage * voltageFactor;
          
          // Apply voltage calibration if set
          const calibration = meta.device.meta?.voltage_calibration || 0;
          if (calibration !== 0) {
            voltage = voltage * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.voltage_precision ?? 1;
          voltage = Number(voltage.toFixed(precision));
          
          result.voltage = voltage;
          result.voltage_phase_a = voltage;
        }
        if ('rmsVoltagePhB' in msg.data) {
          let voltage = msg.data.rmsVoltagePhB * voltageFactor;
          
          // Apply voltage phase B calibration if set
          const calibration = meta.device.meta?.voltage_phase_b_calibration || 0;
          if (calibration !== 0) {
            voltage = voltage * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.voltage_phase_b_precision ?? 1;
          voltage = Number(voltage.toFixed(precision));
          
          result.voltage_phase_b = voltage;
        }
        if ('rmsVoltagePhC' in msg.data) {
          let voltage = msg.data.rmsVoltagePhC * voltageFactor;
          
          // Apply voltage phase C calibration if set
          const calibration = meta.device.meta?.voltage_phase_c_calibration || 0;
          if (calibration !== 0) {
            voltage = voltage * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.voltage_phase_c_precision ?? 1;
          voltage = Number(voltage.toFixed(precision));
          
          result.voltage_phase_c = voltage;
        }

        // Current - Apply multiplier/divisor scaling
        const getCurrentFactor = () => {
          const multiplier = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acCurrentMultiplier');
          const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acCurrentDivisor');
          return multiplier && divisor ? multiplier / divisor : 0.01; // Default to /100 if not available
        };
        
        const currentFactor = getCurrentFactor();
        
        if ('rmsCurrent' in msg.data) {
          let current = msg.data.rmsCurrent * currentFactor;
          
          // Apply current calibration if set
          const calibration = meta.device.meta?.current_calibration || 0;
          if (calibration !== 0) {
            current = current * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.current_precision ?? 2;
          current = Number(current.toFixed(precision));
          
          result.current = current;
          result.current_phase_a = current;
        }
        if ('rmsCurrentPhB' in msg.data) {
          let current = msg.data.rmsCurrentPhB * currentFactor;
          
          // Apply current phase B calibration if set
          const calibration = meta.device.meta?.current_phase_b_calibration || 0;
          if (calibration !== 0) {
            current = current * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.current_phase_b_precision ?? 2;
          current = Number(current.toFixed(precision));
          
          result.current_phase_b = current;
        }
        if ('rmsCurrentPhC' in msg.data) {
          let current = msg.data.rmsCurrentPhC * currentFactor;
          
          // Apply current phase C calibration if set
          const calibration = meta.device.meta?.current_phase_c_calibration || 0;
          if (calibration !== 0) {
            current = current * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.current_phase_c_precision ?? 2;
          current = Number(current.toFixed(precision));
          
          result.current_phase_c = current;
        }
        if ('rmsCurrentN' in msg.data) {
          let current = msg.data.rmsCurrentN * currentFactor;
          
          // Apply current neutral calibration if set
          const calibration = meta.device.meta?.current_neutral_calibration || 0;
          if (calibration !== 0) {
            current = current * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.current_neutral_precision ?? 2;
          current = Number(current.toFixed(precision));
          
          result.current_neutral = current;
        }

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

        if ('currentSummDelivered' in msg.data) {
          let energy = msg.data['currentSummDelivered'] / 1000;
          
          // Apply energy calibration if set
          const calibration = meta.device.meta?.energy_calibration || 0;
          if (calibration !== 0) {
            energy = energy * (1 + calibration / 100);
          }
          
          // Apply precision rounding
          const precision = meta.device.meta?.energy_precision ?? 0;
          const rounded = Number(energy.toFixed(precision));
          
          result.energy = rounded;
          result.energy_consumed = rounded;
        }
        if ('currentSummReceived' in msg.data) {
          result.energy_produced = Math.round(msg.data['currentSummReceived'] / 1000);
        }

        if ('currentTier1SummDelivered' in msg.data) {
          result.energy_consumed_tariff1 = Math.round(msg.data['currentTier1SummDelivered'] / 1000);
        }
        if ('currentTier2SummDelivered' in msg.data) {
          result.energy_consumed_tariff2 = Math.round(msg.data['currentTier2SummDelivered'] / 1000);
        }
        if ('currentTier1SummReceived' in msg.data) {
          result.energy_produced_tariff1 = Math.round(msg.data['currentTier1SummReceived'] / 1000);
        }
        if ('currentTier2SummReceived' in msg.data) {
          result.energy_produced_tariff2 = Math.round(msg.data['currentTier2SummReceived'] / 1000);
        }

        return result;
      },
    }
  ],

  toZigbee: [
    {
      key: [
        'power_calibration', 'power_precision',
        'voltage_calibration', 'voltage_precision', 
        'current_calibration', 'current_precision',
        'energy_calibration', 'energy_precision',
        'power_phase_b_calibration', 'power_phase_b_precision',
        'power_phase_c_calibration', 'power_phase_c_precision',
        'voltage_phase_b_calibration', 'voltage_phase_b_precision',
        'voltage_phase_c_calibration', 'voltage_phase_c_precision',
        'current_phase_b_calibration', 'current_phase_b_precision',
        'current_phase_c_calibration', 'current_phase_c_precision',
        'current_neutral_calibration', 'current_neutral_precision'
      ],
      convertSet: async (entity, key, value, meta) => {
        // Validate calibration inputs (-50 to +50%)
        if (key.includes('_calibration')) {
          if (typeof value !== 'number' || value < -50 || value > 50) {
            throw new Error(`${key} must be a number between -50 and +50 (percentage)`);
          }
        }
        
        // Validate precision inputs (0 to 5 decimal places)
        if (key.includes('_precision')) {
          if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 5) {
            throw new Error(`${key} must be an integer between 0 and 5 (decimal places)`);
          }
        }
        
        // Store the value in device metadata
        meta.device.meta = meta.device.meta || {};
        meta.device.meta[key] = value;
        meta.device.save();
        
        return {state: {[key]: value}};
      },
      convertGet: async (entity, key, meta) => {
        // Return current value or default
        let defaultValue = 0;
        
        // Set defaults for precision based on measurement type
        if (key.includes('_precision')) {
          if (key.includes('power')) defaultValue = 1;      // 1 decimal for power (W)
          else if (key.includes('voltage')) defaultValue = 1; // 1 decimal for voltage (V)
          else if (key.includes('current')) defaultValue = 2; // 2 decimals for current (A)
          else if (key.includes('energy')) defaultValue = 0;  // 0 decimals for energy (kWh)
        }
        
        const value = meta.device.meta?.[key] ?? defaultValue;
        return {state: {[key]: value}};
      },
    },
  ],

  exposes: [
    e.power(),
    e.voltage(),
    e.current(),

    e.numeric('power_phase_a', ea.STATE).withUnit('W').withDescription('Instantaneous measured power on Phase A'),
    e.power_phase_b(),
    e.power_phase_c(),
    e.numeric('power_total', ea.STATE).withUnit('W').withDescription('Total instantaneous measured power (sum of all phases)'),

    e.numeric('voltage_phase_a', ea.STATE).withUnit('V').withDescription('Measured electrical potential on Phase A'),
    e.voltage_phase_b(),
    e.voltage_phase_c(),

    e.numeric('current_phase_a', ea.STATE).withUnit('A').withDescription('Instantaneous measured current on Phase A'),
    e.current_phase_b(),
    e.current_phase_c(),
    e.numeric('current_neutral', ea.STATE).withUnit('A').withDescription('Instantaneous measured current on Neutral'),

    e.energy(),
    e.numeric('energy_consumed', ea.STATE).withUnit('kWh').withDescription('Total energy consumed from the grid - OBIS 1.8.0'),
    e.numeric('energy_produced', ea.STATE).withUnit('kWh').withDescription('Total energy returned to the grid - OBIS 2.8.0'),

    e.numeric('energy_consumed_tariff1', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 1 (peak/high) - OBIS 1.8.1'),
    e.numeric('energy_consumed_tariff2', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 2 (off-peak/low) - OBIS 1.8.2'),
    e.numeric('energy_produced_tariff1', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 1 (peak/high) - OBIS 2.8.1'),
    e.numeric('energy_produced_tariff2', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 2 (off-peak/low) - OBIS 2.8.2'),
    
    // Calibration and precision settings
    e.numeric('power_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Power calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('power_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for power readings (0-5)'),
      
    e.numeric('voltage_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Voltage calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('voltage_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for voltage readings (0-5)'),
      
    e.numeric('current_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Current calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('current_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for current readings (0-5)'),
      
    e.numeric('energy_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Energy calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('energy_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for energy readings (0-5)'),
      
    // Phase B calibration and precision
    e.numeric('power_phase_b_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Power Phase B calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('power_phase_b_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Power Phase B readings (0-5)'),
      
    e.numeric('power_phase_c_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Power Phase C calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('power_phase_c_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Power Phase C readings (0-5)'),
      
    e.numeric('voltage_phase_b_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Voltage Phase B calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('voltage_phase_b_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Voltage Phase B readings (0-5)'),
      
    e.numeric('voltage_phase_c_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Voltage Phase C calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('voltage_phase_c_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Voltage Phase C readings (0-5)'),
      
    e.numeric('current_phase_b_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Current Phase B calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('current_phase_b_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Current Phase B readings (0-5)'),
      
    e.numeric('current_phase_c_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Current Phase C calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('current_phase_c_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Current Phase C readings (0-5)'),
      
    e.numeric('current_neutral_calibration', ea.ALL).withUnit('%').withValueMin(-50).withValueMax(50).withValueStep(0.1)
      .withDescription('Current Neutral calibration adjustment in percentage (-50% to +50%)'),
    e.numeric('current_neutral_precision', ea.ALL).withValueMin(0).withValueMax(5).withValueStep(1)
      .withDescription('Number of decimal places for Current Neutral readings (0-5)'),
  ],

  ota: ota.zigbeeOTA,

  endpoint: (device) => ({default: 2}),

  configure: async (device, coordinatorEndpoint, logger) => {
    const endpoint = device.getEndpoint(2);

    // Bind clusters - this is usually important
    try {
      await reporting.bind(endpoint, coordinatorEndpoint, ['haElectricalMeasurement', 'seMetering']);
    } catch (error) {
      // Binding can fail but device may still work
    }

    // Configure electrical measurement reporting
    await reporting.readEletricalMeasurementMultiplierDivisors(endpoint).catch(() => {});
    
    // Configure all electrical measurement attributes with reasonable intervals
    const electricalConfig = [
      { attr: 'activePower', change: 25 },          // Phase A power - report when changes by 25W
      { attr: 'activePowerPhB', change: 25 },       // Phase B power
      { attr: 'activePowerPhC', change: 25 },       // Phase C power
      { attr: 'rmsVoltage', change: 10 },           // Phase A voltage - report when changes by 1V (scaled by 10)
      { attr: 'rmsVoltagePhB', change: 10 },        // Phase B voltage
      { attr: 'rmsVoltagePhC', change: 10 },        // Phase C voltage
      { attr: 'rmsCurrent', change: 50 },           // Phase A current - report when changes by 0.5A (scaled by 100)
      { attr: 'rmsCurrentPhB', change: 50 },        // Phase B current
      { attr: 'rmsCurrentPhC', change: 50 },        // Phase C current
      { attr: 'rmsCurrentN', change: 50 }           // Neutral current - report when changes by 0.5A (scaled by 100)
    ];
    
    for (const config of electricalConfig) {
      await endpoint.configureReporting('haElectricalMeasurement', [{
        attribute: config.attr,
        minimumReportInterval: 10,      // Don't report more often than every 10 seconds
        maximumReportInterval: 300,     // Report at least every 5 minutes even if no change
        reportableChange: config.change  // Only report when value changes significantly
      }]).catch(() => {});
    }

    // Read metering multiplier/divisor
    await reporting.readMeteringMultiplierDivisor(endpoint).catch(() => {});

    // Configure metering attributes
    const meteringAttributes = [
      'currentSummDelivered',
      'currentSummReceived',
      'currentTier1SummDelivered',
      'currentTier2SummDelivered',
      'currentTier1SummReceived',
      'currentTier2SummReceived'
    ];

    await endpoint.read('seMetering', meteringAttributes).catch(() => {});

    for (const attr of meteringAttributes) {
      await endpoint.configureReporting('seMetering', [{
        attribute: attr,
        minimumReportInterval: 300,      // Don't report more often than every 5 minutes
        maximumReportInterval: 3600,     // Report at least every hour
        reportableChange: 1000            // Report when energy changes by 1 kWh
      }]).catch(() => {});
    }
  },
};

module.exports = definition;

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Oct 23, 2025

It's quite a lot of code, can we merge it with fz.electrical_measurement?

@Fabiancrg
Copy link
Copy Markdown
Contributor Author

I shrinked it down to this by using m.electricityMeter, I will check if it can be merged with fz.electrical_measurement

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const m = require('zigbee-herdsman-converters/lib/modernExtend');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const e = exposes.presets;
const ea = exposes.access;

const definition = {
  zigbeeModel: ['EMIZB-151'],
  model: 'EMIZB-151',
  vendor: 'Frient',
  description: 'Electricity Meter Interface 2 P1',

  // Use the official modern extend for three-phase electricity meter (includes all calibration and precision)
  extend: [m.electricityMeter({threePhase: true})],

  // Add additional seMetering functionality for energy tariffs
  fromZigbee: [
    {
      cluster: 'seMetering',
      type: ['attributeReport', 'readResponse'],
      convert: (model, msg, publish, options, meta) => {
        const result = {};

        // Additional energy measurements for tariffs (not covered by standard electricityMeter)
        if ('currentSummReceived' in msg.data) {
          result.energy_produced = Math.round(msg.data['currentSummReceived'] / 1000);
        }

        if ('currentTier1SummDelivered' in msg.data) {
          result.energy_consumed_tariff1 = Math.round(msg.data['currentTier1SummDelivered'] / 1000);
        }
        if ('currentTier2SummDelivered' in msg.data) {
          result.energy_consumed_tariff2 = Math.round(msg.data['currentTier2SummDelivered'] / 1000);
        }
        if ('currentTier1SummReceived' in msg.data) {
          result.energy_produced_tariff1 = Math.round(msg.data['currentTier1SummReceived'] / 1000);
        }
        if ('currentTier2SummReceived' in msg.data) {
          result.energy_produced_tariff2 = Math.round(msg.data['currentTier2SummReceived'] / 1000);
        }

        return result;
      },
    }
  ],

  // Additional exposes for the extra seMetering attributes not covered by electricityMeter
  exposes: [
    e.numeric('energy_produced', ea.STATE).withUnit('kWh').withDescription('Total energy returned to the grid - OBIS 2.8.0'),
    e.numeric('energy_consumed_tariff1', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 1 (peak/high) - OBIS 1.8.1'),
    e.numeric('energy_consumed_tariff2', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 2 (off-peak/low) - OBIS 1.8.2'),
    e.numeric('energy_produced_tariff1', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 1 (peak/high) - OBIS 2.8.1'),
    e.numeric('energy_produced_tariff2', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 2 (off-peak/low) - OBIS 2.8.2'),
  ],

  ota: true,
  endpoint: (device) => ({default: 2}),

  configure: async (device, coordinatorEndpoint, logger) => {
    const endpoint = device.getEndpoint(2);

    // Bind seMetering cluster for the additional energy tariff measurements
    try {
      await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']);
    } catch (error) {
      // Binding can fail but device may still work
    }

    // Read metering multiplier/divisor
    await reporting.readMeteringMultiplierDivisor(endpoint).catch(() => {});

    // Configure the additional seMetering attributes for energy tariffs
    const meteringAttributes = [
      'currentSummReceived',
      'currentTier1SummDelivered',
      'currentTier2SummDelivered',
      'currentTier1SummReceived',
      'currentTier2SummReceived'
    ];

    // Read the attributes initially
    await endpoint.read('seMetering', meteringAttributes).catch(() => {});

    // Configure reporting for energy tariff attributes
    for (const attr of meteringAttributes) {
      await endpoint.configureReporting('seMetering', [{
        attribute: attr,
        minimumReportInterval: 300,      // Don't report more often than every 5 minutes
        maximumReportInterval: 3600,     // Report at least every hour
        reportableChange: 1000            // Report when energy changes by 1 kWh
      }]).catch(() => {});
    }
  },
};

module.exports = definition;

@Fabiancrg
Copy link
Copy Markdown
Contributor Author

@Koenkk, fz.electrical_measurement handles cluster haElectricalMeasurement but the part I would like to add in the existing converter is related to the cluster seMetering.
Maybe we can merge the new part with fz.metering, I will try that and come back to you.

@Fabiancrg
Copy link
Copy Markdown
Contributor Author

Ok, I tested a new converter (see bellow) with and updated fz.metering, I added this:

        // Support for tariff-based energy measurements (e.g., P1/OBIS smart meters)
        if (msg.data.currentTier1SummDelivered !== undefined) {
            const value = msg.data.currentTier1SummDelivered;
            const property = postfixWithEndpointName("energy_tier1", msg, model, meta);
            payload[property] = value * (factor ?? 1);
        }
        if (msg.data.currentTier2SummDelivered !== undefined) {
            const value = msg.data.currentTier2SummDelivered;
            const property = postfixWithEndpointName("energy_tier2", msg, model, meta);
            payload[property] = value * (factor ?? 1);
        }
        if (msg.data.currentTier1SummReceived !== undefined) {
            const value = msg.data.currentTier1SummReceived;
            const property = postfixWithEndpointName("produced_energy_tier1", msg, model, meta);
            payload[property] = value * (factor ?? 1);
        }
        if (msg.data.currentTier2SummReceived !== undefined) {
            const value = msg.data.currentTier2SummReceived;
            const property = postfixWithEndpointName("produced_energy_tier2", msg, model, meta);
            payload[property] = value * (factor ?? 1);
        }

The converter is now:

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const m = require('zigbee-herdsman-converters/lib/modernExtend');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const e = exposes.presets;
const ea = exposes.access;

const definition = {
  zigbeeModel: ['EMIZB-151'],
  model: 'EMIZB-151',
  vendor: 'Frient',
  description: 'Electricity Meter Interface 2 P1',

  // Use modern extend for three-phase electricity meter + standard metering converter
  extend: [m.electricityMeter({threePhase: true})],
  
  // Add standard fz.metering for energy and tariff data
  fromZigbee: [fz.metering],

  // Add exposes for the seMetering attributes from fz.metering
  exposes: [
    e.numeric('energy_tier1', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 1 (peak/high) - OBIS 1.8.1'),
    e.numeric('energy_tier2', ea.STATE).withUnit('kWh').withDescription('Energy consumed in tariff 2 (off-peak/low) - OBIS 1.8.2'),
    e.numeric('produced_energy', ea.STATE).withUnit('kWh').withDescription('Total energy returned to the grid - OBIS 2.8.0'),
    e.numeric('produced_energy_tier1', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 1 (peak/high) - OBIS 2.8.1'),
    e.numeric('produced_energy_tier2', ea.STATE).withUnit('kWh').withDescription('Energy produced in tariff 2 (off-peak/low) - OBIS 2.8.2'),
  ],

  ota: true,
  endpoint: (device) => ({default: 2}),

  configure: async (device, coordinatorEndpoint, logger) => {
    const endpoint = device.getEndpoint(2);

    // Bind seMetering cluster
    try {
      await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']);
    } catch (error) {
      // Binding can fail but device may still work
    }

    // Read metering multiplier/divisor (required by fz.metering)
    await reporting.readMeteringMultiplierDivisor(endpoint).catch(() => {});

    // Configure reporting for all energy attributes including tariffs
    const meteringAttributes = [
      'instantaneousDemand',        // power
      'currentSummDelivered',       // total energy consumed
      'currentSummReceived',        // total energy produced
      'currentTier1SummDelivered',  // energy consumed tariff 1 (OBIS 1.8.1)
      'currentTier2SummDelivered',  // energy consumed tariff 2 (OBIS 1.8.2)
      'currentTier1SummReceived',   // energy produced tariff 1 (OBIS 2.8.1)
      'currentTier2SummReceived'    // energy produced tariff 2 (OBIS 2.8.2)
    ];

    // Read the attributes initially
    await endpoint.read('seMetering', meteringAttributes).catch(() => {});

    // Configure reporting for all metering attributes
    for (const attr of meteringAttributes) {
      await endpoint.configureReporting('seMetering', [{
        attribute: attr,
        minimumReportInterval: 300,      // Don't report more often than every 5 minutes
        maximumReportInterval: 3600,     // Report at least every hour
        reportableChange: 1000            // Report when energy changes by 1 kWh
      }]).catch(() => {});
    }
  },
};

module.exports = definition;

Should I create another another PR for the update of fz.metering ?

@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Oct 24, 2025

Should I create another another PR for the update of fz.metering ?

Can be updated in this PR.

Added support for tariff-based energy measurements including tiered energy delivered and received.
Copy link
Copy Markdown
Contributor Author

@Fabiancrg Fabiancrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Koenkk here is the updates for the converter and metering, hope I did it correctly.
I have to say it's the first time I update something so I hope I did it right.

Copy link
Copy Markdown
Contributor Author

@Fabiancrg Fabiancrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed " }, "

Removed try-catch block around reporting.bind call.
Copy link
Copy Markdown
Contributor Author

@Fabiancrg Fabiancrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope I did it correctly now

Comment thread src/converters/fromZigbee.ts Outdated
}
if (msg.data.currentTier2SummReceived !== undefined) {
const value = msg.data.currentTier2SummReceived;
const property = postfixWithEndpointName("produced_energy_tier2", msg, model, meta);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const property = postfixWithEndpointName("produced_energy_tier2", msg, model, meta);
const property = postfixWithEndpointName("produced_energy_tier_2", msg, model, meta);

(make same change for the others)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poperty fields updated

Comment thread src/devices/frient.ts Outdated

extend: [m.electricityMeter({threePhase: true})],

fromZigbee: [fz.metering],
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be needed, added by m.electricityMeter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, removed

Comment thread src/devices/frient.ts Outdated

await reporting.bind(endpoint, coordinatorEndpoint, ["seMetering"]);

await reporting.readMeteringMultiplierDivisor(endpoint).catch(() => {});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you integrate this into electricityMeter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did an attempt, hope it's good

Fabiancrg and others added 4 commits October 25, 2025 20:20
Copy link
Copy Markdown
Contributor Author

@Fabiancrg Fabiancrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Koenkk I reviewed your comments and updated the required files

Comment thread src/converters/fromZigbee.ts Outdated
}
if (msg.data.currentTier2SummReceived !== undefined) {
const value = msg.data.currentTier2SummReceived;
const property = postfixWithEndpointName("produced_energy_tier2", msg, model, meta);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poperty fields updated

Comment thread src/devices/frient.ts Outdated

extend: [m.electricityMeter({threePhase: true})],

fromZigbee: [fz.metering],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, removed

Comment thread src/devices/frient.ts Outdated

await reporting.bind(endpoint, coordinatorEndpoint, ["seMetering"]);

await reporting.readMeteringMultiplierDivisor(endpoint).catch(() => {});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did an attempt, hope it's good

Copy link
Copy Markdown
Contributor Author

@Fabiancrg Fabiancrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix syntax error

@Koenkk Koenkk merged commit 02c20ae into Koenkk:master Oct 26, 2025
3 checks passed
@Koenkk
Copy link
Copy Markdown
Owner

Koenkk commented Oct 26, 2025

Thanks!

@AJFproduction
Copy link
Copy Markdown

Looking forward to this update. Many thanks for your work.

jacky202509 pushed a commit to jacky202509/zigbee-herdsman-converters that referenced this pull request Nov 13, 2025
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants