In a previous post, I tried to use Matter Scheduling in an ESP32 Thermostat project.

I was amazed to find there was no support for this in the Matter CHIP SDK. I decided to have a go myself. It’s an open source project after all.

TL;DR

I spent time trying to implement the new Matter Schedule feature of the Thermostat. I setup the repo and learned how to run the examples and tests locally. I then learned about Atomic Writes before raising a small PR.

An existing PR

During my investigations, I found that there was an existing PR for this feature, which appeared to be abandoned. I knew I had something to reference at least, but I decided to try myself to begin with.

I cloned the connectedhomeip repo and pulled it down onto my PC (I use WSL)

git clone --recurse-submodules https://github.com/tomasmcguinness/connectedhomeip.git

I then ran the following command

source scripts/activate.sh

When that completed, I compiled it

gn gen out/host
ninja -C out/host

I started by looking at the existing Presets code in the file src/app/clusters/thermostat-server/thermostat-server.cpp. This appears to be where the Thermostat server code is implemented.

Schedule Types

I started with the ScheduleTypes attribute.

Currently, the code returns an empty array.

return aEncoder.EncodeList([](const auto & encoder) -> CHIP_ERROR { return CHIP_NO_ERROR; });   

Using PresetTypes::Id as I guide, I added this

case ScheduleTypes::Id: {
    auto delegate = GetDelegate(aPath.mEndpointId);
    VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));

    return aEncoder.EncodeList([delegate](const auto & encoder) -> CHIP_ERROR {
        for (uint8_t i = 0; true; i++)
        {
            ScheduleTypeStruct::Type scheduleType;
            auto err = delegate->GetScheduleTypeAtIndex(i, scheduleType);
            if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED)
            {
                return CHIP_NO_ERROR;
            }
            ReturnErrorOnFailure(err);
            ReturnErrorOnFailure(encoder.Encode(scheduleType));
        }
    });
}

This method will find the appropriate delegate and then call GetScheduleTypeAtIndex over and over, encoding the results. It does that until it gets a CHIP_ERROR_PROVIDER_LIST_EXHAUSTED, which tells it there are no more types. I’d seen this pattern when implementing OperationalState and DishwasherModes in my Tiny Dishwasher.

C++ is such a scary syntax sometimes, but it all boils down to simple code usually.

I then added a delegate function definition to thermostat-delegate.h

virtual CHIP_ERROR GetScheduleTypeAtIndex(size_t index, Structs::ScheduleTypeStruct::Type & scheduleType) = 0;

At this point, the code all compiled.

Testing it

With ScheduleTypes in place, I now needed a way to actually test it worked. I found this section of the documentation:

https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#running-tests-locally

I started with

./scripts/build_python.sh -i out/python_env

Amazingly, that worked first time!

Next up was

source out/python_env/bin/activate

An example test to run was given like this

./scripts/tests/run_python_test.py --factory-reset --app ./out/linux-x64-light-no-ble/chip-lighting-app --app-args "--trace-to json:log" --script src/python_testing/TC_ACE_1_2.py --script-args "--commissioning-method on-network --qr-code MT:-24J0AFN00KA0648G00"

This returned an error

I guessed there was no “chip-lighting-app” to test against. After more digging, I found out how to compile it.

./scripts/build/build_examples.py --target linux-x64-light-no-ble build

With that compiled, I ran the python_test script again and it failed

After yet more reading, I discovered my python version was most likely the problem. I was on 3.10 and asyncio.Runner() needed 3.11 (According to DeepSeek at any rate)

After more than a few hours of trying to update Python, I eventually just updated Ubuntu to 24.04.

After recreating environments and recompiling stuff, I got some tests to execute! Amazing.

This meant I’d be able to run some tests against a thermostat example to ensure my changes were working.

Adding thermostat tests!

Similar to the light example, there is a thermostat example, so I tried to compile this. I knew it would fail, since I’d already added a new delegate method. I kicked off compilation using this command:

