Skip to content

Matter Appliance minor refactor #2181

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

Open
wants to merge 2 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
179 changes: 179 additions & 0 deletions drivers/SmartThings/matter-appliance/src/common-utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
-- 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.

local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "embedded-cluster-utils"
local im = require "st.matter.interaction_model"
local utils = require "st.utils"
local version = require "version"

local common_utils = {}

common_utils.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map"
common_utils.SUPPORTED_TEMPERATURE_LEVELS_MAP = "__supported_temperature_levels_map"

common_utils.updated_fields = {
{ current_field_name = "__supported_temperature_levels", updated_field_name = common_utils.SUPPORTED_TEMPERATURE_LEVELS_MAP }
}

common_utils.setpoint_limit_device_field = {
MIN_TEMP = "MIN_TEMP",
MAX_TEMP = "MAX_TEMP"
}

local extended_range_support = version.rpc >= 6

-- For RPC version <= 5, temperature conversion for setpoint ranges is not supported. When units are switched,
-- the units of the received command are unknown as the arguments don't contain the unit. To handle this, we
-- use a smaller range so that the temperature ranges for Celsius and Fahrenheit are separate. Take the laundry
-- washer range for example:
-- if the received setpoint command value is in range 13 ~ 55, it is inferred as *C
-- if the received setpoint command value is in range 55.4 ~ 131, it is inferred as *F
-- For RPC version >= 6, we can always assume that the values received from temperatureSetpoint is in Celsius
-- so we can support a larger setpoint range, but we still limit the range to reasonable values.
local default_min_and_max_temp_by_device_type = {
["dishwasher"] = { min_temp = extended_range_support and 0.0 or 33.0, max_temp = extended_range_support and 100.0 or 90.0 },
["dryer"] = { min_temp = extended_range_support and 0.0 or 27.0, max_temp = extended_range_support and 100.0 or 80.0 },
["washer"] = { min_temp = extended_range_support and 0.0 or 13.0, max_temp = extended_range_support and 100.0 or 55.0 },
["oven"] = { min_temp = extended_range_support and 0.0 or 127.0, max_temp = extended_range_support and 400.0 or 260.0 },
["refrigerator"] = { min_temp = extended_range_support and -10.0 or -6.0, max_temp = extended_range_support and 30.0 or 20.0 },
["freezer"] = { min_temp = extended_range_support and -30.0 or -24.0, max_temp = extended_range_support and 0.0 or -12.0 },
["default"] = { min_temp = 0.0, max_temp = extended_range_support and 100.0 or 40.0 }
}

function common_utils.check_field_name_updates(device)
for _, field in ipairs(common_utils.updated_fields) do
if device:get_field(field.current_field_name) then
if field.updated_field_name ~= nil then
device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true})
end
device:set_field(field.current_field_name, nil)
end
end
end

function common_utils.get_endpoints_for_dt(device, device_type)
local endpoints = {}
for _, ep in ipairs(device.endpoints) do
for _, dt in ipairs(ep.device_types) do
if dt.device_type_id == device_type then
table.insert(endpoints, ep.endpoint_id)
break
end
end
end
table.sort(endpoints)
return endpoints
end

function common_utils.query_setpoint_limits(device)
local setpoint_limit_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {})
if device:get_field(common_utils.setpoint_limit_device_field.MIN_TEMP) == nil then
setpoint_limit_read:merge(clusters.TemperatureControl.attributes.MinTemperature:read())
end
if device:get_field(common_utils.setpoint_limit_device_field.MAX_TEMP) == nil then
setpoint_limit_read:merge(clusters.TemperatureControl.attributes.MaxTemperature:read())
end
if #setpoint_limit_read.info_blocks ~= 0 then
device:send(setpoint_limit_read)
end
end

