Skip to content

Commit 9ec90e4

Browse files
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.
1 parent bddd262 commit 9ec90e4

File tree

3 files changed

+104
-52
lines changed

3 files changed

+104
-52
lines changed

drivers/SmartThings/matter-media/src/init.lua

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
-- Copyright 2022 SmartThings
2-
--
3-
-- Licensed under the Apache License, Version 2.0 (the "License");
4-
-- you may not use this file except in compliance with the License.
5-
-- You may obtain a copy of the License at
6-
--
7-
-- http://www.apache.org/licenses/LICENSE-2.0
8-
--
9-
-- Unless required by applicable law or agreed to in writing, software
10-
-- distributed under the License is distributed on an "AS IS" BASIS,
11-
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
-- See the License for the specific language governing permissions and
13-
-- limitations under the License.
1+
-- Copyright © 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
143

154
-- Opportunities for improvement:
165
-- * MediaInput cluster could be used to support the MediaSource capability.
176
-- * Channel cluster could be used to support the TvChannel capability.
187
-- * AdvancedSeek feature support.
8+
9+
local MatterDriver = require "st.matter.driver"
1910
local capabilities = require "st.capabilities"
2011
local clusters = require "st.matter.clusters"
21-
local MatterDriver = require "st.matter.driver"
12+
local st_utils = require "st.utils"
13+
14+
local LEVEL_BOUND_RECEIVED = "__level_bound_received"
15+
local LEVEL_MIN = "__level_min"
16+
local LEVEL_MAX = "__level_max"
2217

2318
local VOLUME_STEP = 5
2419

@@ -116,9 +111,25 @@ local function on_off_attr_handler(driver, device, ib, response)
116111
end
117112

118113
local function level_attr_handler(driver, device, ib, response)
119-
if ib.data.value ~= nil then
120-
local volume = math.floor((ib.data.value / 254.0 * 100) + 0.5)
121-
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.audioVolume.volume(volume))
114+
if ib.data.value == nil then return end
115+
local min_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN) or 0
116+
local max_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX) or 254
117+
-- Convert level (0-254) to volume (0-100), taking reported min and max level values into account
118+
local volume = st_utils.round(((ib.data.value - min_volume) * 100) / (max_volume - min_volume))
119+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.audioVolume.volume(volume))
120+
end
121+
122+
function level_bounds_handler_factory(minOrMax)
123+
return function(driver, device, ib, response)
124+
if ib.data.value == nil then return end
125+
device:set_field(LEVEL_BOUND_RECEIVED..minOrMax, ib.data.value)
126+
local min = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN)
127+
local max = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX)
128+
if min ~= nil and max ~= nil and (min < 0 or max > 254 or min >= max) then
129+
device.log.warn_with({hub_logs = true}, string.format("Device reported invalid min level value [%d] or max level value [%d]", min, max))
130+
device:set_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX, nil)
131+
device:set_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN, nil)
132+
end
122133
end
123134
end
124135

@@ -163,7 +174,10 @@ end
163174

