Skip to content

Commit e3cc040

Browse files
SuGliderpre-commit-ci-lite[bot]
andauthoredJan 7, 2025··
feat(matter): new Matter Endpoint for Thermostat (#10755)
* feat(matter): add new matter endpoint for thermostat * fix(matter): not used variable from log_e() message * feat(matter): adds specifc type name for thermostat auto mode enabled * fix(matter): suggested changes in pr review * feat(matter): added the whole list of thermostat operational modes * fix(matter): fixed type in thermostat operational modes * ci(pre-commit): Apply automatic fixes * fix(matter): typos caught by CI codespell --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent b07eb17 commit e3cc040

File tree

7 files changed

+872
-0
lines changed

7 files changed

+872
-0
lines changed
 

‎CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ set(ARDUINO_LIBRARY_Matter_SRCS
181181
libraries/Matter/src/MatterEndpoints/MatterPressureSensor.cpp
182182
libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp
183183
libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp
184+
libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp
184185
libraries/Matter/src/Matter.cpp)
185186

186187
set(ARDUINO_LIBRARY_PPP_SRCS
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
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.
14+
15+
/*
16+
This example is an example code that will create a Matter Device which can be
17+
commissioned and controlled from a Matter Environment APP.
18+
Additionally the ESP32 will send debug messages indicating the Matter activity.
19+
Turning DEBUG Level ON may be useful to following Matter Accessory and Controller messages.
20+
*/
21+
22+
// Matter Manager
23+
#include <Matter.h>
24+
#include <WiFi.h>
25+
26+
// List of Matter Endpoints for this Node
27+
// Matter Thermostat Endpoint
28+
MatterThermostat SimulatedThermostat;
29+
30+
// WiFi is manually set and started
31+
const char *ssid = "your-ssid"; // Change this to your WiFi SSID
32+
const char *password = "your-password"; // Change this to your WiFi password
33+
34+
// set your board USER BUTTON pin here - decommissioning button
35+
const uint8_t buttonPin = BOOT_PIN; // Set your pin here. Using BOOT Button.
36+
37+
// Button control - decommision the Matter Node
38+
uint32_t button_time_stamp = 0; // debouncing control
39+
bool button_state = false; // false = released | true = pressed
40+
const uint32_t decommissioningTimeout = 5000; // keep the button pressed for 5s, or longer, to decommission
41+
42+
// Simulate a system that will activate heating/cooling in addition to a temperature sensor - add your preferred code here
43+
float getSimulatedTemperature(bool isHeating, bool isCooling) {
44+
// read sensor temperature and apply heating/cooling
45+
float simulatedTempHWSensor = SimulatedThermostat.getLocalTemperature();
46+
47+
if (isHeating) {
48+
// it will increase to simulate a heating system
49+
simulatedTempHWSensor = simulatedTempHWSensor + 0.5;
50+
}
51+
if (isCooling) {
52+
// it will decrease to simulate a colling system
53+
simulatedTempHWSensor = simulatedTempHWSensor - 0.5;
54+
}
55+
// otherwise, it will keep the temperature stable
56+
return simulatedTempHWSensor;
57+
}
58+
59+
void setup() {
60+
// Initialize the USER BUTTON (Boot button) that will be used to decommission the Matter Node
61+
pinMode(buttonPin, INPUT_PULLUP);
62+
63+
Serial.begin(115200);
64+
65+
// Manually connect to WiFi
66+
WiFi.begin(ssid, password);
67+
// Wait for connection
68+
while (WiFi.status() != WL_CONNECTED) {
69+
delay(500);
70+
Serial.print(".");
71+
}
72+
Serial.println();
73+
74+
// Simulated Thermostat in COOLING and HEATING mode with Auto Mode to keep the temperature between setpoints
75+
// Auto Mode can only be used when the control sequence of operation is Cooling & Heating
76+
SimulatedThermostat.begin(MatterThermostat::THERMOSTAT_SEQ_OP_COOLING_HEATING, MatterThermostat::THERMOSTAT_AUTO_MODE_ENABLED);
77+
78+
// Matter beginning - Last step, after all EndPoints are initialized
79+
Matter.begin();
80+
81+
// Check Matter Accessory Commissioning state, which may change during execution of loop()
82+
if (!Matter.isDeviceCommissioned()) {
83+
Serial.println("");
84+
Serial.println("Matter Node is not commissioned yet.");
85+
Serial.println("Initiate the device discovery in your Matter environment.");
86+
Serial.println("Commission it to your Matter hub with the manual pairing code or QR code");
87+
Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str());
88+
Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str());
89+
// waits for Matter Thermostat Commissioning.
90+
uint32_t timeCount = 0;
91+
while (!Matter.isDeviceCommissioned()) {
92+
delay(100);
93+
if ((timeCount++ % 50) == 0) { // 50*100ms = 5 sec
94+
Serial.println("Matter Node not commissioned yet. Waiting for commissioning.");
95+
}
96+
}
97+
Serial.println("Matter Node is commissioned and connected to Wi-Fi. Ready for use.");
98+
99+
// after commissioning, set initial thermostat parameters
100+
// start the thermostat in AUTO mode
101+
SimulatedThermostat.setMode(MatterThermostat::THERMOSTAT_MODE_AUTO);
102+
// cooling setpoint must be lower than heating setpoint by at least 2.5C (deadband), in auto mode
103+
SimulatedThermostat.setCoolingHeatingSetpoints(20.0, 23.00); // the target cooler and heating setpoint
104+
// set the local temperature sensor in Celsius
105+
SimulatedThermostat.setLocalTemperature(12.50);
106+
107+
Serial.println();
108+
Serial.printf(
109+
"Initial Setpoints are %.01fC to %.01fC with a minimum 2.5C difference\r\n", SimulatedThermostat.getHeatingSetpoint(),
110+
SimulatedThermostat.getCoolingSetpoint()
111+
);
112+
Serial.printf("Auto mode is ON. Initial Temperature of %.01fC \r\n", SimulatedThermostat.getLocalTemperature());
113+
Serial.println("Local Temperature Sensor will be simulated every 10 seconds and changed by a simulated heater and cooler to move in between setpoints.");
114+
}
115+
}
116+
117+
// This will simulate the thermostat control system (heating and cooling)
118+
// User can set a local temperature using the Serial input (type a number and press Enter)
119+
// New temperature can be an positive or negative temperature in Celsius, between -50C and 50C
120+
// Initial local temperature is 10C as defined in getSimulatedTemperature() function
121+
void readSerialForNewTemperature() {
122+
static String newTemperatureStr;
123+
124+
while (Serial.available()) {
125+
char c = Serial.read();
126+
if (c == '\n' || c == '\r') {
127+
if (newTemperatureStr.length() > 0) {
128+
// convert the string to a float value
129+
float newTemperature = newTemperatureStr.toFloat();
130+
// check if the new temperature is valid
131+
if (newTemperature >= -50.0 && newTemperature <= 50.0) {
132+
// set the new temperature
133+
SimulatedThermostat.setLocalTemperature(newTemperature);
134+
Serial.printf("New Temperature is %.01fC\r\n", newTemperature);
135+
} else {
136+
Serial.println("Invalid Temperature value. Please type a number between -50 and 50");
137+
}
138+
newTemperatureStr = "";
139+
}
140+
} else {
141+
if (c == '+' || c == '-' || (c >= '0' && c <= '9') || c == '.') {
142+
newTemperatureStr += c;
143+
} else {
144+
Serial.println("Invalid character. Please type a number between -50 and 50");
145+
newTemperatureStr = "";
146+
}
147+
}
148+
}
149+
}
150+
151+
// loop will simulate the thermostat control system
152+
// User can set a local temperature using the Serial input (type a number and press Enter)
153+
// User can change the thermostat mode using the Matter APP (smartphone)
154+
// The loop will simulate a heating and cooling system and the associated local temperature change
155+
void loop() {
156+
static uint32_t timeCounter = 0;
157+
158+
// Simulate the heating and cooling systems
159+
static bool isHeating = false;
160+
static bool isCooling = false;
161+
162+
// check if a new temperature is typed in the Serial Monitor
163+
readSerialForNewTemperature();
164+
165+
// simulate thermostat with heating/cooling system and the associated local temperature change, every 10s
166+
if (!(timeCounter++ % 20)) { // delaying for 500ms x 20 = 10s
167+
float localTemperature = getSimulatedTemperature(isHeating, isCooling);
168+
// Print the current thermostat local temperature value
169+
Serial.printf("Current Local Temperature is %.01fC\r\n", localTemperature);
170+
SimulatedThermostat.setLocalTemperature(localTemperature); // publish the new temperature value
171+
172+
// Simulate the thermostat control system - User has 4 modes: OFF, HEAT, COOL, AUTO
173+
switch (SimulatedThermostat.getMode()) {
174+
case MatterThermostat::THERMOSTAT_MODE_OFF:
175+
// turn off the heating and cooling systems
176+
isHeating = false;
177+
isCooling = false;
178+
break;
179+
case MatterThermostat::THERMOSTAT_MODE_AUTO:
180+
// User APP has set the thermostat to AUTO mode -- keeping the tempeature between both setpoints
181+
// check if the heating system should be turned on or off
182+
if (localTemperature < SimulatedThermostat.getHeatingSetpoint() + SimulatedThermostat.getDeadBand()) {
183+
// turn on the heating system and turn off the cooling system
184+
isHeating = true;
185+
isCooling = false;
186+
}
187+
if (localTemperature > SimulatedThermostat.getCoolingSetpoint() - SimulatedThermostat.getDeadBand()) {
188+
// turn off the heating system and turn on the cooling system
189+
isHeating = false;
190+
isCooling = true;
191+
}
192+
break;
193+
case MatterThermostat::THERMOSTAT_MODE_HEAT:
194+
// Simulate the heating system - User has turned the heating system ON
195+
isHeating = true;
196+
isCooling = false; // keep the cooling system off as it is in heating mode
197+
// when the heating system is in HEATING mode, it will be turned off as soon as the local temperature is above the setpoint
198+
if (localTemperature > SimulatedThermostat.getHeatingSetpoint()) {
199+
// turn off the heating system
200+
isHeating = false;
201+
}
202+
break;
203+
case MatterThermostat::THERMOSTAT_MODE_COOL:
204+
// Simulate the cooling system - User has turned the cooling system ON
205+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_COOL) {
206+
isCooling = true;
207+
isHeating = false; // keep the heating system off as it is in cooling mode
208+
// when the cooling system is in COOLING mode, it will be turned off as soon as the local temperature is bellow the setpoint
209+
if (localTemperature < SimulatedThermostat.getCoolingSetpoint()) {
210+
// turn off the cooling system
211+
isCooling = false;
212+
}
213+
}
214+
break;
215+
default: log_e("Invalid Thermostat Mode %d", SimulatedThermostat.getMode());
216+
}
217+
// Reporting Heating and Cooling status
218+
Serial.printf(
219+
"\tThermostat Mode: %s >>> Heater is %s -- Cooler is %s\r\n", MatterThermostat::getThermostatModeString(SimulatedThermostat.getMode()),
220+
isHeating ? "ON" : "OFF", isCooling ? "ON" : "OFF"
221+
);
222+
}
223+
// Check if the button has been pressed
224+
if (digitalRead(buttonPin) == LOW && !button_state) {
225+
// deals with button debouncing
226+
button_time_stamp = millis(); // record the time while the button is pressed.
227+
button_state = true; // pressed.
228+
}
229+
230+
if (digitalRead(buttonPin) == HIGH && button_state) {
231+
button_state = false; // released
232+
}
233+
234+
// Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node
235+
uint32_t time_diff = millis() - button_time_stamp;
236+
if (button_state && time_diff > decommissioningTimeout) {
237+
Serial.println("Decommissioning the Light Matter Accessory. It shall be commissioned again.");
238+
Matter.decommission();
239+
button_time_stamp = millis(); // avoid running decommissining again, reboot takes a second or so
240+
}
241+
242+
delay(500);
243+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"fqbn_append": "PartitionScheme=huge_app",
3+
"requires": [
4+
"CONFIG_SOC_WIFI_SUPPORTED=y",
5+
"CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y"
6+
]
7+
}

