SmartReef v1.0 - firmware do akwarium

User avatar
Lector
Posts: 2469
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 24 times

Post

Jeszcze w wersji testowej ale już się pochwale kolejnym dziełem z AI :)
Sterownik ma za zadanie pilnować temperatury wody i pokrywy z oświetleniem w akwarium, oraz sterować stopniowym załączaniem oświetlenia.
Screenshot 2026-07-02 at 11-23-07 SUPLA-SmartReef - SUPLA Cloud.png
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.
Screenshot 2026-07-02 at 11-23-21 Chłodzenie wody - SUPLA Cloud.png
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", &notifWaterCooling);
    cfg->getUInt8("notif_cover_cooling", &notifCoverCooling);
    cfg->getUInt8("notif_temp_error", &notifTempError);
  }
  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);
  }
}
Mile widziane sugestie :)

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.
You do not have the required permissions to view the files attached to this post.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/

Return to “Projekty użytkowników”