ThermoStateMachine.cpp
11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#include "ThermoStateMachine.h"
static const char * TAG = "EasyOpenTherm ThermoStateMachine";
// Define a dead zone: the boiler will start heating if room temperature is below room setpoint minus lower dead zone to prevent oscillation or 'hunting'
#define ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE (0.1f)
// Define a dead zone: the boiler will start cooling if room temperature is aboce room setpoint plus higher dead zone (if the boiler can cool) to prevent oscillation or 'hunting'
#define ROOM_TEMPERATURE_SETPOINT_HIGHER_DEAD_ZONE (0.1f)
// Anti hunting tim einterval in seconds
#define ANTI_HUNTING_TIME_INTERVAL_S (600)
#define ROOM_TEMPERATURE_STALE_INTERVAL_S (900)
ThermoStateMachine::ThermoStateMachine() {
}
ThermoStateMachine::~ThermoStateMachine() {
}
bool ThermoStateMachine::connected() {
return _state != ThermostatState::WAITING_FOR_SECONDARY_CONFIGURATION;
}
void ThermoStateMachine::initPrimaryFlags(OpenTherm::CONFIGURATION_FLAGS configurationFlags) {
_configurationFlags = (uint8_t) configurationFlags;
_primaryFlags = 0;
// Enable DHW if boiler is capable of DHW
if(_configurationFlags & ((uint8_t) OpenTherm::CONFIGURATION_FLAGS::SECONDARY_DHW_PRESENT)) {
_primaryFlags |= (uint8_t) OpenTherm::STATUS_FLAGS::PRIMARY_DHW_ENABLE;
}
// If boiler is capable of cooling, signal this by setting 'canCool'
_canCool = (_configurationFlags & (uint8_t) OpenTherm::CONFIGURATION_FLAGS::SECONDARY_COOLING) != 0;
// Do not enable OTC
// _primaryFlags |= (uint8_t) OpenTherm::STATUS_FLAGS::PRIMARY_OTC_ENABLE;
_state = ThermostatState::OFF;
_state_c_str = "Off";
}
// Observe a dead zone around the room temperature setpoint of ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE below the setpoint and of ROOM_TEMPERATURE_SETPOINT_HIGHER_DEAD_ZONE above the setpoint
// If state is IDLE stay IDLE if the room temperature is in between roomTemperatureSetpoint - ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE and roomTemperatureSetpoint + ROOM_TEMPERATURE_SETPOINT_HIGHER_DEAD_ZONE
// UNLESS the room temperature setpoint is changed (indicating a user request for a change in temperature), change the state to HEATING or COOLING depending on the difference between actual room
// temperature and room temperature setpoint
// If state is OFF and either an 'ON'-request is set, or the room temperature setpoint is set change to IDLE
// in any state but OFF, if an 'OFF'-request is set change to OFF
// If state is IDLE and the room temperature drops below roomTemperatureSetpoint - ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE change the state to HEATING
// If state is IDLE and the room temperature rises above roomTemperatureSetpoint - ROOM_TEMPERATURE_SETPOINT_HIGHER_DEAD_ZONE change the state to COOLING
// If state is HEATING and the room temperature is equal or above the room temperature setpoint change state to ANTI_HUNTING
// If state is COOLING and the room temperature is equal or below the room temperature setpoint change state to ANTI_HUNTING
// If state is COOLING and 'canCool' is false change state to IDLE since there is nothing the boiler can do
// If state is ANTI_HUNTING for ANTI_HUNTING_INTERVAL_S seconds change state to IDLE
// If state is WAITING_FOR_SECONDARY_CONFIGURATION keep that state, no communcation has yet taken place between thermostat (primary) and boiler (secondary)
// Return true if the state has changed, false otherwise. Any state change MUST return true, because otherwise the caller of this function does not take any action
bool ThermoStateMachine::update(float roomTemperature,
uint32_t roomTemperatureTimestampS,
float roomTemperatureSetpoint,
ThermostatRequest request) {
_roomTemperatureStale = (_previousRoomTemperatureTimestampS != 0 && roomTemperatureTimestampS - _previousRoomTemperatureTimestampS > ROOM_TEMPERATURE_STALE_INTERVAL_S);
_previousRoomTemperatureTimestampS = roomTemperatureTimestampS;
roomTemperature = roundf(roomTemperature * 10.0f) / 10.0f; // Round to one decimal
roomTemperatureSetpoint = roundf(roomTemperatureSetpoint * 10.0f) / 10.0f; // Round to one decimal
// Record room temperature for use by other member functions
_roomTemperature = roomTemperature;
Serial.printf("updateState room temperature is %.01f, room temperature setpoint is %.01f, previous is %.01f\n", roomTemperature, roomTemperatureSetpoint, _previousRoomTemperatureSetpoint);
if(request == ThermostatRequest::OFF) {
// If an 'OFF' request is received and the thermoostat state is NOT OFF and NOT waiting for secondary configuration, change state to OFF
// If an 'OFF' request is received and the thermostat state was already off, the state does not change
// In both cases there is nothing more to do
if(_state != ThermostatState::OFF && _state != ThermostatState::WAITING_FOR_SECONDARY_CONFIGURATION) {
_state = ThermostatState::OFF;
return true;
}
return false;
}
// If an 'ON' request is received and the thermostate state is OFF, change state to IDLE (IDLE implies that the boiler is NOT switched on,
// unless in te NEXT run room temperature and room temperature setpoint imply to turn on heating or cooling)
// In all other cases the thermostat was already 'on' (any state but OFF), so the request is ignored
if(request == ThermostatRequest::AUTO || request == ThermostatRequest::HEAT || request == ThermostatRequest::COOL) {
if(_state == ThermostatState::OFF) {
_state = ThermostatState::IDLE;
return true;
}
}
// A change in roomtemperature setpoint is received in 'OFF' state; just record the new setpoint
if(_state == ThermostatState::OFF && roomTemperatureSetpoint != _previousRoomTemperatureSetpoint) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
return false;
}
if(_state == ThermostatState::IDLE) {
// A change in setpoint is directly honoured, without taking deadzones into regard unless the room temperature timestamp is stale
if(_roomTemperatureStale) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
return false;
}
if(roomTemperatureSetpoint != _previousRoomTemperatureSetpoint) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
if(roomTemperature < roomTemperatureSetpoint) {
if(request == ThermostatRequest::AUTO || request == ThermostatRequest::HEAT) {
_state = ThermostatState::HEATING;
return true;
} else {
// It is colder than the setpoint but heating is requested, so stay IDLE
return false;
}
} else if(roomTemperature > roomTemperatureSetpoint) {
if(_canCool && (request == ThermostatRequest::AUTO || request == ThermostatRequest::COOL)) {
_state = ThermostatState::COOLING;
return true;
} else {
// It is hotter than the setpoint but either cooling is not enabled or not requested, so stay IDLE
return false;
}
} else {
// Room temperature is exactly at setpoint, stay IDLE
return false;
}
}
if(roomTemperature >= roomTemperatureSetpoint - ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE && roomTemperature <= roomTemperatureSetpoint + ROOM_TEMPERATURE_SETPOINT_HIGHER_DEAD_ZONE) {
// Room temperature at setpoint plus or minus the dead zones, so stay idle
return false;
}
if(roomTemperature < roomTemperatureSetpoint - ROOM_TEMPERATURE_SETPOINT_LOWER_DEAD_ZONE) {
_state = ThermostatState::HEATING;
return true;
} else {
if(_canCool) {
_state = ThermostatState::COOLING;
return true;
} else {
return false; // It is too hot but cooling is not enabled. Stay IDLE
}
}
return false; // Not reached
} else if(_state == ThermostatState::HEATING) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
if(roomTemperature < roomTemperatureSetpoint && !_roomTemperatureStale) {
return false; // Keep on heating
} else {
_state = ThermostatState::ANTI_HUNTING;
_antiHuntingStartTimestampS = time(nullptr);
return true;
}
} else if(_state == ThermostatState::COOLING) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
if(roomTemperature > roomTemperatureSetpoint && !_roomTemperatureStale) {
return false; // Keep on cooling
} else {
_state = ThermostatState::ANTI_HUNTING;
_antiHuntingStartTimestampS = time(nullptr);
return true;
}
} else if(_state == ThermostatState::ANTI_HUNTING) {
if(time(nullptr) - _antiHuntingStartTimestampS > ANTI_HUNTING_TIME_INTERVAL_S) {
_state = ThermostatState::IDLE;
return true;
}
return false; // Stay in anti hunting state until enough time has passed
} else if(_state == ThermostatState::WAITING_FOR_SECONDARY_CONFIGURATION) {
_previousRoomTemperatureSetpoint = roomTemperatureSetpoint;
return false;
} else if(_state == ThermostatState::OFF) {
return false;
} else {
Serial.printf("Unknown state %d\n", _state);
return false;
}
}
void ThermoStateMachine::updatePrimaryFlags() {
switch(_state) {
case ThermostatState::OFF:
_state_c_str = "Off";
// Switch off the boiler by disabling both heating and cooling
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_COOLING_ENABLE);
break;
case ThermostatState::IDLE:
_state_c_str = "Idle";
// Switch off the boiler by disabling both heating and cooling (may already be done by state change to anti hunting)
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_COOLING_ENABLE);
break;
case ThermostatState::HEATING:
_state_c_str = "Heating";
// Switch on the boiler by enabling heating and disabling heating and cooling
_primaryFlags |= uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_COOLING_ENABLE);
break;
case ThermostatState::COOLING:
_state_c_str = "Cooling";
// Switch on the cooling capabolities of the boiler by disabling heating and enabling heating and cooling
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
_primaryFlags |= uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_COOLING_ENABLE);
break;
case ThermostatState::ANTI_HUNTING:
_state_c_str = "Anti hunting";
// Switch off the boiler by disabling both heating and cooling
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
_primaryFlags &= ~uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_COOLING_ENABLE);
break;
case ThermostatState::WAITING_FOR_SECONDARY_CONFIGURATION:
_state_c_str = "Waiting for seconday configuration";
break;
default:
_state_c_str = "Unknown";
break;
}
}
OpenTherm::STATUS_FLAGS ThermoStateMachine::getPrimaryFlags() {
return OpenTherm::STATUS_FLAGS(_primaryFlags);
}
ThermostatState ThermoStateMachine::getState() {
return _state;
}
bool ThermoStateMachine::getRoomTemperatureStale() {
return _roomTemperatureStale;
}
const char * ThermoStateMachine::c_str() {
return _state_c_str;
}