Sterownik ma za zadanie pilnować temperatury wody i pokrywy z oświetleniem w akwarium, oraz sterować stopniowym załączaniem oświetlenia. Za pilnowanie wody odpowiada termostat ustawiony jako chłodzenie w trybie ręcznym, harmonogram od chłodzenia (można ewentualnie zmienić na harmonogram grzania) steruje stopniowym zapalaniem oświetlenia. Nastawy w harmonogramie temperatur:
Wyłączony - oświetlenie wyłączone
20 stopni - tylko kanał nocny
21 stopni - tylko kanał dodatkowy
22 stopnie - kanał dodatkowy + świetlówki 1 + 2
23 stopnie - kanał dodatkowy + świetlówki 1 + 2 + 3
W konfiguracji można wybrać kiedy ma chodzić chłodzenie pokrywy - kanał dodatkowy oraz świetlówki 1-3, oraz można sterować temperaturą czujnika pokrywy (po włączeniu ustawiamy jaka temperatura ma załączyć).
Dodane wysyłanie powiadomień dla kanałów termostat, chłodzenie pokrywy, oraz dla modułu przy problemie z odczytem temperatury.
Dodany wirtualny kanał Harmonogram oświetlenia z pamięcią stanu do aktywacji harmonogramu lub wyłączenia w tedy ręcznie możemy sterować oświetleniem.
W planach dodanie obsługi ESP-NOW do połączenia bezpośredniego dodatkowych dwóch modułów.
Aktualnie udostępniam tylko kod.
Code: Select all
/*
* SUPLA-SmartReef
*/
#include <SuplaDevice.h>
#include <supla/network/esp_wifi.h>
#include <supla/control/relay.h>
#include <supla/control/light_relay.h>
#include <supla/control/virtual_relay.h>
#include <supla/control/button.h>
#include <supla/control/hvac_base.h>
#include <supla/control/internal_pin_output.h>
#include <supla/device/status_led.h>
#include <supla/device/notifications.h>
#include <supla/clock/clock.h>
#include <supla/storage/storage.h>
#include <supla/storage/eeprom.h>
#include <supla/storage/config.h>
#include <supla/storage/littlefs_config.h>
#include <supla/network/esp_web_server.h>
#include <supla/network/html/device_info.h>
#include <supla/network/html/protocol_parameters.h>
#include <supla/network/html/status_led_parameters.h>
#include <supla/network/html/wifi_parameters.h>
#include <supla/network/html/time_parameters.h>
#include <supla/device/supla_ca_cert.h>
#include <HTTPUpdateServer.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <supla/sensor/thermometer.h>
#define STATUS_LED_GPIO 23
#define BUTTON_CFG_GPIO 0
#define COOLING_THERMOSTAT_GPIO 27
#define LAMP_COOLING_GPIO 14
#define NIGHT_LED_GPIO 26
#define FLUORESCENT_1_GPIO 32
#define FLUORESCENT_2_GPIO 25
#define FLUORESCENT_3_GPIO 33
#define HIDDEN_REMOTE_GPIO 12
#define ONE_WIRE_BUS 1
const char* const firmware_version = "SmartReef v1.0";
const char* const SENSOR_NAMES[] = {
"Woda",
"Pokrywa"
};
const int SENSOR_COUNT = sizeof(SENSOR_NAMES) / sizeof(SENSOR_NAMES[0]);
const int CH_HVAC_COOLING = 0;
const int CH_TEMP_WATER = 1;
const int CH_TEMP_COVER = 2;
const int CH_COVER_COOLING = 3;
const int CH_NIGHT_LED = 4;
const int CH_FLUORESCENT_1 = 5;
const int CH_FLUORESCENT_2 = 6;
const int CH_FLUORESCENT_3 = 7;
const int CH_HIDDEN_REMOTE = 8;
const int CH_LIGHTING_SCHEDULE = 9;
const int SUPLA_CHANNEL_COUNT = 10;
Supla::ESPWifi wifi;
Supla::Eeprom eeprom;
Supla::LittleFsConfig configSupla{2048};
Supla::Device::StatusLed statusLed(STATUS_LED_GPIO, false);
Supla::EspWebServer suplaServer;
HTTPUpdateServer httpUpdater;
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
DeviceAddress discoveredAddresses[10];
int discoveredCount = 0;
Supla::Control::HvacBase* hvacCooling = nullptr;
Supla::Control::Relay* relayLampCooling = nullptr;
Supla::Control::LightRelay* relayNightLed = nullptr;
Supla::Control::LightRelay* relayFluorescent1 = nullptr;
Supla::Control::LightRelay* relayFluorescent2 = nullptr;
Supla::Control::LightRelay* relayFluorescent3 = nullptr;
Supla::Control::Relay* relayHiddenRemote = nullptr;
Supla::Control::VirtualRelay* relayLightingSchedule = nullptr;
const int COVER_TEMP_SENSOR_INDEX = 1;
const int32_t DEFAULT_COVER_TEMP_ON_X10 = 300;
void scanDS18B20() {
pinMode(ONE_WIRE_BUS, INPUT_PULLUP);
delay(250);
sensors.begin();
delay(50);
discoveredCount = 0;
DeviceAddress tempAddr;
oneWire.reset_search();
while (oneWire.search(tempAddr) && discoveredCount < 10) {
if (sensors.validAddress(tempAddr) && tempAddr[0] == 0x28) {
memcpy(discoveredAddresses[discoveredCount], tempAddr, sizeof(DeviceAddress));
discoveredCount++;
}
}
}
class ConfigurableDS18B20 : public Supla::Sensor::Thermometer {
public:
ConfigurableDS18B20(int sensorNumber) : sensorNum(sensorNumber) {
memset(deviceAddress, 0, sizeof(deviceAddress));
}
void onInit() override {
loadAddress();
channel.setNewValue(getValue());
}
double getValue() override {
if (deviceAddress[0] == 0 && deviceAddress[7] == 0) return -275.0;
sensors.requestTemperaturesByAddress(deviceAddress);
double temp = sensors.getTempC(deviceAddress);
if (temp == DEVICE_DISCONNECTED_C) return -275.0;
return temp;
}
void loadAddress() {
auto cfg = Supla::Storage::ConfigInstance();
if (!cfg) return;
char key[20];
snprintf(key, sizeof(key), "ds_blob_%d", sensorNum);
size_t blobSize = sizeof(deviceAddress);
cfg->getBlob(key, (char*)deviceAddress, blobSize);
}
protected:
int sensorNum;
DeviceAddress deviceAddress;
};
ConfigurableDS18B20* dsSensors[SENSOR_COUNT];
void handleSmartReefSaveAction() {
auto server = suplaServer.getServerPtr();
if (!server) return;
auto cfg = Supla::Storage::ConfigInstance();
if (cfg) {
cfg->setUInt8("cover_cool_night", server->hasArg("cover_cool_night") ? 1 : 0);
cfg->setUInt8("cover_cool_fl1", server->hasArg("cover_cool_fl1") ? 1 : 0);
cfg->setUInt8("cover_cool_fl2", server->hasArg("cover_cool_fl2") ? 1 : 0);
cfg->setUInt8("cover_cool_fl3", server->hasArg("cover_cool_fl3") ? 1 : 0);
cfg->setUInt8("cover_cool_hidden", server->hasArg("cover_cool_hidden") ? 1 : 0);
cfg->setUInt8("cover_cool_temp", server->hasArg("cover_cool_temp") ? 1 : 0);
if (server->hasArg("cover_temp_on")) {
String tempValue = server->arg("cover_temp_on");
tempValue.replace(",", ".");
int32_t tempOnX10 = (int32_t)(tempValue.toFloat() * 10.0 + 0.5);
cfg->setInt32("cover_temp_on", tempOnX10);
}
cfg->setUInt8("notif_water_cooling", server->hasArg("notif_water_cooling") ? 1 : 0);
cfg->setUInt8("notif_cover_cooling", server->hasArg("notif_cover_cooling") ? 1 : 0);
cfg->setUInt8("notif_temp_error", server->hasArg("notif_temp_error") ? 1 : 0);
for (int t = 0; t < SENSOR_COUNT; t++) {
char argName[20];
snprintf(argName, sizeof(argName), "ds_assign_%d", t);
if (server->hasArg(argName)) {
int selectedIdx = server->arg(argName).toInt();
char idxKey[20];
snprintf(idxKey, sizeof(idxKey), "ds_idx_%d", t);
cfg->setUInt8(idxKey, selectedIdx);
char blobKey[20];
snprintf(blobKey, sizeof(blobKey), "ds_blob_%d", t);
if (selectedIdx >= 0 && selectedIdx < discoveredCount) {
cfg->setBlob(blobKey, (const char*)discoveredAddresses[selectedIdx], 8);
} else {
uint8_t zeroAddress[8] = {0, 0, 0, 0, 0, 0, 0, 0};
cfg->setBlob(blobKey, (const char*)zeroAddress, 8);
}
}
}
cfg->commit();
for (int i = 0; i < SENSOR_COUNT; i++) {
if (dsSensors[i]) dsSensors[i]->loadAddress();
}
}
server->sendHeader("Location", "/smartreef?saved=1");
server->send(303, "text/plain", "");
}
/*
* =====================================================
* STRONA WWW SmartReef (GET)
* =====================================================
* handleSmartReefPage() –
* - generuje HTML formularza konfiguracji SmartReef
* - wyświetla:
* * przypisanie czujników DS18B20
* * parametry pompy ogrzewania (warunki start/stop, minimalny czas)
* * blokadę kanałów (pompa, chlor, ogrzewanie, UV)
* * zależności kanałów od pompy (chlor, UV)
* * opcję sterowania kanałami zależnymi przez harmonogram
* * powiadomienia (zabezpieczenie, pompa, ogrzewanie)
* - zawiera przyciski:
* * SUPLA SETTINGS
* * UPDATE FIRMWARE
* * SAVE
*/
void handleSmartReefPage() {
auto server = suplaServer.getServerPtr();
if (!server) return;
auto cfg = Supla::Storage::ConfigInstance();
String html = "";
html += "<!doctype html><html lang=en><head><meta content=\"text/html;charset=UTF-8\" http-equiv=content-type><meta content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\" name=viewport><title>SUPLA - " + String(firmware_version) + "</title><style>*,*::before,*::after{box-sizing:border-box}html:focus-within{scroll-behavior:smooth}html,body,h1,h2,h3,h4,p{font-family:HelveticaNeue,\"Helvetica Neue\",HelveticaNeueRoman,HelveticaNeue-Roman,\"Helvetica Neue Roman\",TeXGyreHerosRegular,Helvetica,Tahoma,Geneva,Arial,sans-serif;margin:0;padding:0}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5;font-size:14px;font-weight:400;background:#00d151;color:#fff;font-stretch:normal}h1,h3{margin:10px 0;font-weight:300;font-size:23px}input,button,textarea,select{font:inherit}.wrapper{display:flex;flex-direction:column;justify-content:center;min-height:100vh}.content{text-align:center;padding:20px 10px}#logo{display:inline-block;height:155px;background:#00d151}.box{background:#fff;color:#000;border-radius:10px;padding:5px 10px;margin:10px 0;box-shadow:0 5px 6px rgb(0 0 0 / .3)}.box h3{margin-top:0;margin-bottom:5px}.form{text-align:left;max-width:500px;margin:0 auto;margin-top:-80px;padding:10px;padding-top:70px}.form-field{display:flex;align-items:center;padding:8px 10px;border-top:1px solid #00d150;margin:0 -10px}.box>.form-field:first-of-type{border-top:0}.form-field label{width:250px;margin-right:5px;color:#00d150}@media screen and (max-width:530px){.form-field{flex-direction:column}.form-field label{width:100%;margin:3px 0}}.form-field>div{width:100%}.form-field input:not([type=range]),.form-field select,.form-field textarea{width:100%;border:1px solid #ccc;border-radius:6px;padding:3px 8px;background:#fff}textarea{resize:vertical}.form-field select{padding-left:3px}.form-field.checkbox label{width:100%;color:#000;display:flex;align-items:center;gap:5px}.form-field.right-checkbox label:last-child{width:100%;text-align:center}.help-link{cursor:help;color:#00d150}.hint{font-size:.8em;opacity:.8;margin-top:2px}button{display:block;color:#fff;background:#000;text-align:center;width:100%;border:0;border-radius:10px;padding:10px;text-transform:uppercase;text-decoration:none;cursor:pointer;font-size:1.3em;margin-top:15px;box-shadow:0 5px 6px rgb(0 0 0 / .3)}#msg{background:#ffe836;position:fixed;width:100%;padding:40px;color:#000;top:0;left:0;box-shadow:0 1px 3px rgb(0 0 0 / .3);text-align:center;font-size:26px}.switch{position:relative;display:inline-block;width:51px;height:25px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s;border-radius:34px}.slider:before{position:absolute;content:\"\";height:17px;width:17px;left:4px;bottom:4px;background-color:#fff;-webkit-transition:.4s;transition:.4s;border-radius:50%}input:checked+.slider{background-color:#00d151}input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}.section-subtitle{margin:10px 0 2px 0;color:#00d150;font-size:0.9em;font-weight:bold;text-transform:uppercase}</style></head><body><div class=\"wrapper\"><div class=\"content\">";
html += "<svg id=logo version=1.1 viewBox=\"0 0 200 200\" x=0 xml:space=preserve y=0><path d=\"M59.3,2.5c18.1,0.6,31.8,8,40.2,23.5c3.1,5.7,4.3,11.9,4.1,18.3c-0.1,3.6-0.7,7.1-1.9,10.6c-0.2,0.7-0.1,1.1,0.6,1.5c12.8,7.7,25.5,15.4,38.3,23c2.9,1.7,5.8,3.4,8.7,5.3c1,0.6,1.6,0.6,2.5-0.1c4.5-3.6,9.8-5.3,15.7-5.4c12.5-0.1,22.9,7.9,25.2,19c1.9,9.2-2.9,19.2-11.8,23.9c-8.4,4.5-16.9,4.5-25.5,0.2c-0.7-0.3-1-0.2-1.5,0.3c-4.8,4.9-9.7,9.8-14.5,14.6c-5.3,5.3-10.6,10.7-15.9,16c-1.8,1.8-3.6,3.7-5.4,5.4c-0.7,0.6-0.6,1,0,1.6c3.6,3.4,5.8,7.5,6.2,12.2c0.7,7.7-2.2,14-8.8,18.5c-12.3,8.6-30.3,3.5-35-10.4c-2.8-8.4,0.6-17.7,8.6-22.8c0.9-0.6,1.1-1,0.8-2c-2-6.2-4.4-12.4-6.6-18.6c-6.3-17.6-12.7-35.1-19-52.7c-0.2-0.7-0.5-1-1.4-0.9c-12.5,0.7-23.6-2.6-33-10.4c-8-6.6-12.9-15-14.2-25c-1.5-11.5,1.7-21.9,9.6-30.7C32.5,8.9,42.2,4.2,53.7,2.7c0.7-0.1,1.5-0.2,2.2-0.2C57,2.4,58.2,2.5,59.3,2.5z M76.5,81c0,0.1,0.1,0.3,0.1,0.6c1.6,6.3,3.2,12.6,4.7,18.9c4.5,17.7,8.9,35.5,13.3,53.2c0.2,0.9,0.6,1.1,1.6,0.9c5.4-1.2,10.7-0.8,15.7,1.6c0.8,0.4,1.2,0.3,1.7-0.4c11.2-12.9,22.5-25.7,33.4-38.7c0.5-0.6,0.4-1,0-1.6c-5.6-7.9-6.1-16.1-1.3-24.5c0.5-0.8,0.3-1.1-0.5-1.6c-9.1-4.7-18.1-9.3-27.2-14c-6.8-3.5-13.5-7-20.3-10.5c-0.7-0.4-1.1-0.3-1.6,0.4c-1.3,1.8-2.7,3.5-4.3,5.1c-4.2,4.2-9.1,7.4-14.7,9.7C76.9,80.3,76.4,80.3,76.5,81z M89,42.6c0.1-2.5-0.4-5.4-1.5-8.1C83,23.1,74.2,16.9,61.7,15.8c-10-0.9-18.6,2.4-25.3,9.7c-8.4,9-9.3,22.4-2.2,32.4c6.8,9.6,19.1,14.2,31.4,11.9C79.2,67.1,89,55.9,89,42.6z M102.1,188.6c0.6,0.1,1.5-0.1,2.4-0.2c9.5-1.4,15.3-10.9,11.6-19.2c-2.6-5.9-9.4-9.6-16.8-8.6c-8.3,1.2-14.1,8.9-12.4,16.6C88.2,183.9,94.4,188.6,102.1,188.6z M167.7,88.5c-1,0-2.1,0.1-3.1,0.3c-9,1.7-14.2,10.6-10.8,18.6c2.9,6.8,11.4,10.3,19,7.8c7.1-2.3,11.1-9.1,9.6-15.9C180.9,93,174.8,88.5,167.7,88.5z\"/></svg>";
html += "<div class=\"form\"><h1>" + String(firmware_version) + "</h1>";
if (server->hasArg("saved")) html += "<div id=\"msg\">Data saved</div>";
html += "<form action=/smartreef method=POST>";
html += "<div class=box><h3>Czujniki DS18B20</h3>";
for (int t = 0; t < SENSOR_COUNT; t++) {
DeviceAddress savedAddr;
memset(savedAddr, 0, sizeof(DeviceAddress));
if (cfg) {
char blobKey[20];
snprintf(blobKey, sizeof(blobKey), "ds_blob_%d", t);
cfg->getBlob(blobKey, (char*)savedAddr, 8);
}
bool isAssigned = false;
for (int b = 0; b < 8; b++) {
if (savedAddr[b] != 0) {
isAssigned = true;
break;
}
}
html += "<div class=form-field><label for=ds_assign_" + String(t) + ">" + String(SENSOR_NAMES[t]) + "</label><div><select name=ds_assign_" + String(t) + " id=ds_assign_" + String(t) + ">";
html += "<option value=255" + String(!isAssigned ? " selected" : "") + ">Nie przypisany</option>";
for (int i = 0; i < discoveredCount; i++) {
String idStr = "";
for (int b = 0; b < 8; b++) {
char hex[3];
snprintf(hex, sizeof(hex), "%02X", discoveredAddresses[i][b]);
idStr += hex;
}
bool match = true;
for (int b = 0; b < 8; b++) {
if (discoveredAddresses[i][b] != savedAddr[b]) {
match = false;
break;
}
}
html += "<option value=\"" + String(i) + "\"" + String(match && isAssigned ? " selected" : "") + ">" + idStr + "</option>";
}
html += "</select></div></div>";
}
html += "</div>";
auto addToggleSwitch = [](String name, String label, uint8_t val) {
return "<div class=\"form-field right-checkbox\"><label for=\"" + name + "\">" + label + "</label><label><span class=switch><input type=checkbox value=1 name=\"" + name + "\" id=\"" + name + "\"" + String(val ? " checked" : "") + "><span class=slider></span></span></label></div>";
};
uint8_t coolNight = 0, coolFl1 = 0, coolFl2 = 0, coolFl3 = 0, coolHidden = 0, coolTemp = 0;
int32_t coverTempOnX10 = DEFAULT_COVER_TEMP_ON_X10;
if (cfg) {
cfg->getUInt8("cover_cool_night", &coolNight);
cfg->getUInt8("cover_cool_fl1", &coolFl1);
cfg->getUInt8("cover_cool_fl2", &coolFl2);
cfg->getUInt8("cover_cool_fl3", &coolFl3);
cfg->getUInt8("cover_cool_hidden", &coolHidden);
cfg->getUInt8("cover_cool_temp", &coolTemp);
cfg->getInt32("cover_temp_on", &coverTempOnX10);
if (coverTempOnX10 == 0) coverTempOnX10 = DEFAULT_COVER_TEMP_ON_X10;
}
html += "<div class=box><h3>Chłodzenie pokrywy</h3>";
html += addToggleSwitch("cover_cool_night", "LED nocne", coolNight);
html += addToggleSwitch("cover_cool_fl1", "Blue Plus 1", coolFl1);
html += addToggleSwitch("cover_cool_fl2", "Blue Plus 2", coolFl2);
html += addToggleSwitch("cover_cool_fl3", "Coral Plus", coolFl3);
html += addToggleSwitch("cover_cool_hidden", "Dodatkowe oświetlenie", coolHidden);
html += addToggleSwitch("cover_cool_temp", "Temperatura", coolTemp);
html += "<div class=form-field id=cover_temp_field style=\"justify-content:space-between;" + String(coolTemp ? "" : "display:none;") + "\"><label>Temperatura załączenia</label><input type=number min=0 max=80 step=0.1 name=cover_temp_on style=\"width:80px;text-align:center\" value=\"" + String(coverTempOnX10 / 10.0, 1) + "\"></div>";
html += "<script>document.addEventListener('DOMContentLoaded',function(){var sw=document.getElementById('cover_cool_temp');var field=document.getElementById('cover_temp_field');if(sw&&field){var sync=function(){field.style.display=sw.checked?'flex':'none';};sw.addEventListener('change',sync);sync();}});</script>";
html += "</div>";
uint8_t notifWaterCooling = 0, notifCoverCooling = 0, notifTempError = 0;
if (cfg) {
cfg->getUInt8("notif_water_cooling", ¬ifWaterCooling);
cfg->getUInt8("notif_cover_cooling", ¬ifCoverCooling);
cfg->getUInt8("notif_temp_error", ¬ifTempError);
}
html += "<div class=box><h3>Powiadomienia</h3>";
html += addToggleSwitch("notif_water_cooling", "Chłodzenie wody", notifWaterCooling);
html += addToggleSwitch("notif_cover_cooling", "Chłodzenie pokrywy", notifCoverCooling);
html += addToggleSwitch("notif_temp_error", "Błąd temperatury", notifTempError);
html += "</div>";
html += "<script>setTimeout(function(){var msgEl=document.getElementById('msg');if(msgEl){msgEl.remove();window.history.replaceState({},document.title,'/smartreef');}},3200);</script>";
html += "<button type=button onclick=\"window.location.href='/'\">SUPLA SETTINGS</button>";
html += "<button type=button onclick=\"window.location.href='/update'\">UPDATE FIRMWARE</button>";
html += "<button type=submit>SAVE</button>";
html += "</form></div></div></div></body></html>";
server->send(200, "text/html", html);
}
/*
* =====================================================
* KLASA: SmartReefMenuLink
* =====================================================
* Dodaje do głównego menu SUPLA przyciski:
* - SmartReef SETTINGS
* - UPDATE FIRMWARE
*/
class SmartReefMenuLink : public Supla::HtmlElement {
public:
SmartReefMenuLink() : HtmlElement(Supla::HTML_SECTION_FORM) {}
void send(Supla::WebSender* sender) override {
sender->send("</div><button type=button onclick=\"window.location.href='/smartreef'\">SMARTREEF SETTINGS</button><button type=button onclick=\"window.location.href='/update'\">UPDATE FIRMWARE</button><div>");
}
};
bool isNotifEnabled(const char *key) {
auto cfg = Supla::Storage::ConfigInstance();
if (!cfg) return false;
uint8_t value = 0;
cfg->getUInt8(key, &value);
return value == 1;
}
void sendSmartReefNotification(int16_t context, const char *cfgKey, const char *title, const String &message) {
if (!isNotifEnabled(cfgKey)) return;
Supla::Notification::Send(context, title, message.c_str());
}
void notifyWaterCoolingState(bool isOn) {
sendSmartReefNotification(
0,
"notif_water_cooling",
"SmartReef",
isOn ? "Chłodzenie wody zostało włączone." : "Chłodzenie wody zostało wyłączone.");
}
void notifyCoverCoolingState(bool isOn) {
sendSmartReefNotification(
3,
"notif_cover_cooling",
"SmartReef",
isOn ? "Chłodzenie pokrywy zostało włączone." : "Chłodzenie pokrywy zostało wyłączone.");
}
void checkWaterCoolingNotification() {
static bool initialized = false;
static bool lastState = false;
bool currentState = digitalRead(COOLING_THERMOSTAT_GPIO) == HIGH;
if (!initialized) {
initialized = true;
lastState = currentState;
return;
}
if (currentState != lastState) {
notifyWaterCoolingState(currentState);
lastState = currentState;
}
}
void checkTemperatureErrorNotification() {
static uint32_t lastCheck = 0;
static uint32_t lastErrorMask = 0;
if (millis() - lastCheck < 60000) return;
lastCheck = millis();
uint32_t currentErrorMask = 0;
String problemSensors = "";
for (int i = 0; i < SENSOR_COUNT; i++) {
if (!dsSensors[i]) continue;
double value = dsSensors[i]->getValue();
if (value <= -274.0) {
currentErrorMask |= (1UL << i);
if (problemSensors.length() > 0) problemSensors += ", ";
problemSensors += SENSOR_NAMES[i];
}
}
uint32_t newErrors = currentErrorMask & ~lastErrorMask;
if (newErrors && problemSensors.length() > 0) {
sendSmartReefNotification(
-1,
"notif_temp_error",
"SmartReef",
"Błąd odczytu temperatury: " + problemSensors);
}
lastErrorMask = currentErrorMask;
}
void setRelayState(Supla::Control::Relay* relay, bool shouldBeOn) {
if (!relay) return;
if (shouldBeOn && !relay->isOn()) {
relay->turnOn();
} else if (!shouldBeOn && relay->isOn()) {
relay->turnOff();
}
}
void updateLightsFromHvacWeeklyProgram() {
if (!hvacCooling || !relayLightingSchedule || !relayLightingSchedule->isOn()) return;
bool nightLedOn = false;
bool fluorescent1On = false;
bool fluorescent2On = false;
bool fluorescent3On = false;
bool hiddenRemoteOn = false;
TWeeklyScheduleProgram currentProgram = hvacCooling->getCurrentProgram();
if (currentProgram.Mode != SUPLA_HVAC_MODE_OFF &&
currentProgram.Mode != SUPLA_HVAC_MODE_NOT_SET) {
// Program tygodniowy HVAC używa nastawy chłodzenia jako kodu sceny oświetlenia.
// 20.00 = LED nocne, 21.00 = dodatkowe oświetlenie, 22.00 = T5 1+2, 23.00 = T5 1+2+3.
switch (currentProgram.SetpointTemperatureCool) {
case 2000:
nightLedOn = true;
break;
case 2100:
hiddenRemoteOn = true;
break;
case 2200:
hiddenRemoteOn = true;
fluorescent1On = true;
fluorescent2On = true;
break;
case 2300:
hiddenRemoteOn = true;
fluorescent1On = true;
fluorescent2On = true;
fluorescent3On = true;
break;
}
}
setRelayState(relayNightLed, nightLedOn);
setRelayState(relayFluorescent1, fluorescent1On);
setRelayState(relayFluorescent2, fluorescent2On);
setRelayState(relayFluorescent3, fluorescent3On);
setRelayState(relayHiddenRemote, hiddenRemoteOn);
}
void updateLampCoolingLogic() {
if (!relayLampCooling) return;
uint8_t coolNight = 0, coolFl1 = 0, coolFl2 = 0, coolFl3 = 0, coolHidden = 0, coolTemp = 0;
int32_t coverTempOnX10 = DEFAULT_COVER_TEMP_ON_X10;
auto cfg = Supla::Storage::ConfigInstance();
if (cfg) {
cfg->getUInt8("cover_cool_night", &coolNight);
cfg->getUInt8("cover_cool_fl1", &coolFl1);
cfg->getUInt8("cover_cool_fl2", &coolFl2);
cfg->getUInt8("cover_cool_fl3", &coolFl3);
cfg->getUInt8("cover_cool_hidden", &coolHidden);
cfg->getUInt8("cover_cool_temp", &coolTemp);
cfg->getInt32("cover_temp_on", &coverTempOnX10);
if (coverTempOnX10 == 0) coverTempOnX10 = DEFAULT_COVER_TEMP_ON_X10;
}
bool coverFanDemand = false;
coverFanDemand |= coolNight && relayNightLed && relayNightLed->isOn();
coverFanDemand |= coolFl1 && relayFluorescent1 && relayFluorescent1->isOn();
coverFanDemand |= coolFl2 && relayFluorescent2 && relayFluorescent2->isOn();
coverFanDemand |= coolFl3 && relayFluorescent3 && relayFluorescent3->isOn();
coverFanDemand |= coolHidden && relayHiddenRemote && relayHiddenRemote->isOn();
if (coolTemp && COVER_TEMP_SENSOR_INDEX < SENSOR_COUNT && dsSensors[COVER_TEMP_SENSOR_INDEX]) {
double coverTemp = dsSensors[COVER_TEMP_SENSOR_INDEX]->getValue();
if (coverTemp > -274.0 && coverTemp >= (coverTempOnX10 / 10.0)) {
coverFanDemand = true;
}
}
bool previousState = relayLampCooling->isOn();
if (coverFanDemand && !previousState) {
relayLampCooling->turnOn();
} else if (!coverFanDemand && previousState) {
relayLampCooling->turnOff();
}
bool currentState = relayLampCooling->isOn();
static bool notificationInitialized = false;
static bool lastNotifiedState = false;
if (!notificationInitialized) {
notificationInitialized = true;
lastNotifiedState = currentState;
} else if (currentState != lastNotifiedState) {
notifyCoverCoolingState(currentState);
lastNotifiedState = currentState;
}
}
void setup() {
Serial.begin(115200);
delay(1000);
new Supla::Clock;
SuplaDevice.allowWorkInOfflineMode(0);
SuplaDevice.setName("SUPLA-SmartReef");
SuplaDevice.setSwVersion(firmware_version);
new Supla::Html::DeviceInfo(&SuplaDevice);
new Supla::Html::WifiParameters;
new Supla::Html::ProtocolParameters(false, false);
new Supla::Html::StatusLedParameters;
new Supla::Html::TimeParameters(&SuplaDevice);
new SmartReefMenuLink();
eeprom.setStateSavePeriod(5000);
auto coolingOutput = new Supla::Control::InternalPinOutput(COOLING_THERMOSTAT_GPIO);
hvacCooling = new Supla::Control::HvacBase(coolingOutput);
hvacCooling->getChannel()->setDefaultFunction(SUPLA_CHANNELFNC_HVAC_THERMOSTAT);
hvacCooling->setDefaultSubfunction(SUPLA_HVAC_SUBFUNCTION_COOL);
hvacCooling->setHeatCoolSupported(false);
hvacCooling->setFanSupported(false);
hvacCooling->setDrySupported(false);
hvacCooling->getChannel()->setChannelNumber(CH_HVAC_COOLING);
hvacCooling->setInitialCaption("Chłodzenie wody");
hvacCooling->setMainThermometerChannelNo(CH_TEMP_WATER);
hvacCooling->setDefaultTemperatureRoomMin(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 2000);
hvacCooling->setDefaultTemperatureRoomMax(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 3000);
hvacCooling->getChannel()->setHvacSetpointTemperatureCool(2550);
hvacCooling->setTemperatureHisteresisMin(10);
hvacCooling->setTemperatureHisteresisMax(1000);
hvacCooling->setTemperatureHisteresis(20);
relayLampCooling = new Supla::Control::Relay(LAMP_COOLING_GPIO, true);
relayNightLed = new Supla::Control::LightRelay(NIGHT_LED_GPIO, true);
relayFluorescent1 = new Supla::Control::LightRelay(FLUORESCENT_1_GPIO, true);
relayFluorescent2 = new Supla::Control::LightRelay(FLUORESCENT_2_GPIO, true);
relayFluorescent3 = new Supla::Control::LightRelay(FLUORESCENT_3_GPIO, true);
relayHiddenRemote = new Supla::Control::Relay(HIDDEN_REMOTE_GPIO, true);
relayLightingSchedule = new Supla::Control::VirtualRelay();
relayLampCooling->getChannel()->setChannelNumber(CH_COVER_COOLING);
relayLampCooling->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
relayLampCooling->setInitialCaption("Chłodzenie pokrywy");
relayNightLed->getChannel()->setChannelNumber(CH_NIGHT_LED);
relayNightLed->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
relayNightLed->setInitialCaption("LED nocne");
relayFluorescent1->getChannel()->setChannelNumber(CH_FLUORESCENT_1);
relayFluorescent1->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
relayFluorescent1->setInitialCaption("Blue Plus");
relayFluorescent2->getChannel()->setChannelNumber(CH_FLUORESCENT_2);
relayFluorescent2->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
relayFluorescent2->setInitialCaption("Blue Plus");
relayFluorescent3->getChannel()->setChannelNumber(CH_FLUORESCENT_3);
relayFluorescent3->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
relayFluorescent3->setInitialCaption("Coral Plus");
relayHiddenRemote->getChannel()->setChannelNumber(CH_HIDDEN_REMOTE);
relayHiddenRemote->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
relayHiddenRemote->setInitialCaption("Dodatkowe oświetlenie");
relayLightingSchedule->getChannel()->setChannelNumber(CH_LIGHTING_SCHEDULE);
relayLightingSchedule->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
relayLightingSchedule->setInitialCaption("Harmonogram oświetlania");
relayLightingSchedule->setDefaultStateRestore();
scanDS18B20();
for (int i = 0; i < SENSOR_COUNT; i++) {
dsSensors[i] = new ConfigurableDS18B20(i);
dsSensors[i]->getChannel()->setChannelNumber(CH_TEMP_WATER + i);
dsSensors[i]->setInitialCaption(SENSOR_NAMES[i]);
}
auto buttonCfg = new Supla::Control::Button(BUTTON_CFG_GPIO, true, true);
buttonCfg->configureAsConfigButton(&SuplaDevice);
SuplaDevice.setSuplaCACert(suplaCACert);
SuplaDevice.setSupla3rdPartyCACert(supla3rdCACert);
Supla::Notification::RegisterNotification(-1);
Supla::Notification::RegisterNotification(0);
Supla::Notification::RegisterNotification(3);
SuplaDevice.begin(SUPLA_CHANNEL_COUNT);
if (suplaServer.getServerPtr()) {
httpUpdater.setup(suplaServer.getServerPtr(), "/update");
suplaServer.getServerPtr()->on("/smartreef", HTTP_GET, handleSmartReefPage);
suplaServer.getServerPtr()->on("/smartreef", HTTP_POST, handleSmartReefSaveAction);
}
}
void loop() {
SuplaDevice.iterate();
static unsigned long lastCoolingLogic = 0;
if (millis() - lastCoolingLogic >= 500) {
lastCoolingLogic = millis();
updateLightsFromHvacWeeklyProgram();
updateLampCoolingLogic();
checkWaterCoolingNotification();
checkTemperatureErrorNotification();
}
static bool LOCAL_WEB_SERVER = false;
if (!LOCAL_WEB_SERVER && Supla::Network::IsReady()) {
LOCAL_WEB_SERVER = true;
SuplaDevice.handleAction(0, Supla::START_LOCAL_WEB_SERVER);
}
}
Co myśłiście o takim wykorzystaniu harmonogramu z termostatu?
Wszystko można zmienić w aplikacji, bez konieczności logowania się w cloud a co najwazniejsze działa lokalnie bez połaczenia z serwerem. Potrzebne tylko pierwsze uruchominie do synchronizacji czasu.
