From 4390be186fe369053564182b9445650cc2e17cc8 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Fri, 9 Jan 2026 12:49:06 -0600 Subject: [PATCH] Matter Media: Account for MinLevel and MaxLevel for volume control This change accounts for the MinLevel and MaxLevel attributes when converting between level values used by the device and percentage values used in the audioVolume capability. --- drivers/SmartThings/matter-media/src/init.lua | 58 ++++++++----- .../src/test/test_matter_media_speaker.lua | 83 ++++++++++++++----- .../test/test_matter_media_video_player.lua | 15 +--- 3 files changed, 104 insertions(+), 52 deletions(-) diff --git a/drivers/SmartThings/matter-media/src/init.lua b/drivers/SmartThings/matter-media/src/init.lua index e041a9fde8..e32a40ac40 100644 --- a/drivers/SmartThings/matter-media/src/init.lua +++ b/drivers/SmartThings/matter-media/src/init.lua @@ -1,24 +1,19 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Opportunities for improvement: -- * MediaInput cluster could be used to support the MediaSource capability. -- * Channel cluster could be used to support the TvChannel capability. -- * AdvancedSeek feature support. + +local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local MatterDriver = require "st.matter.driver" +local st_utils = require "st.utils" + +local LEVEL_BOUND_RECEIVED = "__level_bound_received" +local LEVEL_MIN = "__level_min" +local LEVEL_MAX = "__level_max" local VOLUME_STEP = 5 @@ -116,9 +111,25 @@ local function on_off_attr_handler(driver, device, ib, response) end local function level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - local volume = math.floor((ib.data.value / 254.0 * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.audioVolume.volume(volume)) + if ib.data.value == nil then return end + local min_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN) or 0 + local max_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX) or 254 + -- Convert level (0-254) to volume (0-100), taking reported min and max level values into account + local volume = st_utils.round(((ib.data.value - min_volume) * 100) / (max_volume - min_volume)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.audioVolume.volume(volume)) +end + +function level_bounds_handler_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then return end + device:set_field(LEVEL_BOUND_RECEIVED..minOrMax, ib.data.value) + local min = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN) + local max = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX) + if min ~= nil and max ~= nil and (min < 0 or max > 254 or min >= max) then + device.log.warn_with({hub_logs = true}, string.format("Device reported invalid min level value [%d] or max level value [%d]", min, max)) + device:set_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX, nil) + device:set_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN, nil) + end end end @@ -163,7 +174,10 @@ end local function handle_set_volume(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.volume/100.0 * 254) + local min_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN) or 0 + local max_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX) or 254 + -- Convert volume (0-100) to level (0-254), taking reported min and max level values into account + local level = st_utils.round((cmd.args.volume * (max_volume - min_volume)) / 100) + min_volume local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0, 0) device:send(req) end @@ -231,7 +245,9 @@ local matter_driver_template = { [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, }, [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler + [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler, + [clusters.LevelControl.attributes.MaxLevel.ID] = level_bounds_handler_factory(LEVEL_MAX), + [clusters.LevelControl.attributes.MinLevel.ID] = level_bounds_handler_factory(LEVEL_MIN), }, [clusters.MediaPlayback.ID] = { [clusters.MediaPlayback.attributes.CurrentState.ID] = media_playback_state_attr_handler, @@ -246,7 +262,9 @@ local matter_driver_template = { clusters.OnOff.attributes.OnOff }, [capabilities.audioVolume.ID] = { - clusters.LevelControl.attributes.CurrentLevel + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, }, [capabilities.mediaPlayback.ID] = { clusters.MediaPlayback.attributes.CurrentState, diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua index cf5bc44648..2730428238 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua @@ -1,21 +1,11 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local test = require "integration_test" local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local t_utils = require "integration_test.utils" +local test = require "integration_test" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("media-speaker.yml"), @@ -56,6 +46,8 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device) subscribe_request:merge(clusters.LevelControl.attributes.CurrentLevel:subscribe(mock_device)) + subscribe_request:merge(clusters.LevelControl.attributes.MinLevel:subscribe(mock_device)) + subscribe_request:merge(clusters.LevelControl.attributes.MaxLevel:subscribe(mock_device)) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) @@ -181,7 +173,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0) } }, { @@ -208,6 +200,57 @@ test.register_message_test( } ) +test.register_message_test( + "Set volume command should send the appropriate commands with given MinLevel and MaxLevel attribute values", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 50) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 200) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 60 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round((60 * (200 - 50) / 100.0) + 50), 0, 0, 0) -- 140 + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 110) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(st_utils.round(((110 - 50) * 100) / (200 - 50)))) -- 40% + } + } +) + test.register_message_test( "Volume up/down command should send the appropriate commands", { @@ -224,7 +267,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0) } }, { @@ -262,7 +305,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(25/100.0 * 254), 0, 0, 0) } }, { @@ -300,7 +343,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0) } }, { @@ -330,6 +373,8 @@ test.register_message_test( local function refresh_commands(dev) local req = clusters.OnOff.attributes.OnOff:read(dev) req:merge(clusters.LevelControl.attributes.CurrentLevel:read(dev)) + req:merge(clusters.LevelControl.attributes.MinLevel:read(dev)) + req:merge(clusters.LevelControl.attributes.MaxLevel:read(dev)) return req end diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua index ebae93ca53..108bfc33d1 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities"