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

Matter 1.4 Driver Release #1870

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open

Matter 1.4 Driver Release #1870

wants to merge 15 commits into from

Conversation

nickolas-deboom
Copy link
Contributor

@nickolas-deboom nickolas-deboom commented Jan 13, 2025

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

This PR contains changes to support new device types introduced by the Matter 1.4 specification. These device types include the following:

  • Mounted On/Off Control
  • Mounted Dimmable Load Control
  • Water Heater
  • Solar Power
  • Battery Storage
  • Heat Pump

Summary of Completed Tests

Copy link

Copy link

github-actions bot commented Jan 13, 2025

Test Results

   65 files  + 1    412 suites  +6   0s ⏱️ ±0s
2 037 tests +24  2 037 ✅ +24  0 💤 ±0  0 ❌ ±0 
3 529 runs  +40  3 529 ✅ +40  0 💤 ±0  0 ❌ ±0 

Results for commit 6f0a524. ± Comparison against base commit 7171fd3.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Jan 13, 2025

matter-evse_coverage.xml

File Coverage
All files 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/init.lua 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua 82%

matter-hrap_coverage.xml

File Coverage
All files 100%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/init.lua 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua 82%

matter-switch_coverage.xml

File Coverage
All files 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/init.lua 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua 82%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua 38%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/init.lua 96%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua 96%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/eve-energy/init.lua 91%

matter-thermostat_coverage.xml

File Coverage
All files 82%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/init.lua 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua 82%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua 38%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/init.lua 96%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua 96%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-switch/src/eve-energy/init.lua 91%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-thermostat/src/init.lua 83%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua 46%

Minimum allowed coverage is 90%

Generated by 🐒 cobertura-action against 6f0a524

This initial commit adds support for the mounted on/off control and mounted
dimmable load control device types introduced by the matter 1.4 spec.
Copy link

Duplicate profile check: Passed - no duplicate profiles detected.

@nickolas-deboom
Copy link
Contributor Author

I left a few comments but everything else in the HRAP driver looks good to me! I can't remember but was there a file we've updated in the past to prevent github from checking test coverage of the embedded lua libs files? We could update that as part of this PR as well (I just can't remember where it is)

@nickolas-deboom
Copy link
Contributor Author

nickolas-deboom commented Jan 23, 2025

I get an error when trying to package the hrap driver locally:

» smartthings edge:drivers:package                                                                                                                                nickloasdeboom@MN-NDEBOOM-L
    AxiosError: Request failed with status code 422: {"requestId":"1610617032588185407","error":{"code":"ConstraintViolationError","message":"Invalid device profile specification for 
    eb1b3ce7-a434-4cd9-8fab-e5289b628b35","details":[{"code":"NotValidValue","target":"main.capabilities.routerName","message":"Capability does not exist: routerName version 
    1","details":[]},{"code":"NotValidValue","target":"main.capabilities.threadVersion","message":"Capability does not exist: threadVersion version 
    1","details":[]},{"code":"NotValidValue","target":"main.capabilities.routerState","message":"Capability does not exist: routerState version 1","details":[]}]}}
    Code: ERR_BAD_REQUEST

Is this happening due to the embedded capability definitions?

nickolas-deboom and others added 2 commits January 23, 2025 15:39
The mounted device types will already join to the correct fingerprint
and so do not require the logic added for them in initialize_switch to
select the correct profile.
Comment on lines +351 to +366
local function delete_reporting_timer(device)
local reporting_poll_timer = device:get_field(RECURRING_REPORT_TIMER)
if reporting_poll_timer ~= nil then
device.thread:cancel_timer(reporting_poll_timer)
device:set_field(RECURRING_REPORT_TIMER, nil)
end
end

local function remove_timers(device)
delete_reporting_timer(device)
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
device.thread:cancel_timer(poll_timer)
device:set_field(RECURRING_POLL_TIMER, nil)
end
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 seems like unnecessary breakdown of functionality to me. I would suggest:

Suggested change
local function delete_reporting_timer(device)
local reporting_poll_timer = device:get_field(RECURRING_REPORT_TIMER)
if reporting_poll_timer ~= nil then
device.thread:cancel_timer(reporting_poll_timer)
device:set_field(RECURRING_REPORT_TIMER, nil)
end
end
local function remove_timers(device)
delete_reporting_timer(device)
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
device.thread:cancel_timer(poll_timer)
device:set_field(RECURRING_POLL_TIMER, nil)
end
end
local function remove_timers(device)
-- remove report timer
local reporting_poll_timer = device:get_field(RECURRING_REPORT_TIMER)
if reporting_poll_timer ~= nil then
device.thread:cancel_timer(reporting_poll_timer)
device:set_field(RECURRING_REPORT_TIMER, nil)
end
-- remove poll timer
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
device.thread:cancel_timer(poll_timer)
device:set_field(RECURRING_POLL_TIMER, nil)
end
end

Comment on lines +405 to +406
local val = find_default_endpoint(device, clusters.Thermostat.ID)
return val
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this changed?

Comment on lines +331 to +332
local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport
.ID,
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
local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport
.ID,
local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport.ID,

clusters.ElectricalEnergyMeasurement.ID,
{feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY })
if cumul_eps and #cumul_eps > 0 then
local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should there be a loop here similar to below in case there are multiple endpoints containing the ElectricalEnergyMeasurement cluster? Or is it not expected to have multiple endpoints?

Suggested change
local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)
local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)
for i, ep in ipairs(cumul_eps) do
if i > 1 then
read_req:merge( clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, cumul_eps[i]))
end
end