164175
local function handle_set_volume(driver, device, cmd)
165176
local endpoint_id = device:component_to_endpoint(cmd.component)
166-
local level = math.floor(cmd.args.volume/100.0 * 254)
177+
local min_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MIN) or 0
178+
local max_volume = device:get_field(LEVEL_BOUND_RECEIVED..LEVEL_MAX) or 254
179+
-- Convert volume (0-100) to level (0-254), taking reported min and max level values into account
180+
local level = st_utils.round((cmd.args.volume * (max_volume - min_volume)) / 100) + min_volume
167181
local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0, 0)
168182
device:send(req)
169183
end
@@ -231,7 +245,9 @@ local matter_driver_template = {
231245
[clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler,
232246
},
233247
[clusters.LevelControl.ID] = {
234-
[clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler
248+
[clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler,
249+
[clusters.LevelControl.attributes.MaxLevel.ID] = level_bounds_handler_factory(LEVEL_MAX),
250+
[clusters.LevelControl.attributes.MinLevel.ID] = level_bounds_handler_factory(LEVEL_MIN),
235251
},
236252
[clusters.MediaPlayback.ID] = {
237253
[clusters.MediaPlayback.attributes.CurrentState.ID] = media_playback_state_attr_handler,
@@ -246,7 +262,9 @@ local matter_driver_template = {
246262
clusters.OnOff.attributes.OnOff
247263
},
248264
[capabilities.audioVolume.ID] = {
249-
clusters.LevelControl.attributes.CurrentLevel
265+
clusters.LevelControl.attributes.CurrentLevel,
266+
clusters.LevelControl.attributes.MaxLevel,
267+
clusters.LevelControl.attributes.MinLevel,
250268
},
251269
[capabilities.mediaPlayback.ID] = {
252270
clusters.MediaPlayback.attributes.CurrentState,

drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
1-
-- Copyright 2022 SmartThings
2-
--
3-
-- Licensed under the Apache License, Version 2.0 (the "License");
4-
-- you may not use this file except in compliance with the License.
5-
-- You may obtain a copy of the License at
6-
--
7-
-- http://www.apache.org/licenses/LICENSE-2.0
8-
--
9-
-- Unless required by applicable law or agreed to in writing, software
10-
-- distributed under the License is distributed on an "AS IS" BASIS,
11-
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
-- See the License for the specific language governing permissions and
13-
-- limitations under the License.
1+
-- Copyright © 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
143

15-
local test = require "integration_test"
164
local capabilities = require "st.capabilities"
17-
local t_utils = require "integration_test.utils"
185
local clusters = require "st.matter.clusters"
6+
local st_utils = require "st.utils"
7+
local t_utils = require "integration_test.utils"
8+
local test = require "integration_test"
199

2010
local mock_device = test.mock_device.build_test_matter_device({
2111
profile = t_utils.get_profile_definition("media-speaker.yml"),
@@ -56,6 +46,8 @@ local function test_init()
5646
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
5747
local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device)
5848
subscribe_request:merge(clusters.LevelControl.attributes.CurrentLevel:subscribe(mock_device))
49+
subscribe_request:merge(clusters.LevelControl.attributes.MinLevel:subscribe(mock_device))
50+
subscribe_request:merge(clusters.LevelControl.attributes.MaxLevel:subscribe(mock_device))
5951
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
6052

6153
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
@@ -181,7 +173,7 @@ test.register_message_test(
181173
direction = "send",
182174
message = {
183175
mock_device.id,
184-
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0)
176+
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0)
185177
}
186178
},
187179
{
@@ -208,6 +200,57 @@ test.register_message_test(
208200
}
209201
)
210202

203+
test.register_message_test(
204+
"Set volume command should send the appropriate commands with given MinLevel and MaxLevel attribute values",
205+
{
206+
{
207+
channel = "matter",
208+
direction = "receive",
209+
message = {
210+
mock_device.id,
211+
clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 50)
212+
}
213+
},
214+
{
215+
channel = "matter",
216+
direction = "receive",
217+
message = {
218+
mock_device.id,
219+
clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 200)
220+
}
221+
},
222+
{
223+
channel = "capability",
224+
direction = "receive",
225+
message = {
226+
mock_device.id,
227+
{ capability = "audioVolume", component = "main", command = "setVolume", args = { 60 } }
228+
}
229+
},
230+
{
231+
channel = "matter",
232+
direction = "send",
233+
message = {
234+
mock_device.id,
235+
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round((60 * (200 - 50) / 100.0) + 50), 0, 0, 0) -- 140
236+
}
237+
},
238+
{
239+
channel = "matter",
240+
direction = "receive",
241+
message = {
242+
mock_device.id,
243+
clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 110)
244+
}
245+
},
246+
{
247+
channel = "capability",
248+
direction = "send",
249+
message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(st_utils.round(((110 - 50) * 100) / (200 - 50)))) -- 40%
250+
}
251+
}
252+
)
253+
211254
test.register_message_test(
212255
"Volume up/down command should send the appropriate commands",
213256
{
@@ -224,7 +267,7 @@ test.register_message_test(
224267
direction = "send",
225268
message = {
226269
mock_device.id,
227-
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0)
270+
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0)
228271
}
229272
},
230273
{
@@ -262,7 +305,7 @@ test.register_message_test(
262305
direction = "send",
263306
message = {
264307
mock_device.id,
265-
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0)
308+
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(25/100.0 * 254), 0, 0, 0)
266309
}
267310
},
268311
{
@@ -300,7 +343,7 @@ test.register_message_test(
300343
direction = "send",
301344
message = {
302345
mock_device.id,
303-
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0)
346+
clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, st_utils.round(20/100.0 * 254), 0, 0, 0)
304347
}
305348
},
306349
{
@@ -330,6 +373,8 @@ test.register_message_test(
330373
local function refresh_commands(dev)
331374
local req = clusters.OnOff.attributes.OnOff:read(dev)
332375
req:merge(clusters.LevelControl.attributes.CurrentLevel:read(dev))
376+
req:merge(clusters.LevelControl.attributes.MinLevel:read(dev))
377+
req:merge(clusters.LevelControl.attributes.MaxLevel:read(dev))
333378
return req
334379
end
335380

drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
-- Copyright 2022 SmartThings
2-
--
3-
-- Licensed under the Apache License, Version 2.0 (the "License");
4-
-- you may not use this file except in compliance with the License.
5-
-- You may obtain a copy of the License at
6-
--
7-
-- http://www.apache.org/licenses/LICENSE-2.0
8-
--
9-
-- Unless required by applicable law or agreed to in writing, software
10-
-- distributed under the License is distributed on an "AS IS" BASIS,
11-
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
-- See the License for the specific language governing permissions and
13-
-- limitations under the License.
1+
-- Copyright © 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
143

154
local test = require "integration_test"
165
local capabilities = require "st.capabilities"

0 commit comments

Comments
 (0)