Repository: SmartThingsCommunity/SmartThingsEdgeDrivers
Pull request #2044: Early return and Re-read In Case of SystemMode Race Conditions
Author: login: hcarter-775
Created: 2025-04-02T16:24:53Z | Merged: 2025-04-03T16:05:02Z

# Description of Change
The current SystemMode / ControlSequenceofOperation handling expects for ControlSequenceofOperation to be handled first in all cases, but this is not necessarily the case. This change ensures a particular ordering by early exiting the SystemMode handler if a table that is initialized in the ControlSequenceofOperation handler has not been initialized yet. Then, in the ControlSequenceofOperation handler, it always does a read of SystemMode. At worst, this will cause a dual handling of a single SystemMode value, which will not do anything detrimental. Further, ControlSequenceofOperation should only be run on a subscription.  

# Summary of Completed Tests
Tested on device by SRPOL.
Uni tests updated.


Diff:
diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua
index d58f1971c4..57797c53a1 100644
--- a/drivers/SmartThings/matter-thermostat/src/init.lua
+++ b/drivers/SmartThings/matter-thermostat/src/init.lua
@@ -1073,6 +1073,11 @@ local function humidity_attr_handler(driver, device, ib, response)
 end
 
 local function system_mode_handler(driver, device, ib, response)
+  if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the sequence_of_operation_handler hasn't run.
+    device.log.info_with({hub_logs = true}, "In the SystemMode handler, ControlSequenceOfOperation has not run yet. Aborting.")
+    return
+  end
+
   local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {}
   -- check that the given mode was in the supported modes list
   if tbl_contains(supported_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then
@@ -1161,6 +1166,9 @@ local function sequence_of_operation_handler(driver, device, ib, response)
   device:set_field(DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations)
   local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}})
   device:emit_event_for_endpoint(ib.endpoint_id, event)
+
+  -- do an extra SystemMode read in case of out of order attribute handling
+  device:send(clusters.Thermostat.attributes.SystemMode:read(device))
 end
 
 local function min_deadband_limit_handler(driver, device, ib, response)
diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua
index 3a33e94251..ea859a6ea1 100644
--- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua
+++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua
@@ -327,6 +327,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -340,6 +348,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -391,6 +407,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -404,6 +428,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -417,6 +449,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "cool"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -430,6 +470,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -443,6 +491,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatMode.supportedThermostatModes({"off", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -456,6 +512,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatMode.supportedThermostatModes({"off", "cool"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
   }
 )
 
@@ -475,6 +539,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -488,6 +560,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -543,6 +623,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device_with_auto:generate_test_message("thermostatOne", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat", "auto"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_with_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -569,6 +657,14 @@ test.register_message_test(
         clusters.Thermostat.server.attributes.ControlSequenceOfOperation:build_test_report_data(mock_device_with_auto, THERMOSTAT_TWO_EP, 5)
       }
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_with_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "capability",
       direction = "send",
diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua
index 534d77926d..445badae9a 100644
--- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua
+++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua
@@ -322,6 +322,8 @@ test.register_message_test(
   }
 )
 
+--   test.socket.matter:__expect_send({mock_device_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_auto)})
+
 test.register_message_test(
   "Thermostat mode reports should generate correct messages",
   {
@@ -338,6 +340,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -371,6 +381,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -384,6 +402,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -397,6 +423,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
   }
 )
 
@@ -416,6 +450,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat", "auto"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -429,6 +471,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "heat", "auto"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -442,6 +492,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "auto"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
   },
   { test_init = test_init_auto }
 )
@@ -461,6 +519,14 @@ test.register_message_test(
       channel = "capability",
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
+    },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
     },
 		{
       channel = "matter",
@@ -498,6 +564,14 @@ test.register_message_test(
       channel = "capability",
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat", "auto"}, {visibility={displayed=false}}))
+    },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
     },
 		{
       channel = "matter",
@@ -536,6 +610,14 @@ test.register_message_test(
       channel = "capability",
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "heat", "auto"}, {visibility={displayed=false}}))
+    },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
     },
 		{
       channel = "matter",
@@ -557,6 +639,14 @@ test.register_message_test(
       channel = "capability",
       direction = "send",
       message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "auto"}, {visibility={displayed=false}}))
