Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Aqara Light Switch H2 #1822

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: light-power-energy-powerConsumption
components:
- id: main
capabilities:
- id: switch
version: 1
- id: powerMeter
version: 1
- id: energyMeter
version: 1
- id: powerConsumptionReport
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: Light
52 changes: 46 additions & 6 deletions drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map"
-- rather than COMPONENT_TO_ENDPOINT_MAP.
local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button"
local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device"
local IS_AQARA_SWITCH_DEVICE = "__is_aqara_switch_device"
local COLOR_TEMP_BOUND_RECEIVED = "__colorTemp_bound_received"
local COLOR_TEMP_MIN = "__color_temp_min"
local COLOR_TEMP_MAX = "__color_temp_max"
Expand All @@ -65,6 +66,7 @@ local ON_OFF_SWITCH_ID = 0x0103
local ON_OFF_DIMMER_SWITCH_ID = 0x0104
local ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105
local GENERIC_SWITCH_ID = 0x000F
local ELECTRICAL_SENSOR_ID = 0x0510
local device_type_profile_map = {
[ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary",
[DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level",
Expand Down Expand Up @@ -147,12 +149,20 @@ local device_type_attribute_map = {
clusters.Switch.events.LongPress,
clusters.Switch.events.ShortRelease,
clusters.Switch.events.MultiPressComplete
},
[ELECTRICAL_SENSOR_ID] = {
clusters.ElectricalPowerMeasurement.attributes.ActivePower,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported
}
}

local child_device_profile_overrides = {
{ vendor_id = 0x1321, product_id = 0x000C, child_profile = "switch-binary" },
{ vendor_id = 0x1321, product_id = 0x000D, child_profile = "switch-binary" },
{ vendor_id = 0x115F, product_id = 0x1008, child_profile = "light-power-energy-powerConsumption" }, -- 2 switch
{ vendor_id = 0x115F, product_id = 0x1009, child_profile = "light-power-energy-powerConsumption" }, -- 4 switch
}

local detect_matter_thing
Expand Down Expand Up @@ -256,6 +266,7 @@ local HELD_THRESHOLD = 1
local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8}

local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE"
local BUTTON_DEVICE_PROFILED = "__button_device_profiled"

-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a
-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because
Expand All @@ -270,6 +281,7 @@ local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitc
local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease)

local HUE_MANUFACTURER_ID = 0x100B
local AQARA_MANUFACTURER_ID = 0x115F

--helper function to create list of multi press values
local function create_multi_press_values_list(size, supportsHeld)
Expand Down Expand Up @@ -405,15 +417,24 @@ local function find_default_endpoint(device)
return device.MATTER_DEFAULT_ENDPOINT
end

local function assign_child_profile(device, child_ep)
local function assign_child_profile(device, child_ep, ep_sequence)
local profile

-- check if device has an overridden child profile that differs from the profile
-- that would match the child's device type
for _, fingerprint in ipairs(child_device_profile_overrides) do
if device.manufacturer_info.vendor_id == fingerprint.vendor_id and
device.manufacturer_info.product_id == fingerprint.product_id then
return fingerprint.child_profile
if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID then
if ep_sequence == 1 then
-- To add Electrical Sensor only to the first EDGE_CHILD(light-power-energy-powerConsumption)
-- The profile of the second EDGE_CHILD is determined in the "for" loop below (e.g., light-binary)
device:set_field(IS_AQARA_SWITCH_DEVICE, true, {persist = true})
return fingerprint.child_profile
end
else
return fingerprint.child_profile
end
Comment on lines +433 to +437
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return fingerprint.child_profile
end
else
return fingerprint.child_profile
end
end
return fingerprint.child_profile

There's no reason to overcomplicate this with multiple returns. There just needs to be extra logic before the return for the aqara devices.

Copy link
Contributor Author

@DongHoon-Ryu DongHoon-Ryu Jan 3, 2025

Choose a reason for hiding this comment

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

If two On/Off Light devices are added as EDGE_CHILD, only the first device is defined as fingerprint.child_profile (light-power-energy-powerConsumption profile), and the profile of the second device is to select the light-binary profile by the for loop logic below.
Therefore, we need to keep the existing logic as it is, so I added comments in the source code.

  for _, fingerprint in ipairs(child_device_profile_overrides) do
    if device.manufacturer_info.vendor_id == fingerprint.vendor_id and
       device.manufacturer_info.product_id == fingerprint.product_id then
      if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID then
        if ep_sequence == 1 then
          -- To add Electrical Sensor only to the first EDGE_CHILD(light-power-energy-powerConsumption)
          -- The profile of the second EDGE_CHILD is determined in the "for" loop below (e.g., light-binary)
          device:set_field(IS_AQARA_SWITCH_DEVICE, true, {persist = true})
          return fingerprint.child_profile
        end
      else
        return fingerprint.child_profile
      end
    end
  end

  for _, ep in ipairs(device.endpoints) do
    if ep.endpoint_id == child_ep then

end
end

Expand All @@ -436,6 +457,9 @@ local function assign_child_profile(device, child_ep)
end

local function do_configure(driver, device)
if device:get_field(BUTTON_DEVICE_PROFILED) then
return
end
local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID)
local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID)
local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID)
Expand Down Expand Up @@ -537,12 +561,14 @@ local function initialize_switch(driver, device)
component_map_used = true
end