function common_utils.supports_temperature_level_endpoint(device, endpoint)
local feature = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL
local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = feature})
if #tl_eps == 0 then
device.log.warn(string.format("Device does not support TEMPERATURE_LEVEL feature"))
return false
end
for _, eps in ipairs(tl_eps) do
if eps == endpoint then
return true
end
end
device.log.warn(string.format("Endpoint(%d) does not support TEMPERATURE_LEVEL feature", endpoint))
return false
end

function common_utils.supports_temperature_number_endpoint(device, endpoint)
local feature = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER
local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = feature})
if #tn_eps == 0 then
device.log.warn(string.format("Device does not support TEMPERATURE_NUMBER feature"))
return false
end
for _, eps in ipairs(tn_eps) do
if eps == endpoint then
return true
end
end
device.log.warn(string.format("Endpoint(%d) does not support TEMPERATURE_NUMBER feature", endpoint))
return false
end

function common_utils.temperature_setpoint_attr_handler(device, ib, device_type)
local min = device:get_field(string.format("%s-%d", common_utils.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id))
or default_min_and_max_temp_by_device_type[device_type].min_temp
local max = device:get_field(string.format("%s-%d", common_utils.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id))
or default_min_and_max_temp_by_device_type[device_type].max_temp
if not min or not max or not common_utils.supports_temperature_number_endpoint(device, ib.endpoint_id) then return end
local temp = ib.data.value / 100.0
local unit = "C"
local range = { minimum = min, maximum = max, step = 0.1 }
-- Only emit the capability for RPC version >= 5, since unit conversion for range capabilities is only supported in that case.
if version.rpc >= 5 then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureSetpoint.temperatureSetpointRange({value = range, unit = unit}, { visibility = { displayed = false } }))
end
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureSetpoint.temperatureSetpoint({value = temp, unit = unit}))
end

function common_utils.setpoint_limit_handler(device, ib, limit_field, device_type)
local field = string.format("%s-%d", limit_field, ib.endpoint_id)
local val = ib.data.value / 100.0

local min_temp_in_c = default_min_and_max_temp_by_device_type[device_type].min_temp
local max_temp_in_c = default_min_and_max_temp_by_device_type[device_type].max_temp
if not min_temp_in_c or not max_temp_in_c or not common_utils.supports_temperature_number_endpoint(device, ib.endpoint_id) then return end

val = utils.clamp_value(val, min_temp_in_c, max_temp_in_c)
device:set_field(field, val, { persist = true })
end

function common_utils.handle_temperature_setpoint(device, cmd, device_type)
local value = cmd.args.setpoint
local _, temp_setpoint = device:get_latest_state(
cmd.component, capabilities.temperatureSetpoint.ID,
capabilities.temperatureSetpoint.temperatureSetpoint.NAME,
0, { value = 0, unit = "C" }
)
local ep = device:component_to_endpoint(cmd.component)
local min = device:get_field(string.format("%s-%d", common_utils.setpoint_limit_device_field.MIN_TEMP, ep))
or default_min_and_max_temp_by_device_type[device_type].min_temp
local max = device:get_field(string.format("%s-%d", common_utils.setpoint_limit_device_field.MAX_TEMP, ep))
or default_min_and_max_temp_by_device_type[device_type].max_temp
if not min or not max or not common_utils.supports_temperature_number_endpoint(device, ep) then return end

if value > default_min_and_max_temp_by_device_type[device_type].max_temp and version.rpc <= 5 then
value = utils.f_to_c(value)
end
if value < min or value > max then
device.log.warn(string.format("Invalid setpoint (%s) outside the min (%s) and the max (%s)", value, min, max))
device:emit_event_for_endpoint(ep, capabilities.temperatureSetpoint.temperatureSetpoint(temp_setpoint))
return
end
device:send(clusters.TemperatureControl.commands.SetTemperature(device, ep, utils.round(value * 100), nil))
end

return common_utils
92 changes: 58 additions & 34 deletions drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
-- 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.

local clusters = require "st.matter.clusters"
local utils = require "st.utils"