./scripts/build/build_examples.py --target linux-x64-thermostat-no-ble build

I think there is a patter to the target name, but I just guessed.

As expected, it error’d out.

To implement this method, I turned to the thermostat-common folder under the thermostat example.

I added a simple implementation

This then fixed the failing compilation. I now had a compiled thermostat.

With something compiled, I copied one of the existing test cases and added a simple test to it. A test that should fail, since it expects two values.

To run it, I amended the runner

./scripts/tests/run_python_test.py --factory-reset --app ./out/linux-x64-thermostat-no-ble/thermostat-app --app-args "--trace-to json:log" --script src/python_testing/TC_TSTAT_4_4.py --script-args "--commissioning-method on-network --qr-code MT:-24J0AFN00KA0648G00"

The python script executed, but it looked like it didn’t run my test. It appeared to skip it.

I commented out this line

if self.pics_guard(self.check_pics("TSTAT.S.F0a")):

and ran the script again

This time I got 2 executed, with 1 pass and 1 fail. Progress!

At this point, I realised that I probably didn’t have my thermostat setup with the Scheduling feature enabled.

Hmmm, how the hell did that all work??

Adding the new attributes

Since I was completely in the dark about how the ZAP thing worked, I referred to the existing Pull Request for some clues.

Searching the thermostat.zap file yielded results for “Presets” but nothing for schedules. I copied the value across from the PR.

{
   "name": "ScheduleTypes",
   "code": 73,
   "mfgCode": null,
   "side": "server",
   "type": "array",
   "included": 1,
   "storageOption": "External",
   "singleton": 0,
   "bounded": 0,
   "defaultValue": null,
   "reportable": 1,
   "minInterval": 1,
   "maxInterval": 65534,
   "reportableChange": 0
},

This yielded better errors.

Looking through the stack, this jumped out:

It was requesting endpoint 0x0, which is usually reserved from the Root Endpoint.

I hard-coded the endpoint to 1 in my test and ran it again. It failed, but this time no cluster errors! I amended the delegate implementation to return two schedule types

static ScheduleTypeStruct::Type scheduleTypes[] = {
        { .systemMode           = SystemModeEnum::kHeat,
          .numberOfSchedules    = 1,
          .scheduleTypeFeatures = to_underlying(ScheduleTypeFeaturesBitmap::kSupportsSetpoints) },
        { .systemMode           = SystemModeEnum::kCool,
          .numberOfSchedules    = 1,
          .scheduleTypeFeatures = to_underlying(ScheduleTypeFeaturesBitmap::kSupportsSetpoints) },
    };

and BOOM! Two passing tests.

With my tests working, I decided to go a step further. I started the example thermostat app and then paired it first with my iPhone and then the chip-tool running on an RPi.

I executed the “read” on the schedule types and bingo

Schedules

With my confidence growing, I returned the thermostat-server and started adding more properties to support schedules.

I added NumberOfSchedules to the delegate

uint8_t GetMaxAllowedNumberOfSchedules() = 0;

and an index method for schedules!

CHIP_ERROR GetScheduleAtIndex(size_t index, Structs::ScheduleStruct::Type & scheduleType) = 0;

With my bog standard “Read” appearing to work, it was time to move onto “Write” and I knew this wasn’t going to be easy.

I started by taking a copy of the Preset Write. This was doing what I wanted to do.

What followed was a lesson in how Matter hangs together 🤣

It started with this;

From what I could understand, the TLV decoder wasn’t able to work on the ScheduleStruct.

After first spending about a half editor editing the wrong SchedulStruct (I was editing a copy under an example), I found something.

Unlike some of the other structs, which did this:

using DecodableType = Type;

ScheduleStruct declares its own DecodableType. Hmmm. I updated ScheduleStruct to mirror PresetStruct and the error changed

Ah, that was why it had its own DecodableType. Because it needs to use the DecodableType of the ScheduleTransitionStruct. I tinkered with the Write method a little more and that got it compiling.

