Skip to content

WWSTCERT-10225/10228/10231/10234 add driver to frient EMI devices#2730

Open
marcintyminski wants to merge 4 commits intoSmartThingsCommunity:mainfrom
marcintyminski:Add-support-to-frient-EMIs
Open

WWSTCERT-10225/10228/10231/10234 add driver to frient EMI devices#2730
marcintyminski wants to merge 4 commits intoSmartThingsCommunity:mainfrom
marcintyminski:Add-support-to-frient-EMIs

Conversation

@marcintyminski
Copy link
Contributor

Check all that apply

Type of Change

  • WWST Certification Request
    • If this is your first time contributing code:
      • I have reviewed the README.md file
      • I have reviewed the CODE_OF_CONDUCT.md file
      • I have signed the CLA
    • I plan on entering a WWST Certification Request or have entered a request through the WWST Certification console at developer.smartthings.com
  • Bug fix
  • New feature
  • Refactor

Checklist

  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas
  • I have verified my changes by testing with a device or have communicated a plan for testing
  • I am adding new behavior, such as adding a sub-driver, and have added and run new unit tests to cover the new behavior

Description of Change

Summary of Completed Tests

@github-actions
Copy link

Duplicate profile check: Passed - no duplicate profiles detected.

@github-actions
Copy link

@github-actions
Copy link

github-actions bot commented Jan 26, 2026

Test Results

   71 files    488 suites   0s ⏱️
2 523 tests 2 523 ✅ 0 💤 0 ❌
4 349 runs  4 349 ✅ 0 💤 0 ❌

Results for commit 9b2dc97.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Jan 26, 2026

File Coverage
All files 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua 93%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua 95%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/init.lua 97%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/configurations.lua 95%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua 57%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua 91%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua 95%

Minimum allowed coverage is 90%

Generated by 🐒 cobertura-action against 9b2dc97

@marcintyminski marcintyminski changed the title add driver add driver to frient EMI devices Jan 27, 2026
@KKlimczukS KKlimczukS requested a review from greens February 4, 2026 10:43
@greens greens changed the title add driver to frient EMI devices WWSTCERT-10228/10231/10234 add driver to frient EMI devices Feb 5, 2026
@greens greens changed the title WWSTCERT-10228/10231/10234 add driver to frient EMI devices WWSTCERT-10225/10228/10231/10234 add driver to frient EMI devices Feb 5, 2026
Comment on lines +20 to +28
- title: "Pulse Configuration"
name: pulseConfiguration
description: "Number of pulses the meter outputs per unit"
required: false
preferenceType: integer
definition:
minimum: 50
maximum: 10000
default: 1000
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pulse Configuration is the number of pulses the meter outputs per unit. It may be different depending on the meter, so we need an option to adjust it, so that the device reads measurements properly from a wide range of meters.

Copy link
Contributor

Choose a reason for hiding this comment

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

All of these profiles should have different names, I think.

The first two are going to be frient-specific because of the preferences, and the third has a misleading name since it uses components.

@@ -0,0 +1,15 @@
-- Copyright 2025 SmartThings, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

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

date

attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Uint48,

Choose a reason for hiding this comment

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

Does this and the above need to be Uint48?

@@ -0,0 +1,362 @@
-- Copyright 2025 SmartThings, Inc.

Choose a reason for hiding this comment

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

Date

@@ -0,0 +1,8 @@
-- Copyright 2025 SmartThings, Inc.

Choose a reason for hiding this comment

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

Date

@@ -0,0 +1,15 @@
-- Copyright 2025 SmartThings, Inc.

Choose a reason for hiding this comment

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

Date

}
},
sub_drivers = {
require("frient/EMIZB-151")

Choose a reason for hiding this comment

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

match formatting -> frient.EMIZB-151

@@ -0,0 +1,340 @@
-- Copyright 2025 SmartThings

Choose a reason for hiding this comment

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

date

@@ -0,0 +1,303 @@
-- Copyright 2025 SmartThings

Choose a reason for hiding this comment

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

date

@@ -0,0 +1,396 @@
-- Copyright 2025 SmartThings
Copy link

@pegor-karoglanian pegor-karoglanian Feb 5, 2026

Choose a reason for hiding this comment

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

date

lazy_load_if_possible("frient"),
lazy_load_if_possible("shinasystems"),
lazy_load_if_possible("bituo"),
lazy_load_if_possible("frient/EMIZB-151")

Choose a reason for hiding this comment

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

If I'm not mistake, I think this should live as a subdriver within the frient init. Thoughts @greens ?

deviceProfileName: power-energy-consumption-report
- id: "frient A/S/EMIZB-132"
deviceLabel: frient Energy Monitor
manufacturer: Develco Products A/S

Choose a reason for hiding this comment

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

Are these no longer necessary?

raw_value = raw_value * multiplier / divisor * 1000

-- The result is already in watts, no need to multiply by 1000
device:emit_component_event(device.profile.components['main'], capabilities.powerMeter.power({ value = raw_value, unit = "W" }))
Copy link
Contributor

Choose a reason for hiding this comment

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

when emitting for the main component, you can just use device:emit_event

Copy link
Contributor

@greens greens Feb 5, 2026

Choose a reason for hiding this comment

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

Also this seems identical to the defaults except for the default value, which you could set elsewhere (like in added) with

device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, SIMPLE_METERING_DEFAULT_DIVISOR)

instead of overriding the default behavior

Comment on lines +137 to +142
device:send(ElectricalMeasurement.attributes.ACPowerDivisor:read(device))
device:send(ElectricalMeasurement.attributes.ACPowerMultiplier:read(device))
device:send(ElectricalMeasurement.attributes.ACVoltageMultiplier:read(device))
device:send(ElectricalMeasurement.attributes.ACVoltageDivisor:read(device))
device:send(ElectricalMeasurement.attributes.ACCurrentMultiplier:read(device))
device:send(ElectricalMeasurement.attributes.ACCurrentDivisor:read(device))
Copy link
Contributor

Choose a reason for hiding this comment

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

I would expect these should be sent already by device:refresh() since they're configured attributes.

Comment on lines +171 to +173
if divisor == 0 then
divisor = 1
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of checking for 0 every time here, you could just make sure that when you set a divisor, you just throw out the value if it's 0.

Comment on lines +243 to +298
local active_power_handler = function(component)
local handler = function(driver, device, value, zb_rx)
local raw_value = value.value
-- By default emit raw value
local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_MULTIPLIER_KEY) or 1
local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) or 1

if divisor == 0 then
divisor = 1
end

raw_value = raw_value * multiplier / divisor

device:emit_component_event(device.profile.components[component], capabilities.powerMeter.power({ value = raw_value, unit = "W" }))
end

return handler
end

local rms_voltage_handler = function(component)
local handler = function(driver, device, value, zb_rx)
local raw_value = value.value
-- By default emit raw value
local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY) or 1
local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY) or 1

if divisor == 0 then
divisor = 1
end

raw_value = raw_value * multiplier / divisor

device:emit_component_event(device.profile.components[component], capabilities.voltageMeasurement.voltage({ value = raw_value, unit = "V" }))
end

return handler
end

local rms_current_handler = function(component)
local handler = function(driver, device, value, zb_rx)
local raw_value = value.value
-- By default emit raw value
local multiplier = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY) or 1
local divisor = device:get_field(zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY) or 1

if divisor == 0 then
divisor = 1
end

raw_value = raw_value * multiplier / divisor

device:emit_component_event(device.profile.components[component], capabilities.currentMeasurement.current({ value = raw_value, unit = "A" }))
end

return handler
end
Copy link
Contributor

Choose a reason for hiding this comment

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

these three could be consolidated further by having the mul/div keys, unit, and the attribute as arguments as well

raw_value = 1
end

device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, raw_value, { persist = true })
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not seeing a lot of added value over the default handling here.

Is the device often sending these updates with the mfg-specific bit set? It shouldn't be, since this is a standard attribute.

