11#include < ArduinoLog.h>
2+ #include < LittleFS.h>
23#include < array>
34
45#include " heater.h"
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
1518static 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+
85124void 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+
131175void 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+
141219uint8_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- }
0 commit comments