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"