Comment on lines +40 to +47
{
cluster = SimpleMetering.ID,
attribute = SimpleMetering.attributes.InstantaneousDemand.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Int24,
reportable_change = 1
},
Copy link
Contributor

Choose a reason for hiding this comment

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

same as the default

Copy link
Contributor Author

Choose a reason for hiding this comment

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

default reportable_change is 5. We want it to be as sensitive as possible

Comment on lines +48 to +55
{
cluster = ElectricalMeasurement.ID,
attribute = ElectricalMeasurement.attributes.ActivePower.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Int16,
reportable_change = 5
},
Copy link
Contributor

Choose a reason for hiding this comment

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

same as the default

Comment on lines +32 to +39
{
cluster = SimpleMetering.ID,
attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Uint48,
reportable_change = 1
},
Copy link
Contributor

Choose a reason for hiding this comment

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

same as the defaults

Comment on lines +16 to +19
zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_MULTIPLIER_KEY = "_electrical_measurement_ac_voltage_multiplier"
zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_MULTIPLIER_KEY = "_electrical_measurement_ac_current_multiplier"
zigbee_constants.ELECTRICAL_MEASUREMENT_AC_VOLTAGE_DIVISOR_KEY = "_electrical_measurement_ac_voltage_divisor"
zigbee_constants.ELECTRICAL_MEASUREMENT_AC_CURRENT_DIVISOR_KEY = "_electrical_measurement_ac_current_divisor"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd feel more comfortable if you just made these strings local to this driver rather than writing to the zigbee_constants map.

Comment on lines +1 to +13
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
Copy link
Contributor

@greens greens Feb 5, 2026

Choose a reason for hiding this comment

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

update this copyright/license statement to the shorter and more concise one

Comment on lines +24 to +41
local ATTRIBUTES = {
{
cluster = SimpleMetering.ID,
attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Uint48,
reportable_change = 1
},
{
cluster = SimpleMetering.ID,
attribute = SimpleMetering.attributes.InstantaneousDemand.ID,
minimum_interval = 5,
maximum_interval = 3600,
data_type = data_types.Int24,
reportable_change = 1
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

both the same as the defaults as far as I can tell

Comment on lines +82 to +84
device.thread:call_with_delay(5, function()
do_refresh(self, device)
end)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need to call refresh 5 seconds after you just called it at the top of this function?

device:configure()

if device:supports_capability(capabilities.battery) then
device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(device, 30, 21600, 1))
Copy link
Contributor

Choose a reason for hiding this comment

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

is this not handled by device:configure already?

device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(device, 30, 21600, 1))
end
for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do
if device:get_model() == fingerprint.model and fingerprint.preferences then
Copy link
Contributor

Choose a reason for hiding this comment

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

can't you just check for the existence of the preference itself rather than including a separate boolean in the fingerprints map?

Comment on lines +118 to +131
local function instantaneous_demand_handler(driver, device, value, zb_rx)
local raw_value = value.value
--- demand = demand received * Multipler/Divisor
local multiplier = device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1
local divisor = device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) or SIMPLE_METERING_DEFAULT_DIVISOR
if raw_value < -8388607 or raw_value >= 8388607 then
raw_value = 0
end

raw_value = raw_value * multiplier / divisor * 1000

local raw_value_watts = raw_value
device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({ value = raw_value_watts, unit = "W" }))
end
Copy link
Contributor

Choose a reason for hiding this comment

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

This also seems functionally the same as the default behavior.

device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({ value = raw_value_watts, unit = "W" }))
end

local function energy_meter_handler(driver, device, value, zb_rx)
Copy link
Contributor

Choose a reason for hiding this comment

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

what's different between this handler and the sub-driver's version?


local device_init = function(self, device)
for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do
if device:get_model() == fingerprint.model and fingerprint.battery then
Copy link
Contributor

Choose a reason for hiding this comment

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

you can just check for whether MIN_BAT exists rather than use the extra boolean

@KKlimczukS KKlimczukS requested a review from greens February 9, 2026 08:51
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