if #electrical_sensor_eps > 0 then
profile_name = "water-heater-power-energy-powerConsumption"
end
elseif #thermostat_eps > 0 and device_type ~= HEAT_PUMP_DEVICE_TYPE_ID then
Copy link
Contributor

@hcarter-775 hcarter-775 Feb 3, 2025

Choose a reason for hiding this comment

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

I think we should keep this simple and implement it as:

Suggested change
elseif #thermostat_eps > 0 and device_type ~= HEAT_PUMP_DEVICE_TYPE_ID then
elseif device_type == HEAT_PUMP_DEVICE_TYPE_ID then
profile_name = "heat-pump"
elseif #thermostat_eps > 0 then

This is more extensible and keeps the original logical flow. And to continue a comment I left earlier, rather than heat-pump it should be

    profile_name = "heat-pump-2-thermostat"

in the suggestion I made.

Comment on lines +771 to +778
local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID)
if #heat_pump_eps > 0 then
local component_to_endpoint_map = {
["thermostatOne"] = thermostat_eps[1],
["thermostatTwo"] = thermostat_eps[2],
}
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true})
end
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
local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID)
if #heat_pump_eps > 0 then
local component_to_endpoint_map = {
["thermostatOne"] = thermostat_eps[1],
["thermostatTwo"] = thermostat_eps[2],
}
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true})
end
if #heat_pump_eps > 0 then
local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID)
local component_to_endpoint_map = {
["thermostatOne"] = thermostat_eps[1],
["thermostatTwo"] = thermostat_eps[2],
}
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true})
end

Though this is pretty un-extendable as-is. I do think we should extend this logic to permit 1, 2, or more thermostats, even if we only have a profile to support the 2 case for now.

Comment on lines +1611 to +1613
if version.api < 11 then
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:augment_type(ib.data)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

for my understanding, what is this logic doing?

Comment on lines +1627 to +1631
if tbl_contains(cumul_eps, endpoint_id) then
-- Since cluster at this endpoint supports both CUME & PERE features, we will prefer
-- cumulative_energy_imported_handler to handle the energy report for this endpoint.
return
end
Copy link
Contributor

Choose a reason for hiding this comment

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

let's move this top the top of the function handler. There's no need to do any further logic if this is true.

}
log.debug("component_to_endpoint_map " .. utils.stringify_table(component_to_endpoint_map))
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, { persist = true })
local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID)
local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID) or {}

To prevent an error on the next line in the case that get_endpoints_for_dt returns nil

[clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED),
[clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED),
[clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED),
[clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
[clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED),
[clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this was meant to be PeriodicEnergyExported

local energy_imported_Wh = utils.round(energy_imported_mWh / 1000)
local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {}
cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh
device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { 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.

why are we adding this map handling? Are we expecting devices that support energy control on multiple endpoints? As far as I see, neither of the two new thermostat device types support this.

Comment on lines +1651 to +1652
local cumulative_energy_imported_mWh = ib.data.elements.energy.value
local cumulative_energy_imported_Wh = utils.round(cumulative_energy_imported_mWh / 1000)
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
local cumulative_energy_imported_mWh = ib.data.elements.energy.value
local cumulative_energy_imported_Wh = utils.round(cumulative_energy_imported_mWh / 1000)
local cumulative_energy_imported_Wh = utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh

Copy link
Contributor

Choose a reason for hiding this comment

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

same thing for the periodic energy handler

Comment on lines +1681 to +1682
for i, mode in ipairs(supportWaterHeaterModes) do
if i - 1 == currentMode then
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 numerical ordering of the modes an assumption we can safely make?

)

test.register_message_test(
"Heating setpoint reports from child thermostat devices should emit correct events to the corret endpoint",
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
"Heating setpoint reports from child thermostat devices should emit correct events to the corret endpoint",
"Heating setpoint reports from component thermostat devices should emit correct events to the corret endpoint",

)

test.register_message_test(
"Cooling setpoint reports reports from child thermostat devices should emit correct events to the corret endpoint",
Copy link
Contributor

@hcarter-775 hcarter-775 Feb 3, 2025

Choose a reason for hiding this comment

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

Suggested change
"Cooling setpoint reports reports from child thermostat devices should emit correct events to the corret endpoint",
"Cooling setpoint reports reports from component thermostat devices should emit correct events to the correct endpoint",

@@ -5,3 +5,13 @@ matterGeneric:
- id: 0x0510
- id: 0x050C
deviceProfileName: evse
- id: "matter/solar-power"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should change the name of this driver from matter-evse to matter-energy.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think so.

@@ -115,6 +134,16 @@ local function tbl_contains(array, value)
return false
end

local get_total = function(map)
if map ~= nil and type(map) == "table" then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should leave a debug message for the case where type(map) == "table"

if i > 1 then
read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device, ep))
read_req:merge( clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, eps_to_read[i]))
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
read_req:merge( clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, eps_to_read[i]))
read_req:merge( clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, ep)

Comment on lines +645 to +648
-- Consider only Solar Power / Battery Storage devices and sum up in case there are multiple endpoints.
local battery_storage_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID)
local solar_power_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID)
if (tbl_contains(solar_power_eps, ib.endpoint_id) or tbl_contains(battery_storage_eps, ib.endpoint_id)) and ib.data.value then
Copy link
Contributor

@hcarter-775 hcarter-775 Feb 4, 2025

Choose a reason for hiding this comment

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

This logic doesn't seem necessary, since we wouldn't be subscribing to this attribute in other device types in the first place.


active_power_map[endpoint_id] = watt_value
local total_active_power = get_total(active_power_map)
device:set_field(TOTAL_ACTIVE_POWER, active_power_map)
Copy link
Contributor

Choose a reason for hiding this comment

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

what devices do we expect to support multiple active power measurements?

Comment on lines +239 to +240
local previousTotalConsumptionWh = device:get_latest_state(comp, capabilities.powerConsumptionReport
.ID,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
local previousTotalConsumptionWh = device:get_latest_state(comp, capabilities.powerConsumptionReport
.ID,
local previousTotalConsumptionWh = device:get_latest_state(comp, capabilities.powerConsumptionReport.ID,

-- 2. if the received setpoint command value is in range 86 ~ 176, it is inferred as *F
local WATER_HEATER_MAX_TEMP_IN_C = 80.0
local WATER_HEATER_MIN_TEMP_IN_C = 30.0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should there also be a temperature range defined for Heat Pump? What would the expected range be for that device type? Currently it would use the 5-40 deg C range for thermostats

Copy link
Contributor

Choose a reason for hiding this comment

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

No, a heat pump can just use the thermostat range, since it will be attached to regular thermostats.

end
end

local function periodic_energy_imported_handler(driver, device, ib, response)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you create test case(s) that exercise this function?

device:emit_event_for_endpoint(ib.endpoint_id, event)
end

local function water_heater_mode_handler(driver, device, ib, response)
Copy link
Contributor Author

@nickolas-deboom nickolas-deboom Feb 6, 2025

Choose a reason for hiding this comment

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

Could you also create a test case for the water heater mode handler?

- name: SolarPanel
- id: exportedEnergy
capabilities:
- id: powerConsumptionReport
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the reasoning for displaying the power Consumption report for this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

powerConsumptionReport is the capability used for Energy service. In the long run, we wait for this information to be reported to the Energy service.

capabilities:
- id: thermostatMode
version: 1
- id: relativeHumidityMeasurement
Copy link
Contributor

@hcarter-775 hcarter-775 Feb 13, 2025

Choose a reason for hiding this comment

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

I don't think this should be a required trait. I think we should separate humidity into a separate profile that uses a -humidity for each thermostat in the profile name.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay I will modify it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants