Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 215 additions & 8 deletions src/MainTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ class MainTask : public Task {
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
unsigned long externalPumpStartTime = 0;
bool ntpStarted = false;
char ntpServer[sizeof(settings.system.ntp.server)] = {0};
char ntpTimezone[sizeof(settings.system.ntp.timezone)] = {0};
bool telnetStarted = false;
bool emergencyDetected = false;
unsigned long emergencyFlipTime = 0;
bool freezeDetected = false;
unsigned long freezeDetectedTime = 0;
int8_t heatingScheduleDay = -1;
int8_t heatingScheduleSlot = -1;
uint8_t heatingScheduleTime = 0;
HeatingScheduleAction heatingScheduleAction = HeatingScheduleAction::ON;
float heatingScheduleTarget = 0.0f;
bool heatingScheduleManualOverride = false;

#if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override {
Expand All @@ -62,6 +70,36 @@ class MainTask : public Task {

void setup() {}

inline bool hasNtpConfigChanged() {
return strcmp(this->ntpServer, settings.system.ntp.server) != 0
|| strcmp(this->ntpTimezone, settings.system.ntp.timezone) != 0;
}

inline void clearNtpConfigState() {
this->ntpStarted = false;
this->ntpServer[0] = '\0';
this->ntpTimezone[0] = '\0';
}

inline void applyNtpConfig() {
configTime(0, 0, settings.system.ntp.server);
setenv("TZ", settings.system.ntp.timezone, 1);
tzset();

strncpy(this->ntpServer, settings.system.ntp.server, sizeof(this->ntpServer) - 1);
this->ntpServer[sizeof(this->ntpServer) - 1] = '\0';
strncpy(this->ntpTimezone, settings.system.ntp.timezone, sizeof(this->ntpTimezone) - 1);
this->ntpTimezone[sizeof(this->ntpTimezone) - 1] = '\0';

this->ntpStarted = true;
Log.sinfoln(
FPSTR(L_MAIN),
F("NTP configured: server=%s timezone=%s"),
settings.system.ntp.server,
settings.system.ntp.timezone
);
}

void loop() {
network->loop();

Expand Down Expand Up @@ -113,14 +151,13 @@ class MainTask : public Task {
}

if (network->isConnected()) {
if (!this->ntpStarted) {
if (strlen(settings.system.ntp.server)) {
configTime(0, 0, settings.system.ntp.server);
setenv("TZ", settings.system.ntp.timezone, 1);
tzset();

this->ntpStarted = true;
if (strlen(settings.system.ntp.server) == 0) {
if (this->ntpStarted) {
this->clearNtpConfigState();
}

} else if (!this->ntpStarted || this->hasNtpConfigChanged()) {
this->applyNtpConfig();
}

if (!this->telnetStarted && telnetStream != nullptr) {
Expand All @@ -137,7 +174,7 @@ class MainTask : public Task {

} else {
if (this->ntpStarted) {
this->ntpStarted = false;
this->clearNtpConfigState();
}

if (this->telnetStarted) {
Expand Down Expand Up @@ -193,6 +230,7 @@ class MainTask : public Task {
ESP.restart();
}

this->heatingSchedule();
this->heating();
this->emergency();
this->cascadeControl();
Expand Down Expand Up @@ -241,6 +279,174 @@ class MainTask : public Task {
}
}

inline void resetHeatingScheduleState() {
this->heatingScheduleDay = -1;
this->heatingScheduleSlot = -1;
this->heatingScheduleTime = 0;
this->heatingScheduleAction = HeatingScheduleAction::ON;
this->heatingScheduleTarget = 0.0f;
this->heatingScheduleManualOverride = false;
}

inline uint8_t getHeatingScheduleDayIndex(const tm& localTime) {
return localTime.tm_wday == 0
? (HEATING_SCHEDULE_DAYS - 1)
: (localTime.tm_wday - 1);
}

inline uint8_t getHeatingScheduleTimeStep(const tm& localTime) {
return (
(localTime.tm_hour * 60)
+ localTime.tm_min
) / HEATING_SCHEDULE_STEP_MINUTES;
}

bool getActiveHeatingScheduleSlot(const tm& localTime, uint8_t& dayId, uint8_t& slotId) {
uint8_t currentDayId = this->getHeatingScheduleDayIndex(localTime);
uint8_t currentTimeStep = this->getHeatingScheduleTimeStep(localTime);
for (uint8_t dayOffset = 0; dayOffset < HEATING_SCHEDULE_DAYS; dayOffset++) {
dayId = (
currentDayId
+ HEATING_SCHEDULE_DAYS
- dayOffset
) % HEATING_SCHEDULE_DAYS;

uint8_t maxTimeStep = dayOffset == 0
? currentTimeStep
: (HEATING_SCHEDULE_STEPS_PER_DAY - 1);
int16_t selectedTime = -1;
bool found = false;

for (uint8_t currentSlotId = 0; currentSlotId < HEATING_SCHEDULE_SLOTS; currentSlotId++) {
auto& slot = settings.heatingSchedule.days[dayId].slots[currentSlotId];
if (!slot.isEnabled()) {
continue;
}

uint8_t slotTime = slot.getTime();
if (slotTime <= maxTimeStep && slotTime >= selectedTime) {
selectedTime = slotTime;
slotId = currentSlotId;
found = true;
}
}

if (found) {
return true;
}
}

return false;
}

bool isHeatingScheduleOverridden(const HeatingScheduleSlot& slot) {
switch (slot.action) {
case HeatingScheduleAction::OFF:
return settings.heating.enabled;

case HeatingScheduleAction::ON:
return !settings.heating.enabled;

case HeatingScheduleAction::TARGET:
return !settings.heating.enabled || fabsf(settings.heating.target - slot.target) > 0.0001f;
}

return false;
}

void applyHeatingScheduleSlot(const uint8_t dayId, const uint8_t slotId) {
auto& slot = settings.heatingSchedule.days[dayId].slots[slotId];
bool changed = false;

switch (slot.action) {
case HeatingScheduleAction::OFF:
if (settings.heating.enabled) {
settings.heating.enabled = false;
changed = true;
}
break;

case HeatingScheduleAction::ON:
if (!settings.heating.enabled) {
settings.heating.enabled = true;
changed = true;
}
break;

case HeatingScheduleAction::TARGET:
if (!settings.heating.enabled) {
settings.heating.enabled = true;
changed = true;
}

if (fabsf(settings.heating.target - slot.target) > 0.0001f) {
settings.heating.target = slot.target;
changed = true;
}
break;
}

this->heatingScheduleDay = dayId;
this->heatingScheduleSlot = slotId;
this->heatingScheduleTime = slot.getTime();
this->heatingScheduleAction = slot.action;
this->heatingScheduleTarget = slot.target;

if (!changed) {
return;
}

tMqtt->resetPublishedSettingsTime();
Log.sinfoln(
FPSTR(L_MAIN),
F("Heating schedule applied: day %hhu slot %hhu action %hhu target %.2f"),
dayId,
slotId,
static_cast<uint8_t>(slot.action),
slot.target
);
}

void heatingSchedule() {
if (!settings.heatingSchedule.enabled) {
this->resetHeatingScheduleState();
return;
}

struct tm localTime;
if (!getLocalTime(&localTime)) {
this->heatingScheduleDay = -1;
this->heatingScheduleSlot = -1;
return;
}

uint8_t dayId, slotId;
if (!this->getActiveHeatingScheduleSlot(localTime, dayId, slotId)) {
this->resetHeatingScheduleState();
return;
}

auto& slot = settings.heatingSchedule.days[dayId].slots[slotId];
bool slotChanged = this->heatingScheduleDay != dayId || this->heatingScheduleSlot != slotId;
bool slotConfigChanged = this->heatingScheduleTime != slot.getTime()
|| this->heatingScheduleAction != slot.action
|| fabsf(this->heatingScheduleTarget - slot.target) > 0.0001f;

if (slotChanged || slotConfigChanged) {
this->heatingScheduleManualOverride = false;
this->applyHeatingScheduleSlot(dayId, slotId);
return;
}

if (!this->heatingScheduleManualOverride && this->isHeatingScheduleOverridden(slot)) {
this->heatingScheduleManualOverride = true;
Log.sinfoln(
FPSTR(L_MAIN),
F("Heating schedule overridden manually until next slot")
);
}
}

void heating() {
// freeze protection
if (!settings.heating.enabled) {
Expand Down Expand Up @@ -283,6 +489,7 @@ class MainTask : public Task {
this->freezeDetected = false;
settings.heating.enabled = true;
fsSettings.update();
tMqtt->resetPublishedSettingsTime();

Log.sinfoln(
FPSTR(L_MAIN),
Expand Down
2 changes: 1 addition & 1 deletion src/MqttTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class MqttTask : public Task {

this->client->onMessage([this] (void*, size_t length) {
const String& topic = this->client->messageTopic();
if (!length || length > 2048 || !topic.length()) {
if (!length || length > 4096 || !topic.length()) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/PortalTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ class PortalTask : public LeanTask {
this->webServer->send(406);
return;

} else if (plain.length() > 2536) {
} else if (plain.length() > 4096) {
this->webServer->send(413);
return;
}
Expand Down Expand Up @@ -474,7 +474,7 @@ class PortalTask : public LeanTask {
this->webServer->send(406);
return;

} else if (plain.length() > 2536) {
} else if (plain.length() > 4096) {
this->webServer->send(413);
return;
}
Expand Down
40 changes: 40 additions & 0 deletions src/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ struct NetworkSettings {
} sta;
} networkSettings;

struct HeatingScheduleSlot {
uint8_t time = 0;
HeatingScheduleAction action = HeatingScheduleAction::ON;
float target = DEFAULT_HEATING_TARGET_TEMP;

inline bool isEnabled() const {
return (this->time & HEATING_SCHEDULE_SLOT_DISABLED_MASK) == 0;
}

inline void setEnabled(bool value) {
if (value) {
this->time &= HEATING_SCHEDULE_SLOT_TIME_MASK;
} else {
this->time = (this->time & HEATING_SCHEDULE_SLOT_TIME_MASK) | HEATING_SCHEDULE_SLOT_DISABLED_MASK;
}
}

inline uint8_t getTime() const {
return this->time & HEATING_SCHEDULE_SLOT_TIME_MASK;
}

inline void setTime(uint8_t value) {
this->time = (this->time & HEATING_SCHEDULE_SLOT_DISABLED_MASK) | (value & HEATING_SCHEDULE_SLOT_TIME_MASK);
}
};

struct HeatingScheduleDay {
HeatingScheduleSlot slots[HEATING_SCHEDULE_SLOTS] = {
{0, HeatingScheduleAction::ON, DEFAULT_HEATING_TARGET_TEMP},
{24, HeatingScheduleAction::ON, DEFAULT_HEATING_TARGET_TEMP},
{48, HeatingScheduleAction::ON, DEFAULT_HEATING_TARGET_TEMP},
{72, HeatingScheduleAction::ON, DEFAULT_HEATING_TARGET_TEMP}
};
};

struct Settings {
struct {
uint8_t logLevel = DEFAULT_LOG_LEVEL;
Expand Down Expand Up @@ -126,6 +161,11 @@ struct Settings {
} freezeProtection;
} heating;

struct {
bool enabled = false;
HeatingScheduleDay days[HEATING_SCHEDULE_DAYS];
} heatingSchedule;

struct {
bool enabled = true;
float target = DEFAULT_DHW_TARGET_TEMP;
Expand Down
13 changes: 13 additions & 0 deletions src/defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
#define DEFAULT_DHW_MIN_TEMP 30
#define DEFAULT_DHW_MAX_TEMP 60

#define HEATING_SCHEDULE_DAYS 7
#define HEATING_SCHEDULE_SLOTS 4
#define HEATING_SCHEDULE_STEP_MINUTES 15
#define HEATING_SCHEDULE_STEPS_PER_DAY (24 * 60 / HEATING_SCHEDULE_STEP_MINUTES)
#define HEATING_SCHEDULE_SLOT_DISABLED_MASK 0x80
#define HEATING_SCHEDULE_SLOT_TIME_MASK 0x7f

#define THERMOSTAT_INDOOR_DEFAULT_TEMP 20
#define THERMOSTAT_INDOOR_MIN_TEMP 5
#define THERMOSTAT_INDOOR_MAX_TEMP 40
Expand Down Expand Up @@ -168,4 +175,10 @@ enum class HysteresisAction : uint8_t {
SET_ZERO_TARGET = 1
};

enum class HeatingScheduleAction : uint8_t {
OFF = 0,
ON = 1,
TARGET = 2
};

char buffer[255];
Loading