+    },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device_auto.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
     },
 		{
       channel = "matter",
diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua
index d8146257ef..d965b5e4bf 100644
--- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua
+++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua
@@ -282,6 +282,14 @@ test.register_message_test(
       direction = "send",
       message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -317,6 +325,14 @@ test.register_message_test(
       message = mock_device:generate_test_message("main",
         capabilities.thermostatMode.supportedThermostatModes({ "off", "cool", "heat" }, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -331,6 +347,14 @@ test.register_message_test(
       message = mock_device:generate_test_message("main",
         capabilities.thermostatMode.supportedThermostatModes({ "off", "heat" }, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",
@@ -345,6 +369,14 @@ test.register_message_test(
       message = mock_device:generate_test_message("main",
         capabilities.thermostatMode.supportedThermostatModes({ "off",  "cool" }, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
   }
 )
 
@@ -365,6 +397,14 @@ test.register_message_test(
       message = mock_device:generate_test_message("main",
         capabilities.thermostatMode.supportedThermostatModes({ "off", "cool", "heat" }, {visibility={displayed=false}}))
     },
+    {
+      channel = "matter",
+      direction = "send",
+      message = {
+        mock_device.id,
+        clusters.Thermostat.attributes.SystemMode:read()
+      }
+    },
     {
       channel = "matter",
       direction = "receive",


Reviews:
user: login: nickolas-deboom
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T15:42:59Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:18:52Z
user: login: ctowns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:30:32Z

Review comments:
diff_hunk: @@ -1073,6 +1073,11 @@ local function humidity_attr_handler(driver, device, ib, response)
 end
 
 local function system_mode_handler(driver, device, ib, response)
+  if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the sequence_of_operation_handler hasn't run.
+    device.log.info_with({hub_logs = true}, "In the SystemMode handler, ControlSequenceOfOperation has not run yet. Aborting.")
path: drivers/SmartThings/matter-thermostat/src/init.lua
user: login: ctowns
body: Why does this need to be info level in our hub logs?
created_at: 2025-04-03T16:18:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1161,6 +1166,9 @@ local function sequence_of_operation_handler(driver, device, ib, response)
   device:set_field(DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations)
   local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}})
   device:emit_event_for_endpoint(ib.endpoint_id, event)
+
+  -- do an extra SystemMode read in case of out of order attribute handling
+  device:send(clusters.Thermostat.attributes.SystemMode:read(device))
path: drivers/SmartThings/matter-thermostat/src/init.lua
user: login: ctowns
body: Is an extra read neccessary? We have all the information we need at this point, right? So couldn't we just call into a helper function with the relevant information such as the supported modes and the optional modes seen? The same helper function could be used by the attribute handler as well, I'm just not sure why we require an additional device read
created_at: 2025-04-03T16:30:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T16:26:41Z
body: ## Test Results
   66 files    420 suites   0s ⏱️
2 164 tests 2 164 ✅ 0 💤 0 ❌
3 685 runs  3 685 ✅ 0 💤 0 ❌

Results for commit 566e1a57.

[test-results]:data:application/gzip;base64,H4sIAI+s7mcC/2WMSwqDMAAFryJZd2HM115G8oVQNSWflfTuTYLSSJczjzcHsG41ETwHSh8DiNmlRngaC+ocRHJ+L6JimVIdJ0jxhUvMSv25l3v3n8UKt96ECcGH04S81yqinJx0RW/q12zcJRv3ReW3zaUCgFBqoCAMzkQijoyGQo4SMSm4xppAbKFmM2bg8wV/IV9aCgEAAA==

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-02T16:26:42Z
body: <strong></strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `85%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua | `50%` | :x: |
| /home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/matter-thermostat/src/init.lua | `86%` | :x: |

_Minimum allowed coverage is `90%`_

<p align="right">Generated by :monkey: cobertura-action against 566e1a57195b383ed1ab0b37ba8d4d514f1d7947 </p>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-02T16:41:23Z
body: Channel deleted.
author_association: NONE