‎libraries/Matter/keywords.txt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ MatterContactSensor KEYWORD1
2424
MatterPressureSensor KEYWORD1
2525
MatterOccupancySensor KEYWORD1
2626
MatterOnOffPlugin KEYWORD1
27+
MatterThermostat KEYWORD1
28+
ControlSequenceOfOperation_t KEYWORD1
29+
ThermostatMode_t KEYWORD1
30+
EndPointCB KEYWORD1
31+
EndPointHeatingSetpointCB KEYWORD1
32+
EndPointCoolingSetpointCB KEYWORD1
33+
EndPointTemperatureCB KEYWORD1
34+
EndPointModeCB KEYWORD1
35+
EndPointSpeedCB KEYWORD1
36+
EndPointOnOffCB KEYWORD1
37+
EndPointBrightnessCB KEYWORD1
38+
EndPointRGBColorCB KEYWORD1
2739

2840
#######################################
2941
# Methods and Functions (KEYWORD2)
@@ -78,6 +90,24 @@ setPressure KEYWORD2
7890
getPressure KEYWORD2
7991
setOccupancy KEYWORD2
8092
getOccupancy KEYWORD2
93+
getControlSequence KEYWORD2
94+
getMinHeatSetpoint KEYWORD2
95+
getMaxHeatSetpoint KEYWORD2
96+
getMinCoolSetpoint KEYWORD2
97+
getMaxCoolSetpoint KEYWORD2
98+
getDeadBand KEYWORD2
99+
setCoolingSetpoint KEYWORD2
100+
getCoolingSetpoint KEYWORD2
101+
setHeatingSetpoint KEYWORD2
102+
getHeatingSetpoint KEYWORD2
103+
setCoolingHeatingSetpoints KEYWORD2
104+
setLocalTemperature KEYWORD2
105+
getLocalTemperature KEYWORD2
106+
getThermostatModeString KEYWORD2
107+
onChangeMode KEYWORD2
108+
onChangeLocalTemperature KEYWORD2
109+
onChangeCoolingSetpoint KEYWORD2
110+
onChangeHeatingSetpoint KEYWORD2
81111