I recompiled the thermostat example and tried to write to the schedules using chip-tool. I got another *different* error response.

The cause of this error appears to come from this

The source of that error, I guessed, was this:

What the hell was an AtomicWrite?

Atomic Batteries to Power

So, it turns out, Atomic Writes are a part of the Matter Specification (Section 7.15).

You can start an Atomic Write, make several changes and then commit those changes. I didn’t even know this was a thing in Matter.

For starters, how would I know from the specification that Schedules is atomic? The quality column lists NT as the values.

This turns out to be two things

and, you guessed it….

I’m so glad I abandoned any efforts to build a .Net version of Matter. This spec is so dense!!

This chart helps explain it further

We start a request, make a few changes and then commit them. This explains why Presets have delegate methods like CommitPendingPresets and ClearPendingPresetList. Another penny drops.

After more reading and more logging, I got this command to work

chip-tool thermostat atomic-request 0 [81] 0x11 0x1 --Timeout 9000

This starts an atomic request for attribute 81 (0x51 which is schedules). To end the atomic request, we call it with 1 instead of 0 (CommitWrite vs BeginWrite).

chip-tool thermostat atomic-request 1 [81] 0x11 0x1

When CommitWrite is received, it will perform CommitPendingSchedules (which is empty right now). There is also a RollbackWrite (2), which would revert anything pending. This is making sense to me now.

With in the TC_TSTAT_4_2.py test file, the additional methods like check_atomic_response and send_atomic_request_begin_command also made sense. I was able to use these to write more tests.

Another mistake I spotted was that in the ThermostatAttrAccess::Read method, I was using the GetScheduleAtIndex delegate method twice. I needed a GetPendingScheduleAtIndex method to support the atomic write. It all fell into place now.

Starting Small

At this point, I had made a large number of changes and I had only scratched the surface.

The first rule of a good PR is that’s its as small as possible, to make the reviewer’s life easy. If I wanted any hope of getting my code into the CHIP SDK, I needed to small, well formed PR!

I created a new branch and added enough code for the ScheduleTypes attribute.

Let’s see how I get on!

It would be a nice feather in my cap if I could get some code into Matter 1.5 release.

UPDATE: A few hours later

I never updated the ZAP file to enable the Matter Schedule feature. This file lives in the thermostat-common folder

I did this by first launching the ZAP tool, which is a UI

../../../scripts/tools/zap/run_zaptool.sh thermostat.zap

I then navigated to the Thermostat cluster and switched on the MatterScheduleConfiguration (MSCH) feature

When prompted to turn on the attributes and commands, I said yes. After getting some advice, I should have said no!

Under the “Attributes” tab, I enabled only ScheduleTypes, which is the attribute is implemented. All the others had a warning. These are mandatory features, but I hadn’t implemented them, so I think they stay off.

I then ran this command to generate all the code and configuration. For some reason, you need to run this generate from the root folder.

scripts/tools/zap/generate.py ~/development/connectedhomeip/examples/thermostat/thermostat-common/thermostat.zap 

Committed and pushed and after many, many hours, and an update from master, all the build actions in the PR completed.

https://github.com/project-chip/connectedhomeip/pull/41160

UPDATE: 9th October 2025

The PR got it first approval It’s need two before it can be merged.

UPDATE: 28th October 2025

It has been merged! That’s quite a significant personal milestone for me I think. I have code in the official Matter SDK. Now I can start work on another one of the attributes. At this rate it will take it six months 🤣

Support

If you found this blog post useful and want to show your appreciation, you can always buy me a coffee or subscribe to my Patreon. Thanks!!

Buy Me a Coffee at ko-fi.com

Be sure to check out my YouTube channel.

One response

  1. […] recently, the Matter Schedules feature of the Thermostat isn’t implemented in the CHIP-SDK. I’m trying to add this by raising PRs, but it’s slow going […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.