local ep_sequence = 0
for _, ep in ipairs(switch_eps) do
if device:supports_server_cluster(clusters.OnOff.ID, ep) then
num_switch_server_eps = num_switch_server_eps + 1
if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint
ep_sequence = ep_sequence + 1
local name = string.format("%s %d", device.label, num_switch_server_eps)
local child_profile = assign_child_profile(device, ep)
local child_profile = assign_child_profile(device, ep, ep_sequence)
driver:try_create_device(
{
type = "EDGE_CHILD",
Expand Down Expand Up @@ -579,6 +605,7 @@ local function initialize_switch(driver, device)
end
device:try_update_metadata({profile = profile_name})
device:set_field(DEFERRED_CONFIGURE, true)
device:set_field(BUTTON_DEVICE_PROFILED, true)
elseif #button_eps > 0 then
local battery_support = false
if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and
Expand All @@ -599,6 +626,7 @@ local function initialize_switch(driver, device)
if profile_name then
device:try_update_metadata({profile = profile_name})
device:set_field(DEFERRED_CONFIGURE, true)
device:set_field(BUTTON_DEVICE_PROFILED, true)
else
configure_buttons(device)
end
Expand Down Expand Up @@ -680,7 +708,9 @@ local function device_init(driver, device)
end
local main_endpoint = find_default_endpoint(device)
for _, ep in ipairs(device.endpoints) do
if ep.endpoint_id ~= main_endpoint and ep.endpoint_id ~= 0 then
if ep.endpoint_id ~= main_endpoint and
(device:get_field(IS_AQARA_SWITCH_DEVICE) or ep.endpoint_id ~= 0) then
-- insert energy management into InteractionRequest list when IS_AQARA_SWITCH_DEVICE
Comment on lines -683 to +713
Copy link
Contributor

Choose a reason for hiding this comment

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

What attribute are we expecting to track for the Aqara switch at its 0 endpoint?

Copy link
Contributor Author

@DongHoon-Ryu DongHoon-Ryu Jan 3, 2025

Choose a reason for hiding this comment

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

ep0 has an Electrical sensor(0x0510). So I added the following and added the attributes to the subscribe_list in the unit test.

  [ELECTRICAL_SENSOR_ID] = {
    clusters.ElectricalPowerMeasurement.attributes.ActivePower,
    clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
    clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported,
    clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported
  }

local id = 0
for _, dt in ipairs(ep.device_types) do
id = math.max(id, dt.device_type_id)
Expand Down Expand Up @@ -983,7 +1013,11 @@ local function cumul_energy_exported_handler(driver, device, ib, response)
if ib.data.elements.energy then
local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT
device:set_field(TOTAL_EXPORTED_ENERGY, watt_hour_value)
device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
if device:get_field(IS_AQARA_SWITCH_DEVICE) then
device:emit_event_for_endpoint(1, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
else
Comment on lines +1016 to +1018
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this- is unnecessary

Copy link
Contributor Author

@DongHoon-Ryu DongHoon-Ryu Jan 3, 2025

Choose a reason for hiding this comment

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

  • When EDGE_CHILD has powerMeter and energyMeter capability in its profile
    . When the device card is implemented as below, the event related to the Electrical Sensor(0x0510) comes to the main profile containing ep4, which causes a crash because the related capability is not defined in the button device profile to handle it.
    . So, it is added to send the event to the first endpoint(ep1) containing the actual powerMeter or energyMeter capability.
endpoint      device type
******************************************
ep0              Root(0x0016), Electrical sensor(0x0510)
ep1              On/Off Light(0x0100)            <-- EDGE_CHILD(light-power-energy-powerConsumption profile)
ep2              On/Off Light(0x0100)            <-- EDGE_CHILD(light-binary profile)
ep4              Generic Switch(0x000F)          <-- main profile(4-button profile)
ep5              Generic Switch(0x000F)
ep6              Generic Switch(0x000F)
ep7              Generic Switch(0x000F)

device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
end
end
end

Expand Down Expand Up @@ -1046,7 +1080,11 @@ end
local function active_power_handler(driver, device, ib, response)
if ib.data.value then
local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT
device:emit_event(capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
if device:get_field(IS_AQARA_SWITCH_DEVICE) then
device:emit_event_for_endpoint(1, capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
else
Comment on lines +1083 to +1085
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this- is unnecessary

Copy link
Contributor Author

@DongHoon-Ryu DongHoon-Ryu Jan 3, 2025

Choose a reason for hiding this comment

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

  • When EDGE_CHILD has powerMeter and energyMeter capability in its profile
    . When the device card is implemented as below, the event related to the Electrical Sensor(0x0510) comes to the main profile containing ep4, which causes a crash because the related capability is not defined in the button device profile to handle it.
    . So, it is added to send the event to the first endpoint(ep1) containing the actual powerMeter or energyMeter capability.
endpoint      device type
******************************************
ep0              Root(0x0016), Electrical sensor(0x0510)
ep1              On/Off Light(0x0100)            <-- EDGE_CHILD(light-power-energy-powerConsumption profile)
ep2              On/Off Light(0x0100)            <-- EDGE_CHILD(light-binary profile)
ep4              Generic Switch(0x000F)          <-- main profile(4-button profile)
ep5              Generic Switch(0x000F)
ep6              Generic Switch(0x000F)
ep7              Generic Switch(0x000F)

device:emit_event(capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
end
end
end

Expand Down Expand Up @@ -1174,6 +1212,7 @@ local matter_driver_template = {
[clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler,
},
[clusters.ElectricalEnergyMeasurement.ID] = {
[clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true),
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry for the confusion- see this PR which will change the system to only use imported and not exported energy. This fixup be merged before this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After that PR is merged, I will merge this PR.

[clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true),
[clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false),
},
Expand Down Expand Up @@ -1234,6 +1273,7 @@ local matter_driver_template = {
clusters.PowerSource.attributes.BatPercentRemaining,
},
[capabilities.energyMeter.ID] = {
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported
},
Expand Down
Loading
Loading