Skip to content

Commit ff492f9

Browse files
committed
Feature: allow to save heaters config to reload them at startup
1 parent 4a06427 commit ff492f9

File tree

7 files changed

+114
-41
lines changed

7 files changed

+114
-41
lines changed

src/ambient/ambient.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void APB::Ambient::setup(Scheduler &scheduler) {
1313
if(initialised) {
1414
readValuesTask.set(APB_AMBIENT_UPDATE_INTERVAL_SECONDS * 1000, TASK_FOREVER, std::bind(&Ambient::readSensor, this));
1515
scheduler.addTask(readValuesTask);
16+
readSensor();
1617
readValuesTask.enable();
1718
Log.infoln(LOG_SCOPE "Ambient initialised");
1819
} else {

src/heater.cpp

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <ArduinoLog.h>
2+
#include <LittleFS.h>
23
#include <array>
34

45
#include "heater.h"
@@ -9,7 +10,9 @@
910
#include <SmoothThermistor.h>
1011
#endif
1112

13+
#include "settings.h"
1214
#include "utils.h"
15+
#define HEATERS_CONF_FILENAME APB_CONFIG_DIRECTORY "/heaters.json"
1316

1417

1518
static const char *TEMPERATURE_NOT_FOUND_WARNING_LOG = "%s Cannot set heater temperature without temperature sensor";
@@ -27,6 +30,8 @@ struct APB::Heater::Private {
2730
float dewpointOffset;
2831
float rampOffset = 0;
2932

33+
bool applyAtStartup = false;
34+
3035
Task loopTask;
3136
char log_scope[20];
3237
uint8_t index;
@@ -78,16 +83,51 @@ void APB::Heater::setup(uint8_t index, Scheduler &scheduler) {
7883
d->loopTask.set(APB_HEATER_UPDATE_INTERVAL_SECONDS * 1000, TASK_FOREVER, std::bind(&Heater::Private::loop, d));
7984
scheduler.addTask(d->loopTask);
8085
d->loopTask.enable();
81-
86+
loadFromJson();
8287
Log.infoln("%s Heater initialised", d->log_scope);
88+
}
8389

90+
91+
void APB::Heater::loadFromJson() {
92+
if(LittleFS.exists(HEATERS_CONF_FILENAME)) {
93+
File file = LittleFS.open(HEATERS_CONF_FILENAME, "r");
94+
if(!file) {
95+
Log.errorln("%s Error opening heaters configuration file", d->log_scope);
96+
return;
97+
} else {
98+
Log.infoln("%s Heaters configuration file opened", d->log_scope);
99+
}
100+
JsonDocument doc;
101+
DeserializationError error = deserializeJson(doc, file);
102+
if(error) {
103+
Log.errorln("%s Error parsing heaters configuration file: %s", d->log_scope, error.c_str());
104+
return;
105+
}
106+
JsonArray heaters = doc.as<JsonArray>();
107+
if(heaters.size() <= d->index) {
108+
Log.errorln("%s Heaters configuration file doesn't have enough heaters", d->log_scope);
109+
return;
110+
}
111+
bool applyAtStartup = heaters[d->index]["apply_at_startup"].as<bool>();
112+
Log.infoln("%s Heaters configuration file loaded, applyAtStartup=%T", d->log_scope, applyAtStartup);
113+
if(applyAtStartup) {
114+
d->readTemperature();
115+
const char *error = setState(heaters[d->index].as<JsonObject>());
116+
if(error) {
117+
Log.errorln("%s Error setting heater state from configuration: %s", d->log_scope, error);
118+
}
119+
}
120+
}
84121
}
122+
123+
85124
void APB::Heater::toJson(JsonObject heaterStatus) {
86125
heaterStatus["mode"] = modeAsString(),
87126
heaterStatus["max_duty"] = maxDuty();
88127
heaterStatus["duty"] = duty();
89128
heaterStatus["active"] = active();
90129
heaterStatus["has_temperature"] = temperature().has_value();
130+
heaterStatus["apply_at_startup"] = d->applyAtStartup;
91131
optional::if_present(rampOffset(), [&](float v){ heaterStatus["ramp_offset"] = v; });
92132
optional::if_present(minDuty(), [&](float v){ heaterStatus["min_duty"] = v; });
93133
optional::if_present(temperature(), [&](float v){ heaterStatus["temperature"] = v; });
@@ -128,6 +168,10 @@ bool APB::Heater::active() const {
128168
return d->getDuty() > 0;
129169
}
130170

171+
bool APB::Heater::applyAtStartup() const {
172+
return d->applyAtStartup;
173+
}
174+
131175
void APB::Heater::setMaxDuty(float duty) {
132176
if(duty > 0) {
133177
d->maxDuty = duty;
@@ -138,6 +182,40 @@ void APB::Heater::setMaxDuty(float duty) {
138182
d->loop();
139183
}
140184

185+
const char *APB::Heater::setState(JsonObject json) {
186+
Heater::Mode mode = Heater::modeFromString(json["mode"]);
187+
d->applyAtStartup = json["apply_at_startup"].as<bool>();
188+
if(mode == Heater::Mode::off) {
189+
setMaxDuty(0);
190+
return nullptr;
191+
}
192+
193+
float duty = json["max_duty"];
194+
static const char *temperatureErrorMessage = "Unable to set target temperature. Heater probably doesn't have a temperature sensor.";
195+
static const char *dewpointTemperatureErrorMessage = "Unable to set target temperature. Either the heater doesn't have a temperature sensor, or you're missing an ambient sensor.";
196+
197+
if(mode == Heater::Mode::fixed) {
198+
setMaxDuty(json["max_duty"]);
199+
}
200+
if(mode == Heater::Mode::dewpoint) {
201+
float dewpointOffset = json["dewpoint_offset"];
202+
float minDuty = json["min_duty"].is<float>() ? json["min_duty"] : 0.f;
203+
float rampOffset = json["ramp_offset"].is<float>() ? json["ramp_offset"] : 0.f;
204+
if(!setDewpoint(dewpointOffset, duty, minDuty, rampOffset)) {
205+
return dewpointTemperatureErrorMessage;
206+
}
207+
}
208+
if(mode == Heater::Mode::target_temperature) {
209+
float targetTemperature = json["target_temperature"];
210+
float rampOffset = json["ramp_offset"].is<float>() ? json["ramp_offset"] : 0.f;
211+
float minDuty = json["min_duty"].is<float>() ? json["min_duty"] : 0.f;
212+
if(!setTemperature(targetTemperature, duty, minDuty, rampOffset)) {
213+
return temperatureErrorMessage;
214+
}
215+
}
216+
return nullptr;
217+
}
218+
141219
uint8_t APB::Heater::index() const {
142220
return d->index;
143221
}
@@ -336,8 +414,18 @@ void APB::Heaters::toJson(JsonArray heatersStatus) {
336414
});
337415
}
338416

339-
void APB::Heaters::load() {
417+
void APB::Heaters::saveConfig() {
418+
Log.infoln("[Heaters] Saving heaters configuration");
419+
LittleFS.mkdir(APB_CONFIG_DIRECTORY);
420+
File file = LittleFS.open(HEATERS_CONF_FILENAME, "w",true);
421+
if(!file) {
422+
Log.errorln("[Heaters] Error opening heaters configuration file" );
423+
return;
424+
}
425+
JsonDocument doc;
426+
toJson(doc.to<JsonArray>());
427+
serializeJson(doc, file);
428+
file.close();
429+
Log.infoln("[Heaters] Heaters configuration saved");
340430
}
341431

342-
void APB::Heaters::save() {
343-
}

src/heater.h

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ namespace Heaters {
1818
using Array = std::array<APB::Heater, APB_HEATERS_SIZE>;
1919
extern Array &Instance;
2020
void toJson(JsonArray heatersStatus);
21-
void load();
22-
void save();
21+
void saveConfig();
2322
}
2423

2524

@@ -35,22 +34,29 @@ class Heater {
3534

3635
float maxDuty() const;
3736
float duty() const;
38-
void setMaxDuty(float duty);
39-
bool setTemperature(float targetTemperature, float maxDuty=1, float minDuty=0, float rampOffset=0);
40-
bool setDewpoint(float offset, float maxDuty=1, float minDuty=0, float rampOffset=0);
37+
38+
const char *setState(JsonObject jsonObject);
39+
4140
std::optional<float> temperature() const;
4241
std::optional<float> targetTemperature() const;
4342
std::optional<float> dewpointOffset() const;
4443
std::optional<float> rampOffset() const;
4544
std::optional<float> minDuty() const;
4645
bool active() const;
46+
bool applyAtStartup() const;
4747

4848
Mode mode() const;
4949
static std::forward_list<String> validModes();
5050
static Mode modeFromString(const String &mode);
5151
const String modeAsString() const;
5252
uint8_t index() const;
5353
private:
54+
bool setTemperature(float targetTemperature, float maxDuty=1, float minDuty=0, float rampOffset=0);
55+
bool setDewpoint(float offset, float maxDuty=1, float minDuty=0, float rampOffset=0);
56+
void setMaxDuty(float duty);
57+
void loadFromJson();
58+
59+
5460
struct Private;
5561
friend struct Private;
5662
std::shared_ptr<Private> d;

src/settings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include "powermonitor.h"
88
#include <unordered_map>
99

10+
#define APB_CONFIG_DIRECTORY "/config"
11+
1012
namespace APB {
1113
class Settings {
1214
public:

src/webserver.cpp

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ void APB::WebServer::setup() {
9595
void APB::WebServer::onRestart(AsyncWebServerRequest *request) {
9696
JsonResponse response(request);
9797
response.root()["status"] = "restarting";
98-
new Task(1000, TASK_ONCE, [](){ esp_restart(); }, &scheduler, true);
98+
new Task(3000, TASK_ONCE, [](){ esp_restart(); }, &scheduler, true);
9999
}
100100

101101
void APB::WebServer::onGetStatus(AsyncWebServerRequest *request) {
@@ -281,37 +281,13 @@ void APB::WebServer::onPostSetHeater(AsyncWebServerRequest *request, JsonVariant
281281
}
282282

283283
Heater &heater = Heaters::Instance[json["index"]];
284-
285-
if(mode == Heater::Mode::off) {
286-
heater.setMaxDuty(0);
287-
onGetHeaters(request);
284+
const char *errorMessage = heater.setState(json);
285+
if(errorMessage) {
286+
JsonResponse::error(500, errorMessage, request);
288287
return;
289288
}
290-
291-
float duty = json["max_duty"];
292-
static const char *temperatureErrorMessage = "Unable to set target temperature. Heater probably doesn't have a temperature sensor.";
293-
static const char *dewpointTemperatureErrorMessage = "Unable to set target temperature. Either the heater doesn't have a temperature sensor, or you're missing an ambient sensor.";
294-
295-
if(mode == Heater::Mode::fixed) {
296-
heater.setMaxDuty(json["max_duty"]);
297-
}
298-
if(mode == Heater::Mode::dewpoint) {
299-
float dewpointOffset = json["dewpoint_offset"];
300-
float minDuty = json["min_duty"].is<float>() ? json["min_duty"] : 0.f;
301-
float rampOffset = json["ramp_offset"].is<float>() ? json["ramp_offset"] : 0.f;
302-
if(!heater.setDewpoint(dewpointOffset, duty, minDuty, rampOffset)) {
303-
JsonResponse::error(500, dewpointTemperatureErrorMessage, request);
304-
return;
305-
}
306-
}
307-
if(mode == Heater::Mode::target_temperature) {
308-
float targetTemperature = json["target_temperature"];
309-
float rampOffset = json["ramp_offset"].is<float>() ? json["ramp_offset"] : 0.f;
310-
float minDuty = json["min_duty"].is<float>() ? json["min_duty"] : 0.f;
311-
if(!heater.setTemperature(targetTemperature, duty, minDuty, rampOffset)) {
312-
JsonResponse::error(500, temperatureErrorMessage, request);
313-
return;
314-
}
289+
if(heater.applyAtStartup()) {
290+
Heaters::saveConfig();
315291
}
316292
onGetHeaters(request);
317293
}

web/src/features/sensors/heaters/Heaters.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const SetHeaterModal = ({heater: originalHeater, show, onClose, index}) => {
154154
</Form.Group>
155155

156156
</Collapse>
157-
157+
<Form.Check id={`applyAtStartup-${index}`} label='Apply at startup' checked={heater.apply_at_startup} onChange={e => setHeater({...heater, 'apply_at_startup': e.target.checked})} />
158158
</Form>
159159
</Modal.Body>
160160
<Modal.Footer>

web/src/setupProxy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { createProxyMiddleware } = require('http-proxy-middleware');
22

33
module.exports = function(app) {
4-
const target = 'http://astropowerbox-c11.lan'
4+
const target = 'http://astropowerbox-travel.lan'
55
console.log(`****** proxy setup: "/api" => ${target}`)
66
app.use(
77
'/api',

0 commit comments

Comments
 (0)