82112
#######################################
83113
# Constants (LITERAL1)
@@ -104,3 +134,15 @@ FAN_MODE_SEQ_OFF_LOW_MED_HIGH_AUTO LITERAL1
104134
FAN_MODE_SEQ_OFF_LOW_HIGH_AUTO LITERAL1
105135
FAN_MODE_SEQ_OFF_HIGH_AUTO LITERAL1
106136
FAN_MODE_SEQ_OFF_HIGH LITERAL1
137+
THERMOSTAT_SEQ_OP_COOLING LITERAL1
138+
THERMOSTAT_SEQ_OP_COOLING_REHEAT LITERAL1
139+
THERMOSTAT_SEQ_OP_HEATING LITERAL1
140+
THERMOSTAT_SEQ_OP_HEATING_REHEAT LITERAL1
141+
THERMOSTAT_SEQ_OP_COOLING_HEATING LITERAL1
142+
THERMOSTAT_SEQ_OP_COOLING_HEATING_REHEAT LITERAL1
143+
THERMOSTAT_MODE_OFF LITERAL1
144+
THERMOSTAT_MODE_AUTO LITERAL1
145+
THERMOSTAT_MODE_COOL LITERAL1
146+
THERMOSTAT_MODE_HEAT LITERAL1
147+
THERMOSTAT_AUTO_MODE_DISABLED LITERAL1
148+
THERMOSTAT_AUTO_MODE_ENABLED LITERAL1

‎libraries/Matter/src/Matter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <MatterEndpoints/MatterPressureSensor.h>
3333
#include <MatterEndpoints/MatterOccupancySensor.h>
3434
#include <MatterEndpoints/MatterOnOffPlugin.h>
35+
#include <MatterEndpoints/MatterThermostat.h>
3536

3637
using namespace esp_matter;
3738

