Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c484f14

Browse files
committedDec 18, 2024·
feat(matter): add new matter endpoint for thermostat
1 parent 6bf6df2 commit c484f14

File tree

7 files changed

+862
-0
lines changed

7 files changed

+862
-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: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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, true);
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("Initial Setpoints are %.01fC to %.01fC with a minimum 2.5C difference\r\n", SimulatedThermostat.getHeatingSetpoint(), SimulatedThermostat.getCoolingSetpoint());
109+
Serial.printf("Auto mode is ON. Initial Temperature of %.01fC \r\n", SimulatedThermostat.getLocalTemperature());
110+
Serial.println("Local Temperature Sensor will be simulated every 10 seconds and changed by a simulated heater and cooler to move in between setpoints.");
111+
}
112+
}
113+
114+
// This will simulate the thermostat control system (heating and cooling)
115+
// User can set a local temperature using the Serial input (type a number and press Enter)
116+
// New temperature can be an positive or negative temperature in Celsius, between -50C and 50C
117+
// Initial local temperature is 10C as defined in getSimulatedTemperature() function
118+
void readSerialForNewTemperature() {
119+
static String newTemperatureStr;
120+
121+
while (Serial.available()) {
122+
char c = Serial.read();
123+
if (c == '\n' || c == '\r') {
124+
if (newTemperatureStr.length() > 0) {
125+
// convert the string to a float value
126+
float newTemperature = newTemperatureStr.toFloat();
127+
// check if the new temperature is valid
128+
if (newTemperature >= -50.0 && newTemperature <= 50.0) {
129+
// set the new temperature
130+
SimulatedThermostat.setLocalTemperature(newTemperature);
131+
Serial.printf("New Temperature is %.01fC\r\n", newTemperature);
132+
} else {
133+
Serial.println("Invalid Temperature value. Please type a number between -50 and 50");
134+
}
135+
newTemperatureStr = "";
136+
}
137+
} else {
138+
if (c == '+' || c == '-' || (c >= '0' && c <= '9') || c == '.') {
139+
newTemperatureStr += c;
140+
} else {
141+
Serial.println("Invalid character. Please type a number between -50 and 50");
142+
newTemperatureStr = "";
143+
}
144+
}
145+
}
146+
}
147+
148+
// loop will simulate the thermostat control system
149+
// User can set a local temperature using the Serial input (type a number and press Enter)
150+
// User can change the thermostat mode using the Matter APP (smartphone)
151+
// The loop will simulate a heating and cooling system and the associated local temperature change
152+
void loop() {
153+
static uint32_t timeCounter = 0;
154+
155+
// Simulate the heating and cooling systems
156+
static bool isHeating = false;
157+
static bool isCooling = false;
158+
159+
// check if a new temperature is typed in the Serial Monitor
160+
readSerialForNewTemperature();
161+
162+
// simulate thermostat with heating/cooling system and the associated local temperature change, every 10s
163+
if (!(timeCounter++ % 20)) { // delaying for 500ms x 20 = 10s
164+
float localTemperature = getSimulatedTemperature(isHeating, isCooling);
165+
// Print the current thermostat local temperature value
166+
Serial.printf("Current Local Temperature is %.01fC\r\n", localTemperature);
167+
SimulatedThermostat.setLocalTemperature(localTemperature); // publish the new temperature value
168+
169+
// Simulate the thermostat control system - User has 4 modes: OFF, HEAT, COOL, AUTO
170+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_OFF) {
171+
// turn off the heating and cooling systems
172+
isHeating = false;
173+
isCooling = false;
174+
}
175+
// User APP has set the thermostat to AUTO mode -- keeping the tempeature between both setpoints
176+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_AUTO) {
177+
// check if the heating system should be turned on or off
178+
if (localTemperature < SimulatedThermostat.getHeatingSetpoint() + SimulatedThermostat.getDeadBand()) {
179+
// turn on the heating system and turn off the cooling system
180+
isHeating = true;
181+
isCooling = false;
182+
}
183+
if (localTemperature > SimulatedThermostat.getCoolingSetpoint() - SimulatedThermostat.getDeadBand()) {
184+
// turn off the heating system and turn on the cooling system
185+
isHeating = false;
186+
isCooling = true;
187+
}
188+
}
189+
// User APP has set the thermostat to AUTO mode -- keeping the tempeature between both setpoints
190+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_AUTO) {
191+
// check if the heating system should be turned on or off
192+
if (localTemperature < SimulatedThermostat.getHeatingSetpoint() + SimulatedThermostat.getDeadBand()) {
193+
// turn on the heating system and turn off the cooling system
194+
isHeating = true;
195+
isCooling = false;
196+
}
197+
if (localTemperature > SimulatedThermostat.getCoolingSetpoint() - SimulatedThermostat.getDeadBand()) {
198+
// turn off the heating system and turn on the cooling system
199+
isHeating = false;
200+
isCooling = true;
201+
}
202+
}
203+
// Simulate the heating system - User has turned the heating system ON
204+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_HEAT) {
205+
isHeating = true;
206+
isCooling = false; // keep the cooling system off as it is in heating mode
207+
// when the heating system is in HEATING mode, it will be turned off as soon as the local temperature is above the setpoint
208+
if (localTemperature > SimulatedThermostat.getHeatingSetpoint()) {
209+
// turn off the heating system
210+
isHeating = false;
211+
}
212+
}
213+
// Simulate the cooling system - User has turned the cooling system ON
214+
if (SimulatedThermostat.getMode() == MatterThermostat::THERMOSTAT_MODE_COOL) {
215+
isCooling = true;
216+
isHeating = false; // keep the heating system off as it is in cooling mode
217+
// when the cooling system is in COOLING mode, it will be turned off as soon as the local temperature is bellow the setpoint
218+
if (localTemperature < SimulatedThermostat.getCoolingSetpoint()) {
219+
// turn off the cooling system
220+
isCooling = false;
221+
}
222+
}
223+
224+
// Reporting Heating and Cooling status
225+
Serial.printf("\tThermostat Mode: %s >>> Heater is %s -- Cooler is %s\r\n", MatterThermostat::getThermostatModeString(SimulatedThermostat.getMode()), isHeating ? "ON" : "OFF", isCooling ? "ON" : "OFF");
226+
}
227+
228+
// Check if the button has been pressed
229+
if (digitalRead(buttonPin) == LOW && !button_state) {
230+
// deals with button debouncing
231+
button_time_stamp = millis(); // record the time while the button is pressed.
232+
button_state = true; // pressed.
233+
}
234+
235+
if (digitalRead(buttonPin) == HIGH && button_state) {
236+
button_state = false; // released
237+
}
238+
239+
// Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node
240+
uint32_t time_diff = millis() - button_time_stamp;
241+
if (button_state && time_diff > decommissioningTimeout) {
242+
Serial.println("Decommissioning the Light Matter Accessory. It shall be commissioned again.");
243+
Matter.decommission();
244+
button_time_stamp = millis(); // avoid running decommissining again, reboot takes a second or so
245+
}
246+
247+
delay(500);
248+
}
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ 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
39+
EndPointTemperatureCB KEYWORD1
2740