local version = require "version"

if version.api < 10 then
clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring"
clusters.DishwasherAlarm = require "DishwasherAlarm"
Expand All @@ -20,12 +34,13 @@ if version.api < 11 then
clusters.MicrowaveOvenMode = require "MicrowaveOvenMode"
end

-- this cluster is not supported in any release of the lua libs
clusters.OvenMode = require "OvenMode"
if version.api < 12 then
clusters.OvenMode = require "OvenMode"
end

local embedded_cluster_utils = {}

local embedded_clusters = {
local embedded_clusters_api_10 = {
[clusters.ActivatedCarbonFilterMonitoring.ID] = clusters.ActivatedCarbonFilterMonitoring,
[clusters.DishwasherAlarm.ID] = clusters.DishwasherAlarm,
[clusters.DishwasherMode.ID] = clusters.DishwasherMode,
Expand All @@ -35,43 +50,52 @@ local embedded_clusters = {
[clusters.OperationalState.ID] = clusters.OperationalState,
[clusters.RefrigeratorAlarm.ID] = clusters.RefrigeratorAlarm,
[clusters.RefrigeratorAndTemperatureControlledCabinetMode.ID] = clusters.RefrigeratorAndTemperatureControlledCabinetMode,
[clusters.TemperatureControl.ID] = clusters.TemperatureControl,
[clusters.TemperatureControl.ID] = clusters.TemperatureControl
}

local embedded_clusters_api_11 = {
[clusters.MicrowaveOvenControl.ID] = clusters.MicrowaveOvenControl,
[clusters.MicrowaveOvenMode.ID] = clusters.MicrowaveOvenMode,
[clusters.OvenMode.ID] = clusters.OvenMode,
[clusters.OvenMode.ID] = clusters.OvenMode
}

local embedded_clusters_api_12 = {
[clusters.OvenMode.ID] = clusters.OvenMode
}

function embedded_cluster_utils.get_endpoints(device, cluster_id, opts)
-- If using older lua libs and need to check for an embedded cluster feature,
-- we must use the embedded cluster definitions here
if version.api < 10 and embedded_clusters[cluster_id] ~= nil then
local embedded_cluster = embedded_clusters[cluster_id]
local opts = opts or {}
if utils.table_size(opts) > 1 then
device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints")
return
end
local clus_has_features = function(clus, feature_bitmap)
if not feature_bitmap or not clus then return false end
return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map)
end
local eps = {}
for _, ep in ipairs(device.endpoints) do
for _, clus in ipairs(ep.clusters) do
if ((clus.cluster_id == cluster_id)
and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap))
and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH")
or (opts.cluster_type == clus.cluster_type))
or (cluster_id == nil)) then
table.insert(eps, ep.endpoint_id)
if cluster_id == nil then break end
end
-- If using older lua libs and need to check for an embedded cluster feature,
-- we must use the embedded cluster definitions here
if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or
version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil or
version.api < 12 and embedded_clusters_api_12[cluster_id] ~= nil then
local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] or embedded_clusters_api_12[cluster_id]
if not opts then opts = {} end
if utils.table_size(opts) > 1 then
device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints")
return
end
local clus_has_features = function(clus, feature_bitmap)
if not feature_bitmap or not clus then return false end
return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map)
end
local eps = {}
for _, ep in ipairs(device.endpoints) do
for _, clus in ipairs(ep.clusters) do
if ((clus.cluster_id == cluster_id)
and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap))
and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH")
or (opts.cluster_type == clus.cluster_type))
or (cluster_id == nil)) then
table.insert(eps, ep.endpoint_id)
if cluster_id == nil then break end
end
end
return eps
else
return device:get_endpoints(cluster_id, opts)
end
return eps
else
return device:get_endpoints(cluster_id, opts)
end
end

return embedded_cluster_utils
return embedded_cluster_utils
Loading
Loading