@@ -70,6 +71,7 @@ class ArduinoMatter {
7071
friend class MatterPressureSensor;
7172
friend class MatterOccupancySensor;
7273
friend class MatterOnOffPlugin;
74+
friend class MatterThermostat;
7375

7476
protected:
7577
static void _init();
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
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.
14+
15+
#include <sdkconfig.h>
16+
#ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL
17+
18+
#include <Matter.h>
19+
#include <MatterEndpoints/MatterThermostat.h>
20+
21+
using namespace esp_matter;
22+
using namespace esp_matter::endpoint;
23+
using namespace chip::app::Clusters;
24+
25+
// string helper for the THERMOSTAT MODE
26+
const char *MatterThermostat::thermostatModeString[5] = {"OFF", "AUTO", "UNKNOWN", "COOL", "HEAT"};
27+
28+
// endpoint for color light device
29+
namespace esp_matter {
30+
using namespace cluster;
31+
namespace endpoint {
32+
namespace multi_mode_thermostat {
33+
typedef struct config {
34+
cluster::descriptor::config_t descriptor;
35+
cluster::identify::config_t identify;
36+
cluster::scenes_management::config_t scenes_management;
37+
cluster::groups::config_t groups;
38+
cluster::thermostat::config_t thermostat;
39+
} config_t;
40+
41+
uint32_t get_device_type_id() {
42+
return ESP_MATTER_THERMOSTAT_DEVICE_TYPE_ID;
43+
}
44+
45+
uint8_t get_device_type_version() {
46+
return ESP_MATTER_THERMOSTAT_DEVICE_TYPE_VERSION;
47+
}
48+
49+
esp_err_t add(endpoint_t *endpoint, config_t *config) {
50+
if (!endpoint) {
51+
log_e("Endpoint cannot be NULL");
52+
return ESP_ERR_INVALID_ARG;
53+
}
54+
esp_err_t err = add_device_type(endpoint, get_device_type_id(), get_device_type_version());
55+
if (err != ESP_OK) {
56+
log_e("Failed to add device type id:%" PRIu32 ",err: %d", get_device_type_id(), err);
57+
return err;
58+
}
59+
60+
descriptor::create(endpoint, &(config->descriptor), CLUSTER_FLAG_SERVER);
61+
identify::create(endpoint, &(config->identify), CLUSTER_FLAG_SERVER);
62+
groups::create(endpoint, &(config->groups), CLUSTER_FLAG_SERVER);
63+
uint32_t thermostatFeatures = 0;
64+
switch (config->thermostat.control_sequence_of_operation) {
65+
case MatterThermostat::THERMOSTAT_SEQ_OP_COOLING:
66+
case MatterThermostat::THERMOSTAT_SEQ_OP_COOLING_REHEAT: thermostatFeatures = cluster::thermostat::feature::cooling::get_id(); break;
67+
case MatterThermostat::THERMOSTAT_SEQ_OP_HEATING:
68+
case MatterThermostat::THERMOSTAT_SEQ_OP_HEATING_REHEAT: thermostatFeatures = cluster::thermostat::feature::heating::get_id(); break;
69+
case MatterThermostat::THERMOSTAT_SEQ_OP_COOLING_HEATING:
70+
case MatterThermostat::THERMOSTAT_SEQ_OP_COOLING_HEATING_REHEAT:
71+
thermostatFeatures = cluster::thermostat::feature::cooling::get_id() | cluster::thermostat::feature::heating::get_id();
72+
break;
73+
}
74+
cluster::thermostat::create(endpoint, &(config->thermostat), CLUSTER_FLAG_SERVER, thermostatFeatures);
75+
return ESP_OK;
76+
}
77+
78+
endpoint_t *create(node_t *node, config_t *config, uint8_t flags, void *priv_data) {
79+
endpoint_t *endpoint = endpoint::create(node, flags, priv_data);
80+
add(endpoint, config);
81+
return endpoint;
82+
}
83+
} // namespace multi_mode_thermostat
84+
} // namespace endpoint
85+
} // namespace esp_matter
86+
87+
bool MatterThermostat::attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val) {
88+
bool ret = true;
89+
if (!started) {
90+
log_e("Matter Thermostat device has not begun.");
91+
return false;
92+
}
93+
log_d("Thermostat Attr update callback: endpoint: %u, cluster: %u, attribute: %u, val: %u", endpoint_id, cluster_id, attribute_id, val->val.u32);
94+
95+
if (cluster_id == Thermostat::Id) {
96+
switch (attribute_id) {
97+
case Thermostat::Attributes::SystemMode::Id:
98+
if (_onChangeModeCB != NULL) {
99+
ret &= _onChangeModeCB((ThermostatMode_t)val->val.u8);
100+
}
101+
if (_onChangeCB != NULL) {
102+
ret &= _onChangeCB();
103+
}
104+
if (ret == true) {
105+
currentMode = (ThermostatMode_t)val->val.u8;
106+
log_v("Thermostat Mode updated to %d", val->val.u8);
107+
}
108+
break;
109+
case Thermostat::Attributes::LocalTemperature::Id:
110+
if (_onChangeTemperatureCB != NULL) {
111+
ret &= _onChangeTemperatureCB((float)val->val.i16 / 100.00);
112+
}
113+
if (_onChangeCB != NULL) {
114+
ret &= _onChangeCB();
115+
}
116+
if (ret == true) {
117+
localTemperature = val->val.i16;
118+
log_v("Local Temperature updated to %.01fC", (float)val->val.i16 / 100.00);
119+
}
120+
break;
121+
case Thermostat::Attributes::OccupiedCoolingSetpoint::Id:
122+
if (_onChangeCoolingSetpointCB != NULL) {
123+
ret &= _onChangeCoolingSetpointCB((float)val->val.i16 / 100.00);
124+
}
125+
if (_onChangeCB != NULL) {
126+
ret &= _onChangeCB();
127+
}
128+
if (ret == true) {
129+
coolingSetpointTemperature = val->val.i16;
130+
log_v("Cooling Setpoint updated to %.01fC", (float)val->val.i16 / 100.00);
131+
}
132+
break;
133+
case Thermostat::Attributes::OccupiedHeatingSetpoint::Id:
134+
if (_onChangeHeatingSetpointCB != NULL) {
135+
ret &= _onChangeHeatingSetpointCB((float)val->val.i16 / 100.00);
136+
}
137+
if (_onChangeCB != NULL) {
138+
ret &= _onChangeCB();
139+
}
140+
if (ret == true) {
141+
heatingSetpointTemperature = val->val.i16;
142+
log_v("Heating Setpoint updated to %.01fC", (float)val->val.i16 / 100.00);
143+
}
144+
break;
145+
default: log_w("Unhandled Thermostat Attribute ID: %u", attribute_id); break;
146+
}
147+
}
148+
return ret;
149+
}
150+
151+
MatterThermostat::MatterThermostat() {}
152+
153+
MatterThermostat::~MatterThermostat() {
154+
end();
155+
}
156+
157+
bool MatterThermostat::begin(ControlSequenceOfOperation_t _controlSequence, ThermostatAutoMode_t _autoMode) {
158+
ArduinoMatter::_init();
159+
160+
if (getEndPointId() != 0) {
161+
log_e("Matter Thermostat with Endpoint Id %d device has already been created.", getEndPointId());
162+
return false;
163+
}
164+
165+
// check if auto mode is allowed with the control sequence of operation - only allowed for Cooling & Heating
166+
if (_autoMode == THERMOSTAT_AUTO_MODE_ENABLED && _controlSequence != THERMOSTAT_SEQ_OP_COOLING_HEATING
167+
&& _controlSequence != THERMOSTAT_SEQ_OP_COOLING_HEATING_REHEAT) {
168+
log_e("Thermostat in Auto Mode requires a Cooling & Heating Control Sequence of Operation.");
169+
return false;
170+
}
171+
172+
const int16_t _localTemperature = 2000; // initial value to be automatically changed by the Matter Thermostat
173+
const int16_t _coolingSetpointTemperature = 2400; // 24C cooling setpoint
174+
const int16_t _heatingSetpointTemperature = 1600; // 16C heating setpoint
175+
const ThermostatMode_t _currentMode = THERMOSTAT_MODE_OFF;
176+
177+
multi_mode_thermostat::config_t thermostat_config;
178+
thermostat_config.thermostat.control_sequence_of_operation = (uint8_t)_controlSequence;
179+
thermostat_config.thermostat.cooling.occupied_cooling_setpoint = _coolingSetpointTemperature;
180+
thermostat_config.thermostat.heating.occupied_heating_setpoint = _heatingSetpointTemperature;
181+
thermostat_config.thermostat.system_mode = (uint8_t)_currentMode;
182+
thermostat_config.thermostat.local_temperature = _localTemperature;
183+
184+
// endpoint handles can be used to add/modify clusters
185+
endpoint_t *endpoint = multi_mode_thermostat::create(node::get(), &thermostat_config, ENDPOINT_FLAG_NONE, (void *)this);
186+
if (endpoint == nullptr) {
187+
log_e("Failed to create Thermostat endpoint");
188+
return false;
189+
}
190+
if (_autoMode == THERMOSTAT_AUTO_MODE_ENABLED) {
191+
cluster_t *cluster = cluster::get(endpoint, Thermostat::Id);
192+
thermostat_config.thermostat.auto_mode.min_setpoint_dead_band = kDefaultDeadBand; // fixed by default to 2.5C
193+
cluster::thermostat::feature::auto_mode::add(cluster, &thermostat_config.thermostat.auto_mode);
194+
}
195+
196+
controlSequence = _controlSequence;
197+
autoMode = _autoMode;
198+
coolingSetpointTemperature = _coolingSetpointTemperature;
199+
heatingSetpointTemperature = _heatingSetpointTemperature;
200+
localTemperature = _localTemperature;
201+
currentMode = _currentMode;
202+
203+
setEndPointId(endpoint::get_id(endpoint));
204+
log_i("Thermostat created with endpoint_id %d", getEndPointId());
205+
started = true;
206+
return true;
207+
}
208+
209+
void MatterThermostat::end() {
210+
started = false;
211+
}
212+
213+
bool MatterThermostat::setMode(ThermostatMode_t _mode) {
214+
if (!started) {
215+
log_e("Matter Thermostat device has not begun.");
216+
return false;
217+
}
218+
219+
if (autoMode == THERMOSTAT_AUTO_MODE_DISABLED && _mode == THERMOSTAT_MODE_AUTO) {
220+
log_e("Thermostat can't set Auto Mode.");
221+
return false;
222+
}
223+
// check if the requested mode is valid based on the control sequence of operation
224+
// no restrictions for OFF mode
225+
if (_mode != THERMOSTAT_MODE_OFF) {
226+
// check HEAT, COOL and AUTO modes
227+
switch (controlSequence) {
228+
case THERMOSTAT_SEQ_OP_COOLING:
229+
case THERMOSTAT_SEQ_OP_COOLING_REHEAT:
230+
if (_mode == THERMOSTAT_MODE_HEAT || _mode == THERMOSTAT_MODE_AUTO) {
231+
break;
232+
}
233+
log_e("Invalid Thermostat Mode for Cooling Control Sequence of Operation.");
234+
return false;
235+
case THERMOSTAT_SEQ_OP_HEATING:
236+
case THERMOSTAT_SEQ_OP_HEATING_REHEAT:
237+
if (_mode == THERMOSTAT_MODE_COOL || _mode == THERMOSTAT_MODE_AUTO) {
238+
break;
239+
}
240+
log_e("Invalid Thermostat Mode for Heating Control Sequence of Operation.");
241+
return false;
242+
default:
243+
// compiler warning about not handling all enum values
244+
break;
245+
}
246+
}
247+
248+
// avoid processing if there was no change
249+
if (currentMode == _mode) {
250+
return true;
251+
}
252+
253+
esp_matter_attr_val_t modeVal = esp_matter_invalid(NULL);
254+
if (!getAttributeVal(Thermostat::Id, Thermostat::Attributes::SystemMode::Id, &modeVal)) {
255+
log_e("Failed to get Thermostat Mode Attribute.");
256+
return false;
257+
}
258+
if (modeVal.val.u8 != _mode) {
259+
modeVal.val.u8 = _mode;
260+
bool ret;
261+
ret = updateAttributeVal(Thermostat::Id, Thermostat::Attributes::SystemMode::Id, &modeVal);
262+
if (!ret) {
263+
log_e("Failed to update Thermostat Mode Attribute.");
264+
return false;
265+
}
266+
currentMode = _mode;
267+
}
268+
log_v("Thermostat Mode set to %d", _mode);
269+
270+
return true;
271+
}
272+
273+
bool MatterThermostat::setRawTemperature(int16_t _rawTemperature, uint32_t attribute_id, int16_t *internalValue) {
274+
if (!started) {
275+
log_e("Matter Thermostat device has not begun.");
276+
return false;
277+
}
278+
279+
// avoid processing if there was no change
280+
if (*internalValue == _rawTemperature) {
281+
return true;
282+
}
283+
284+
esp_matter_attr_val_t temperatureVal = esp_matter_invalid(NULL);
285+
if (!getAttributeVal(Thermostat::Id, attribute_id, &temperatureVal)) {
286+
log_e("Failed to get Thermostat Temperature or Setpoint Attribute.");
287+
return false;
288+
}
289+
if (temperatureVal.val.i16 != _rawTemperature) {
290+
temperatureVal.val.i16 = _rawTemperature;
291+
bool ret;
292+
ret = updateAttributeVal(Thermostat::Id, attribute_id, &temperatureVal);
293+
if (!ret) {
294+
log_e("Failed to update Thermostat Temperature or Setpoint Attribute.");
295+
return false;
296+
}
297+
*internalValue = _rawTemperature;
298+
}
299+
log_v("Temperature set to %.01fC", (float)_rawTemperature / 100.00);
300+
301+
return true;
302+
}
303+
304+
bool MatterThermostat::setCoolingHeatingSetpoints(double _setpointHeatingTemperature, double _setpointCollingTemperature) {
305+
// at least one of the setpoints must be valid
306+
bool settingCooling = _setpointCollingTemperature != (float)0xffff;
307+
bool settingHeating = _setpointHeatingTemperature != (float)0xffff;
308+
if (!settingCooling && !settingHeating) {
309+
log_e("Invalid Setpoints values. Set correctly at least one of them in Celsius.");
310+
return false;
311+
}
312+
int16_t _rawHeatValue = static_cast<int16_t>(_setpointHeatingTemperature * 100.0f);
313+
int16_t _rawCoolValue = static_cast<int16_t>(_setpointCollingTemperature * 100.0f);
314+
315+
// check limits for the setpoints
316+
if (settingHeating && (_rawHeatValue < kDefaultMinHeatSetpointLimit || _rawHeatValue > kDefaultMaxHeatSetpointLimit)) {
317+
log_e(
318+
"Invalid Heating Setpoint value: %.01fC - valid range %d..%d", _setpointHeatingTemperature, kDefaultMinHeatSetpointLimit / 100,
319+
kDefaultMaxHeatSetpointLimit / 100
320+
);
321+
return false;
322+
}
323+
if (settingCooling && (_rawCoolValue < kDefaultMinCoolSetpointLimit || _rawCoolValue > kDefaultMaxCoolSetpointLimit)) {
324+
log_e(
325+
"Invalid Cooling Setpoint value: %.01fC - valid range %d..%d", _setpointCollingTemperature, kDefaultMinCoolSetpointLimit / 100,
326+
kDefaultMaxCoolSetpointLimit / 100
327+
);
328+
return false;
329+
}
330+
331+
// AUTO mode requires both setpoints to be valid to each other and respect the deadband
332+
if (currentMode == THERMOSTAT_MODE_AUTO) {
333+
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR
334+
float deadband = getDeadBand();
335+
#endif
336+
// only setting Cooling Setpoint
337+
if (settingCooling && !settingHeating && _rawCoolValue < (heatingSetpointTemperature + (kDefaultDeadBand * 10))) {
338+
log_e(
339+
"AutoMode :: Invalid Cooling Setpoint value: %.01fC - must be higher or equal than %.01fC", _setpointCollingTemperature, getHeatingSetpoint() + deadband
340+
);
341+
return false;
342+
}
343+
// only setting Heating Setpoint
344+
if (!settingCooling && settingHeating && _rawHeatValue > (coolingSetpointTemperature - (kDefaultDeadBand * 10))) {
345+
log_e(
346+
"AutoMode :: Invalid Heating Setpoint value: %.01fC - must be lower or equal than %.01fC", _setpointHeatingTemperature, getCoolingSetpoint() - deadband
347+
);
348+
return false;
349+
}
350+
// setting both setpoints
351+
if (settingCooling && settingHeating && (_rawCoolValue <= _rawHeatValue || _rawCoolValue - _rawHeatValue < kDefaultDeadBand * 10.0)) {
352+
log_e(
353+
"AutoMode :: Error - Heating Setpoint %.01fC must be lower than Cooling Setpoint %.01fC with a minimum difference of %0.1fC",
354+
_setpointHeatingTemperature, _setpointCollingTemperature, deadband
355+
);
356+
return false;
357+
}
358+
}
359+
360+
bool ret = true;
361+
if (settingCooling) {
362+
ret &= setRawTemperature(_rawCoolValue, Thermostat::Attributes::OccupiedCoolingSetpoint::Id, &coolingSetpointTemperature);
363+
}
364+
if (settingHeating) {
365+
ret &= setRawTemperature(_rawHeatValue, Thermostat::Attributes::OccupiedHeatingSetpoint::Id, &heatingSetpointTemperature);
366+
}
367+
return ret;
368+
}
369+
370+
#endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
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.
14+
15+
#pragma once
16+
#include <sdkconfig.h>
17+
#ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL
18+
19+
#include <Matter.h>
20+
#include <MatterEndPoint.h>
21+
#include <app-common/zap-generated/cluster-enums.h>
22+
23+
using namespace chip::app::Clusters;
24+
25+
class MatterThermostat : public MatterEndPoint {
26+
public:
27+
// clang-format off
28+
enum ControlSequenceOfOperation_t {
29+
THERMOSTAT_SEQ_OP_COOLING = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kCoolingOnly,
30+
THERMOSTAT_SEQ_OP_COOLING_REHEAT = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kCoolingWithReheat,
31+
THERMOSTAT_SEQ_OP_HEATING = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kHeatingOnly,
32+
THERMOSTAT_SEQ_OP_HEATING_REHEAT = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kHeatingWithReheat,
33+
THERMOSTAT_SEQ_OP_COOLING_HEATING = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kCoolingAndHeating,
34+
THERMOSTAT_SEQ_OP_COOLING_HEATING_REHEAT = (uint8_t) Thermostat::ControlSequenceOfOperationEnum::kCoolingAndHeatingWithReheat,
35+
};
36+
37+
enum ThermostatMode_t {
38+
THERMOSTAT_MODE_OFF = (uint8_t) Thermostat::SystemModeEnum::kOff,
39+
THERMOSTAT_MODE_AUTO = (uint8_t) Thermostat::SystemModeEnum::kAuto,
40+
THERMOSTAT_MODE_COOL = (uint8_t) Thermostat::SystemModeEnum::kCool,
41+
THERMOSTAT_MODE_HEAT = (uint8_t) Thermostat::SystemModeEnum::kHeat,
42+
THERMOSTAT_MODE_EMERGENCY_HEAT = (uint8_t) Thermostat::SystemModeEnum::kEmergencyHeat,
43+
THERMOSTAT_MODE_PRECOOLING = (uint8_t) Thermostat::SystemModeEnum::kPrecooling,
44+
THERMOSTAT_MODE_FAN_ONLY = (uint8_t) Thermostat::SystemModeEnum::kFanOnly,
45+
THERMOSTAT_MODE_DRY = (uint8_t) Thermostat::SystemModeEnum::kDry,
46+
THERMOSTAT_MODE_SLEEP = (uint8_t) Thermostat::SystemModeEnum::kSleep
47+
};
48+
49+
enum ThermostatAutoMode_t {
50+
THERMOSTAT_AUTO_MODE_DISABLED = (uint8_t) Thermostat::SystemModeEnum::kOff,
51+
THERMOSTAT_AUTO_MODE_ENABLED = (uint8_t) Thermostat::SystemModeEnum::kAuto,
52+
};
53+
// clang-format on
54+
55+
MatterThermostat();
56+
~MatterThermostat();
57+
// begin Matter Thermostat endpoint with initial Operation Mode
58+
bool begin(ControlSequenceOfOperation_t controlSequence = THERMOSTAT_SEQ_OP_COOLING, ThermostatAutoMode_t autoMode = THERMOSTAT_AUTO_MODE_DISABLED);
59+
// this will stop processing Thermostat Matter events
60+
void end();
61+
62+
// set the Thermostat Mode
63+
bool setMode(ThermostatMode_t mode);
64+
// get the Thermostat Mode
65+
ThermostatMode_t getMode() {
66+
return currentMode;
67+
}
68+
// returns a friendly string for the Fan Mode
69+
static const char *getThermostatModeString(uint8_t mode) {
70+
return thermostatModeString[mode];
71+
}
72+
73+
// get the Thermostat Control Sequence of Operation
74+
ControlSequenceOfOperation_t getControlSequence() {
75+
return controlSequence;
76+
}
77+
78+
// get the minimum heating setpoint in 1/100th of a Celsio degree
79+
float getMinHeatSetpoint() {
80+
return (float)kDefaultMinHeatSetpointLimit / 100.00;
81+
}
82+
// get the maximum heating setpoint in 1/100th of a Celsio degree
83+
float getMaxHeatSetpoint() {
84+
return (float)kDefaultMaxHeatSetpointLimit / 100.00;
85+
}
86+
// get the minimum cooling setpoint in 1/100th of a Celsio degree
87+
float getMinCoolSetpoint() {
88+
return (float)kDefaultMinCoolSetpointLimit / 100.00;
89+
}
90+
// get the maximum cooling setpoint in 1/100th of a Celsio degree
91+
float getMaxCoolSetpoint() {
92+
return (float)kDefaultMaxCoolSetpointLimit / 100.00;
93+
}
94+
// get the deadband in 1/10th of a Celsio degree
95+
float getDeadBand() {
96+
return (float)kDefaultDeadBand / 10.00;
97+
}
98+
99+
// generic function for setting the cooling and heating setpoints - checks if the setpoints are valid
100+
// it can be used to set both setpoints at the same time or only one of them, by setting the other to (float)0xffff
101+
// Heating Setpoint must be lower than Cooling Setpoint
102+
// When using AUTO mode the Cooling Setpoint must be higher than Heating Setpoint by at least the 2.5C (deadband)
103+
// Thermostat Matter Server will enforce those rules and the Max/Min setpoints limits as in the Matter Specification
104+
bool setCoolingHeatingSetpoints(double _setpointHeatingTemperature, double _setpointCollingTemperature);
105+
106+
// set the heating setpoint in 1/100th of a Celsio degree
107+
bool setHeatingSetpoint(double _setpointHeatingTemperature) {
108+
return setCoolingHeatingSetpoints((double)0xffff, _setpointHeatingTemperature);
109+
}
110+
// get the heating setpoint in 1/100th of a Celsio degree
111+
double getHeatingSetpoint() {
112+
return heatingSetpointTemperature / 100.0;
113+
}
114+
// set the cooling setpoint in 1/100th of a Celsio degree
115+
bool setCoolingSetpoint(double _setpointCollingTemperature) {
116+
return setCoolingHeatingSetpoints(_setpointCollingTemperature, (double)0xffff);
117+
}
118+
// get the cooling setpoint in 1/100th of a Celsio degree
119+
double getCoolingSetpoint() {
120+
return coolingSetpointTemperature / 100.0;
121+
}
122+
123+
// set the local Thermostat temperature in Celsio degrees
124+
bool setLocalTemperature(double temperature) {
125+
// stores up to 1/100th of a Celsio degree precision
126+
int16_t rawValue = static_cast<int16_t>(temperature * 100.0f);
127+
return setRawTemperature(rawValue, Thermostat::Attributes::LocalTemperature::Id, &localTemperature);
128+
}
129+
// returns the local Thermostat float temperature with 1/100th of a Celsio degree precision
130+
double getLocalTemperature() {
131+
return (double)localTemperature / 100.0;
132+
}
133+
134+
// User Callback for whenever the Thermostat Mode is changed by the Matter Controller
135+
using EndPointModeCB = std::function<bool(ThermostatMode_t)>;
136+
void onChangeMode(EndPointModeCB onChangeCB) {
137+
_onChangeModeCB = onChangeCB;
138+
}
139+
140+
// User Callback for whenever the Local Temperature is changed by the Matter Controller
141+
using EndPointTemperatureCB = std::function<bool(float)>;
142+
void onChangeLocalTemperature(EndPointTemperatureCB onChangeCB) {
143+
_onChangeTemperatureCB = onChangeCB;
144+
}
145+
146+
// User Callback for whenever the Cooling or Heating Setpoint is changed by the Matter Controller
147+
using EndPointCoolingSetpointCB = std::function<bool(double)>;
148+
void onChangeCoolingSetpoint(EndPointCoolingSetpointCB onChangeCB) {
149+
_onChangeCoolingSetpointCB = onChangeCB;
150+
}
151+
152+
// User Callback for whenever the Cooling or Heating Setpoint is changed by the Matter Controller
153+
using EndPointHeatingSetpointCB = std::function<bool(double)>;
154+
void onChangeHeatingSetpoint(EndPointHeatingSetpointCB onChangeCB) {
155+
_onChangeHeatingSetpointCB = onChangeCB;
156+
}
157+
158+
// User Callback for whenever any parameter is changed by the Matter Controller
159+
// Main parameters are Thermostat Mode, Local Temperature, Cooling Setpoint and Heating Setpoint
160+
// Those can be obtained using getMode(), getTemperature(), getCoolingSetpoint() and getHeatingSetpoint()
161+
using EndPointCB = std::function<bool(void)>;
162+
void onChange(EndPointCB onChangeCB) {
163+
_onChangeCB = onChangeCB;
164+
}
165+
166+
// this function is called by Matter internal event processor. It could be overwritten by the application, if necessary.
167+
bool attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val);
168+
169+
protected:
170+
bool started = false;
171+
// implementation keeps temperature in 1/100th of a Celsio degree
172+
int16_t coolingSetpointTemperature = 2400; // 24C cooling setpoint
173+
int16_t localTemperature = 2000; // 20C local temperature
174+
int16_t heatingSetpointTemperature = 1600; // 16C heating setpoint
175+
176+
ThermostatMode_t currentMode = THERMOSTAT_MODE_OFF;
177+
ControlSequenceOfOperation_t controlSequence = THERMOSTAT_SEQ_OP_COOLING;
178+
ThermostatAutoMode_t autoMode = THERMOSTAT_AUTO_MODE_DISABLED;
179+
180+
EndPointModeCB _onChangeModeCB = NULL;
181+
EndPointTemperatureCB _onChangeTemperatureCB = NULL;
182+
EndPointCoolingSetpointCB _onChangeCoolingSetpointCB = NULL;
183+
EndPointHeatingSetpointCB _onChangeHeatingSetpointCB = NULL;
184+
EndPointCB _onChangeCB = NULL;
185+
186+
// internal function to set the raw temperature value (Matter Cluster)
187+
bool setRawTemperature(int16_t _rawTemperature, uint32_t attribute_id, int16_t *internalValue);
188+
189+
// clang-format off
190+
// Default Thermostat values - can't be changed - defined in the Thermostat Cluster Server code
191+
static const int16_t kDefaultAbsMinHeatSetpointLimit = 700; // 7C (44.5 F)
192+
static const int16_t kDefaultMinHeatSetpointLimit = 700; // 7C (44.5 F)
193+
static const int16_t kDefaultAbsMaxHeatSetpointLimit = 3000; // 30C (86 F)
194+
static const int16_t kDefaultMaxHeatSetpointLimit = 3000; // 30C (86 F)
195+
196+
static const int16_t kDefaultAbsMinCoolSetpointLimit = 1600; // 16C (61 F)
197+
static const int16_t kDefaultMinCoolSetpointLimit = 1600; // 16C (61 F)
198+
static const int16_t kDefaultAbsMaxCoolSetpointLimit = 3200; // 32C (90 F)
199+
static const int16_t kDefaultMaxCoolSetpointLimit = 3200; // 32C (90 F)
200+
201+
static const int8_t kDefaultDeadBand = 25; // 2.5C when in AUTO mode
202+
// clang-format on
203+
204+
// string helper for the THERMOSTAT MODE
205+
static const char *thermostatModeString[5];
206+
};
207+
#endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */

0 commit comments

Comments
 (0)
Please sign in to comment.