2841
#######################################
2942
# Methods and Functions (KEYWORD2)
@@ -78,6 +91,24 @@ setPressure KEYWORD2
7891
getPressure KEYWORD2
7992
setOccupancy KEYWORD2
8093
getOccupancy KEYWORD2
94+
getControlSequence KEYWORD2
95+
getMinHeatSetpoint KEYWORD2
96+
getMaxHeatSetpoint KEYWORD2
97+
getMinCoolSetpoint KEYWORD2
98+
getMaxCoolSetpoint KEYWORD2
99+
getDeadBand KEYWORD2
100+
setCoolingSetpoint KEYWORD2
101+
getCoolingSetpoint KEYWORD2
102+
setHeatingSetpoint KEYWORD2
103+
getHeatingSetpoint KEYWORD2
104+
setCoolingHeatingSetpoints KEYWORD2
105+
setLocalTemperature KEYWORD2
106+
getLocalTemperature KEYWORD2
107+
getThermostatModeString KEYWORD2
108+
onChangeMode KEYWORD2
109+
onChangeLocalTemperature KEYWORD2
110+
onChangeCoolingSetpoint KEYWORD2
111+
onChangeHeatingSetpoint KEYWORD2
81112

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

0 commit comments

Comments
 (0)
Please sign in to comment.