SmartPool - pomoc przy kodzie ;)

User avatar
klew
Posts: 13907
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław
Has thanked: 134 times
Been thanked: 137 times

Post

vajera wrote: Tue Jun 02, 2026 7:35 pm
Lector wrote: Tue Jun 02, 2026 7:21 pm Takie może głupie pytanie. Czy zapis mojej konfiguracji ma być do tego samego pliku co supla?
W tedy moja konfiguracja nadpisywała ustawienia Supli?

Teraz próbuje rozbić na dwa pliki.
Jeżeli używasz klasy Config to możesz zapisywać do tego samego pliku, co Supla. Pamiętaj tylko, że domyślny rozmiar Config to chyba 1024 bajty.
Tak, i to jest nasz problem :)

Logi trzeba czytać :)

Code: Select all

[08:45:52]KeyValue: serialized configuration size is too big. Config will not work and may be lost
Daj sobie 2x większy config:

Code: Select all

Supla::LittleFsConfig configSupla{2048};
Potem sprawdź w logach po resecie jaki jest rozmiar pliku:

Code: Select all

[08:47:41]LittleFsConfig: file "/supla-dev.cfg" size 1011
Jak będzie blisko granicy (j/w) i pojawią się te logi z błędem serializacji, to przekroczyłeś rozmiar. Jak będziesz miał np. 1500 B, to jest ok i został jeszcze zapas.
Najlepsze suple dla Twojego domu :mrgreen:
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

klew wrote: Wed Jun 03, 2026 8:09 am
vajera wrote: Tue Jun 02, 2026 7:35 pm
Lector wrote: Tue Jun 02, 2026 7:21 pm Takie może głupie pytanie. Czy zapis mojej konfiguracji ma być do tego samego pliku co supla?
W tedy moja konfiguracja nadpisywała ustawienia Supli?

Teraz próbuje rozbić na dwa pliki.
Jeżeli używasz klasy Config to możesz zapisywać do tego samego pliku, co Supla. Pamiętaj tylko, że domyślny rozmiar Config to chyba 1024 bajty.
Tak, i to jest nasz problem :)

Logi trzeba czytać :)

Code: Select all

[08:45:52]KeyValue: serialized configuration size is too big. Config will not work and may be lost
Daj sobie 2x większy config:

Code: Select all

Supla::LittleFsConfig configSupla{2048};
Potem sprawdź w logach po resecie jaki jest rozmiar pliku:

Code: Select all

[08:47:41]LittleFsConfig: file "/supla-dev.cfg" size 1011
Jak będzie blisko granicy (j/w) i pojawią się te logi z błędem serializacji, to przekroczyłeś rozmiar. Jak będziesz miał np. 1500 B, to jest ok i został jeszcze zapas.
Dziś walka dała rezultat i ograniczyłem zapisany plik do 1018, no i teraz jest ok. Pewnie i tak zwiększę pamięć (co też proponowało (AI).
Niestety gdzieś mi się rozjechała logika u muszę ją jeszcze poprawić .
Za to zmieniłem sterowanie harmonogramem, harmonogram wyłącza pompę jak w harmonogramie mam wyłączony (szary kafelek) jakaś temperatura włącza pompę filtrująca.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
klew
Posts: 13907
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław
Has thanked: 134 times
Been thanked: 137 times

Post

Zwiększ sobie ten rozmiar config. 1024 B było ustawione głównie ze względu na esp8266.
Na esp32 (o ile nie robisz bramki ZigBee ;) ), jest pamięci pod dostatkiem
Najlepsze suple dla Twojego domu :mrgreen:
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Jednak to ja już głupieje :P
Logika też działa poprawnie, tak to jest jak na szybko przed pracą chcę się zrobić jak najwięcej i nie ten suwak wyłączyłem :P

Mam nadzieje ze jutro dopisze pogoda i podmienię sterownik przy basenie, no i oby nie zaskoczyły mnie DS18B20 ze nie będą działać :mrgreen:

Harmonogram sterują pompą filtracyjną + zależnymi kanałami.
Screenshot 2026-06-03 at 20-04-04 Termostat Basenu - SUPLA Cloud.png
A tak z ciekawości zapytam, każda godzina ma 4 programy czy idzie gdzieś wybrać w aplikacji lub cloud z którego ma korzystać termostat?
Bo ustawienie na sztywno we firmware to kiepskie rozwiązanie. Czyli aby tak szybko szło przełączyć miedzy 1 a 2.
W aplikacji widzę tylko jeden.

EDIT:
Dobra to godzina podzielona na 15 minut, tylko dlaczego w cloud pokazuje wszystkie cztery aktywne?
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/
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Nowe pytanie, to dodam jako nowy post.
Jak to jest ze podaje histerezę w kodzie

Code: Select all

  hvacPool->setTemperatureHisteresis(10);
  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);
A termostat nie widzi?
Screenshot 2026-06-03 at 20-11-54 Termostat Basenu - SUPLA Cloud.png
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/
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Tak to wygląda w aplikacji :)
https://youtube.com/shorts/AkCtXGtD8_U? ... -qEn078DuQ

Dodam ze wszystko to zrobiło AI, ja tylko dałem jeden przykład który kiedyś robiłem pod nawodnienie też z AI ale temat nie skończony :P
Nic nie napisałam samodzielnie w tym kodzie.
Do tego zostały zaprzęgnięte aż trzy generatory, bo jedne lepiej sobie radziły z jednym a inne z drugim.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
klew
Posts: 13907
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław
Has thanked: 134 times
Been thanked: 137 times

Post

W cloud lub w apce możesz sobie usuwając te programy do kwadransów
Najlepsze suple dla Twojego domu :mrgreen:
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Lector wrote: Wed Jun 03, 2026 6:13 pm Nowe pytanie, to dodam jako nowy post.
Jak to jest ze podaje histerezę w kodzie

Code: Select all

  hvacPool->setTemperatureHisteresis(10);
  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);
A termostat nie widzi?
Screenshot 2026-06-03 at 20-11-54 Termostat Basenu - SUPLA Cloud.png
Odpowiem sobie sam - zła kolejność, najpierw margines a później wartość.

Code: Select all

  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);
  hvacPool->setTemperatureHisteresis(10);
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Jestem zamrożony przez brak odczytu adresów DS18B20 i temperatur.
Na 5 czujników jakie mam żaden mi nie działa, nawet na prostych przykładach pod ESP lub z GG.

Nie wiem czy wina leży w jakiś bibliotekach.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Aktualny kod z wirtualnym jako harmonogram.

Code: Select all

#include <SuplaDevice.h>
#include <supla/network/esp_wifi.h>
#include <supla/control/relay.h>
#include <supla/control/button.h>
#include <supla/control/hvac_base.h>
#include <supla/clock/clock.h>
#include <supla/network/html/time_parameters.h>
#include <supla/network/html/hvac_parameters.h>
#include <supla/control/internal_pin_output.h>
#include <supla/device/status_led.h>
#include <supla/control/action_trigger.h>
#include <supla/control/virtual_relay.h>

#include <supla/storage/storage.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/device/supla_ca_cert.h>

#include <HTTPUpdateServer.h>
#include <EEPROM.h>
#include <supla/storage/eeprom.h> 

#include <OneWire.h>
#include <DallasTemperature.h>
#include <supla/sensor/thermometer.h>

// ======================================================
// GPIO - ESP32 Relay X8
// ======================================================
#define STATUS_LED_GPIO          23
#define BUTTON_CFG_GPIO          0

#define RELAY_1_GPIO             32
#define RELAY_2_GPIO             33
#define RELAY_3_GPIO             25
#define RELAY_4_GPIO             26
#define RELAY_5_GPIO             27
#define RELAY_6_GPIO             14
#define RELAY_7_GPIO             12
#define RELAY_8_GPIO             13

#define ONE_WIRE_BUS             16
#define LIGHT_BUTTON_GPIO        17

// ======================================================
// CONFIG & VARIABLES
// ======================================================
const char* const SENSOR_NAMES[] = {
  "Woda w basenie",
  "Woda w solarach",
  "Woda powrotna",
  "Powietrze"
};
const int SENSOR_COUNT = sizeof(SENSOR_NAMES) / sizeof(SENSOR_NAMES[0]);

Supla::ESPWifi wifi;
Supla::LittleFsConfig configSupla{2048};
Supla::Eeprom eeprom;
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::Relay* relayLock = nullptr;
Supla::Control::Relay* relayPump = nullptr;
Supla::Control::Relay* relayChlorine = nullptr;
Supla::Control::Relay* relayHeatPump = nullptr;
Supla::Control::Relay* relayUV = nullptr;
Supla::Control::Relay* relayLight = nullptr;
Supla::Control::VirtualRelay* virtualRelay = nullptr;
Supla::Control::HvacBase* hvacPool = nullptr;
bool depChlorine = false;
bool depUV = false;

bool lockPump = false;
bool lockChlorine = false;
bool lockHeating = false;
bool lockUV = false;

bool schControlDependencies = true;
bool schEnabled = true;
unsigned long heatPumpStartTime = 0;
bool heatPumpRunningInAuto = false;

// ======================================================
// OBSŁUGA EEPROM (ESP32)
// ======================================================
void zapiszWartoscEEPROM(int adres, uint8_t wartosc) {
  if (EEPROM.read(adres) != wartosc) { 
    EEPROM.write(adres, wartosc);
    EEPROM.commit();
  }
}

uint8_t odczytajWartoscEEPROM(int adres, uint8_t domyslnaWartosc) {
  uint8_t wartosc = EEPROM.read(adres);
  if (wartosc == 0xFF) {
    return domyslnaWartosc;
  }
  return wartosc;
}

// ======================================================
// DS18B20
// ======================================================
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];

// ======================================================
// FUNKCJE POMOCNICZE
// ======================================================
void setRelayBlocked(Supla::Control::Relay* relay, bool blocked) {
  if (!relay || !relay->getChannel()) return;
  if (blocked) {
    relay->turnOff();
    relay->getChannel()->setStateOffline();
  } else {
    relay->getChannel()->setStateOnline();
  }
}

// ======================================================
// ZAPIS FORMULARZA
// ======================================================
void handleSmartPoolSaveAction() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;

  auto cfg = Supla::Storage::ConfigInstance();
  if (cfg) {
    cfg->setUInt8("lock_pump", server->hasArg("lock_pump") ? 1 : 0);
    cfg->setUInt8("lock_chlor", server->hasArg("lock_chlor") ? 1 : 0);
    cfg->setUInt8("lock_heat", server->hasArg("lock_heat") ? 1 : 0);
    cfg->setUInt8("lock_uv", server->hasArg("lock_uv") ? 1 : 0);
    cfg->setUInt8("dep_chlorine", server->hasArg("dep_chlorine") ? 1 : 0);
    cfg->setUInt8("dep_uv", server->hasArg("dep_uv") ? 1 : 0);
    cfg->setUInt8("p_sch_dep", server->hasArg("p_sch_dep") ? 1 : 0);

    // Zoptymalizowany zapis adresów czujników do jednego klucza (Blob)
    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);
          }
       }
    }

    if (server->hasArg("heat_on_t1")) cfg->setUInt8("heat_on_t1", server->arg("heat_on_t1").toInt());
    if (server->hasArg("heat_on_t2")) cfg->setUInt8("heat_on_t2", server->arg("heat_on_t2").toInt());
    if (server->hasArg("heat_on_diff")) cfg->setInt32("heat_on_diff", server->arg("heat_on_diff").toInt());
    if (server->hasArg("heat_off_t1")) cfg->setUInt8("heat_off_t1", server->arg("heat_off_t1").toInt());
    if (server->hasArg("heat_off_t2")) cfg->setUInt8("heat_off_t2", server->arg("heat_off_t2").toInt());
    if (server->hasArg("heat_off_diff")) cfg->setInt32("heat_off_diff", server->arg("heat_off_diff").toInt());
    if (server->hasArg("heat_min_time")) cfg->setInt32("heat_min_time", server->arg("heat_min_time").toInt());

    cfg->commit();

    for (int i = 0; i < SENSOR_COUNT; i++) {
      if (dsSensors[i]) dsSensors[i]->loadAddress();
    }
  }

  server->sendHeader("Location", "/smartpool?saved=1");
  server->send(303, "text/plain", "");
}

// ======================================================
// STRONA WWW
// ======================================================
void handleSmartPoolPage() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;
  auto cfg = Supla::Storage::ConfigInstance();

  String html = "";
  html += "<!doctype html><html lang=en><head><meta charset=UTF-8><meta name=viewport content=width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no><title>SUPLA-SmartPool</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,Arial,sans-serif;margin:0;padding:0}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5;font-size:14px;background:#00d151;color:#fff}h1,h3{margin:10px 0;font-weight:300;font-size:23px}.wrapper{display:flex;flex-direction:column;justify-content:center;min-height:100vh}.content{text-align:center;padding:20px 10px}.box{background:#fff;color:#000;border-radius:10px;padding:5px 10px;margin:10px 0;box-shadow:0 5px 6px rgb(0 0 0 / .3)}#msg{position:fixed;top:0;left:0;width:100%;height:119px;box-sizing:border-box;padding:15px 20px;background:#ffe836;color:#000;text-align:center;font-size:26px;box-shadow:0 2px 5px rgb(0 0 0 / .3);z-index:9999;display:flex;align-items:center;justify-content:center}.form{text-align:left;max-width:500px;margin:0 auto;margin-top:-40px;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:#00d151}@media screen and (max-width:530px){.form-field{flex-direction:column}.form-field label{width:100%;margin:3px 0}}.form-field input,.form-field select{width:100%;border:1px solid #ccc;border-radius:6px;padding:3px 8px;background:#fff}.form-field.right-checkbox label:last-child{width:100%;text-align:center}.switch{position:relative;display:inline-block;width:51px;height:25px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;inset:0;background-color:#ccc;transition:.4s;border-radius:34px}.slider:before{position:absolute;content:\"\";height:17px;width:17px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%}input:checked+.slider{background-color:#00d151}input:checked+.slider:before{transform:translateX(26px)}button{display:block;color:#fff;background:#000;width:100%;border:0;border-radius:10px;padding:10px;text-transform:uppercase;cursor:pointer;font-size:1.3em;margin-top:15px}</style></head><body><div class=wrapper><div class=content><div class=form><h1>SmartPool</h1>";
  if (server->hasArg("saved")) html += "<div id=\"msg\">Data saved</div>";
  html += "<form action=/smartpool method=POST><input type=\"hidden\" name=\"supla_csrf\" value=\"" + server->arg("supla_csrf") + "\">";

  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" : "") + ">ID: " + idStr.substring(0, 8) + "..." + idStr.substring(12) + "</option>";
    }
    html += "</select></div></div>";
  }
  html += "</div>";

  uint8_t h_on_t1 = 0, h_on_t2 = 1, h_off_t1 = 0, h_off_t2 = 2;
  int32_t h_on_diff = 5, h_off_diff = 3, h_min_time = 5;
  if (cfg) {
    cfg->getUInt8("heat_on_t1", &h_on_t1);
    cfg->getUInt8("heat_on_t2", &h_on_t2);
    cfg->getInt32("heat_on_diff", &h_on_diff);
    cfg->getUInt8("heat_off_t1", &h_off_t1);
    cfg->getUInt8("heat_off_t2", &h_off_t2);
    cfg->getInt32("heat_off_diff", &h_off_diff);
    cfg->getInt32("heat_min_time", &h_min_time);
  }

  html += "<div class=box><h3>Pompa ogrzewania</h3>";
  html += "<div class=form-field style=gap:6px;><label>Warunek startu pompy</label><select name=heat_on_t1>";
  for (int i = 0; i < SENSOR_COUNT; i++) html += "<option value=\"" + String(i) + "\"" + String(h_on_t1 == i ? " selected" : "") + ">" + String(SENSOR_NAMES[i]) + "</option>";
  html += "</select><span>&lt;</span><select name=heat_on_t2>";
  for (int i = 0; i < SENSOR_COUNT; i++) html += "<option value=\"" + String(i) + "\"" + String(h_on_t2 == i ? " selected" : "") + ">" + String(SENSOR_NAMES[i]) + "</option>";
  html += "</select><span>o</span><input type=number min=0 name=heat_on_diff style=width:60px value=\"" + String(h_on_diff) + "\"></div>";
  html += "<div class=form-field style=gap:6px;><label>Warunek stopu pompy</label><select name=heat_off_t1>";
  for (int i = 0; i < SENSOR_COUNT; i++) html += "<option value=\"" + String(i) + "\"" + String(h_off_t1 == i ? " selected" : "") + ">" + String(SENSOR_NAMES[i]) + "</option>";
  html += "</select><span>&lt;</span><select name=heat_off_t2>";
  for (int i = 0; i < SENSOR_COUNT; i++) html += "<option value=\"" + String(i) + "\"" + String(h_off_t2 == i ? " selected" : "") + ">" + String(SENSOR_NAMES[i]) + "</option>";
  html += "</select><span>o</span><input type=number min=0 name=heat_off_diff style=width:60px value=\"" + String(h_off_diff) + "\"></div>";
  html += "<div class=form-field style=justify-content:space-between;><label>Minimalny czas pracy (min.)</label><input type=number min=0 name=heat_min_time style=width:60px;text-align:center value=\"" + String(h_min_time) + "\"></div></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 l_pump = 0, l_chlor = 0, l_heat = 0, l_uv = 0, depSch = 0;
  if (cfg) {
    cfg->getUInt8("lock_pump", &l_pump);
    cfg->getUInt8("lock_chlor", &l_chlor);
    cfg->getUInt8("lock_heat", &l_heat);
    cfg->getUInt8("lock_uv", &l_uv);
    cfg->getUInt8("p_sch_dep", &depSch);
  }

  html += "<div class=box><h3>Zabezpieczenie - blokada</h3>";
  html += addToggleSwitch("lock_pump", "Pompa filtracyjna", l_pump);
  html += addToggleSwitch("lock_chlor", "Generator chloru i ozonu", l_chlor);
  html += addToggleSwitch("lock_heat", "Pompa ogrzewania", l_heat);
  html += addToggleSwitch("lock_uv", "Lampa UV-C", l_uv);
  html += "</div>";

  uint8_t chlorine = 0, uv = 0;
  if (cfg) {
    cfg->getUInt8("dep_chlorine", &chlorine);
    cfg->getUInt8("dep_uv", &uv);
  }
  html += "<div class=box><h3>Kanały zależne od pompy filtracyjnej</h3>";
  html += addToggleSwitch("dep_chlorine", "Generator chloru", chlorine);
  html += addToggleSwitch("dep_uv", "Lampa UV-C", uv);
  html += "</div>";

  html += "<div class=box><h3>Harmonogramy pompy filtracyjnej</h3>";
  html += addToggleSwitch("p_sch_dep", "Steruj kanałami zależnymi", depSch);
  html += "</div>";

  html += "<script>setTimeout(function(){var msgEl=document.getElementById('msg');if(msgEl){msgEl.remove();window.history.replaceState({},document.title,'/smartpool');}},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);
}

class SmartPoolMenuLink : public Supla::HtmlElement {
 public:
  SmartPoolMenuLink() : HtmlElement(Supla::HTML_SECTION_FORM) {}
  void send(Supla::WebSender* sender) override {
    sender->send("</div><button type=button onclick=\"window.location.href='/smartpool'\">SMARTPOOL SETTINGS</button><button type=button onclick=\"window.location.href='/update'\">UPDATE FIRMWARE</button><div>");
  }
};

// ======================================================
// HARMONOGRAM HVAC
// ======================================================
void checkHvacSchedulePump() {
  auto cfg = Supla::Storage::ConfigInstance();
  if (!cfg || !relayPump || !hvacPool || !virtualRelay) return;

  bool mainLockActive = (relayLock && relayLock->isOn());

  if (mainLockActive && lockPump) {
    return;
  }

  bool harmonogramActive = virtualRelay->isOn();
  if (!harmonogramActive) return;

  auto currentProgram = hvacPool->getCurrentProgram();
  double my_temp = currentProgram.SetpointTemperatureHeat / 100.0;

  bool shouldBeOn = false;
  if (my_temp >= 20.0 && my_temp <= 40.0) {
    shouldBeOn = true;
  } else {
    shouldBeOn = false;
  }

  bool pumpState = relayPump->isOn();
  if (shouldBeOn && !pumpState) {
    relayPump->turnOn();

    uint8_t dep = 0;
    cfg->getUInt8("p_sch_dep", &dep);
    if (dep == 1) {
      bool schDepChlorine = false;
      bool schDepUV = false;
      uint8_t temp = 0;

      cfg->getUInt8("dep_chlorine", &temp);
      schDepChlorine = (temp == 1);

      cfg->getUInt8("dep_uv", &temp);
      schDepUV = (temp == 1);

      bool mainLockActive = (relayLock && relayLock->isOn());

      cfg->getUInt8("lock_chlor", &temp);
      if (mainLockActive && temp == 1) schDepChlorine = false;

      cfg->getUInt8("lock_uv", &temp);
      if (mainLockActive && temp == 1) schDepUV = false;

      if (schDepChlorine && relayChlorine && !relayChlorine->isOn()) {
        relayChlorine->turnOn();
      }
      if (schDepUV && relayUV && !relayUV->isOn()) {
        relayUV->turnOn();
      }
    }
  } else if (!shouldBeOn && pumpState) {
    relayPump->turnOff();
  }

  if (shouldBeOn && pumpState) {
    uint8_t dep = 0;
    cfg->getUInt8("p_sch_dep", &dep);

    if (dep == 1) {
      bool schDepChlorine = false;
      bool schDepUV = false;
      uint8_t temp = 0;

      cfg->getUInt8("dep_chlorine", &temp);
      schDepChlorine = (temp == 1);

      cfg->getUInt8("dep_uv", &temp);
      schDepUV = (temp == 1);

      bool mainLockActive = (relayLock && relayLock->isOn());

      cfg->getUInt8("lock_chlor", &temp);
      if (mainLockActive && temp == 1) schDepChlorine = false;

      cfg->getUInt8("lock_uv", &temp);
      if (mainLockActive && temp == 1) schDepUV = false;

      if (schDepChlorine && relayChlorine && !relayChlorine->isOn()) {
        relayChlorine->turnOn();
      }

      if (schDepUV && relayUV && !relayUV->isOn()) {
        relayUV->turnOn();
      }
    }
  }
}

// ======================================================
// LOGIKA ZALEŻNOŚCI
// ======================================================
void handleRelayDependencies() {
  auto cfg = Supla::Storage::ConfigInstance();
  if (cfg) {
    uint8_t temp = 0;
    cfg->getUInt8("lock_pump", &temp); lockPump = (temp == 1);
    cfg->getUInt8("lock_chlor", &temp); lockChlorine = (temp == 1);
    cfg->getUInt8("lock_heat", &temp); lockHeating = (temp == 1);
    cfg->getUInt8("lock_uv", &temp); lockUV = (temp == 1);
    cfg->getUInt8("dep_chlorine", &temp); depChlorine = (temp == 1);
    cfg->getUInt8("dep_uv", &temp); depUV = (temp == 1);
  }

  bool mainLockActive = (relayLock && relayLock->isOn());

  setRelayBlocked(relayPump, mainLockActive && lockPump);
  setRelayBlocked(relayChlorine, mainLockActive && lockChlorine);
  setRelayBlocked(relayHeatPump, mainLockActive && lockHeating);
  setRelayBlocked(relayUV, mainLockActive && lockUV);

  if (mainLockActive && lockHeating) {
    heatPumpRunningInAuto = false;
  }

  if (relayPump && !(mainLockActive && lockPump)) {
    static bool lastPumpState = false;
    bool currentPumpState = relayPump->isOn();

    if (lastPumpState == true && currentPumpState == false) {
      if (depChlorine && relayChlorine && relayChlorine->isOn()) relayChlorine->turnOff();
      if (depUV && relayUV && relayUV->isOn()) relayUV->turnOff();
      lastPumpState = currentPumpState;
      return;
    }

    if (!currentPumpState) {
      if (depChlorine && relayChlorine && relayChlorine->isOn()) {
        relayPump->turnOn();
        currentPumpState = true;
      } else if (depUV && relayUV && relayUV->isOn()) {
        relayPump->turnOn();
        currentPumpState = true;
      }
    }

    if (!currentPumpState) {
      if (depChlorine && relayChlorine && relayChlorine->isOn()) relayChlorine->turnOff();
      if (depUV && relayUV && relayUV->isOn()) relayUV->turnOff();
    }

    lastPumpState = currentPumpState;
  }
}

void handleRelayLocks() {
  bool mainLockActive = (relayLock && relayLock->isOn());
  setRelayBlocked(relayPump, mainLockActive && lockPump);
  setRelayBlocked(relayChlorine, mainLockActive && lockChlorine);
  setRelayBlocked(relayHeatPump, mainLockActive && lockHeating);
  setRelayBlocked(relayUV, mainLockActive && lockUV);
  if (mainLockActive && lockHeating) {
    heatPumpRunningInAuto = false;
  }
}

// ======================================================
// SETUP
// ======================================================
void setup() {
  Serial.begin(115200);
  delay(1000);
  
  EEPROM.begin(512);
  delay(10);

  new Supla::Clock;

  SuplaDevice.allowWorkInOfflineMode(0);
  SuplaDevice.setName("SUPLA-SmartPool");
  SuplaDevice.setSwVersion("SmartPool v1.5");

  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 SmartPoolMenuLink();

  relayLock = new Supla::Control::Relay(RELAY_1_GPIO, true);
  relayPump = new Supla::Control::Relay(RELAY_2_GPIO, true);
  relayChlorine = new Supla::Control::Relay(RELAY_3_GPIO, true);
  relayHeatPump = new Supla::Control::Relay(RELAY_4_GPIO, true);
  relayUV = new Supla::Control::Relay(RELAY_5_GPIO, true);
  relayLight = new Supla::Control::Relay(RELAY_6_GPIO, true);

  relayLock->getChannel()->setChannelNumber(0);
  relayLock->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
  relayLock->setInitialCaption("Zabezpieczenie");

  relayPump->getChannel()->setChannelNumber(1);
  relayPump->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
  relayPump->setInitialCaption("Pompa filtracyjna");

  relayChlorine->getChannel()->setChannelNumber(2);
  relayChlorine->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
  relayChlorine->setInitialCaption("Generator chloru i ozonu");

  relayHeatPump->getChannel()->setChannelNumber(3);
  relayHeatPump->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
  relayHeatPump->setInitialCaption("Pompa ogrzewania");

  relayUV->getChannel()->setChannelNumber(4);
  relayUV->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
  relayUV->setInitialCaption("Lampa UV-C");

  relayLight->getChannel()->setChannelNumber(5);
  relayLight->setDefaultFunction(SUPLA_CHANNELFNC_LIGHTSWITCH);
  relayLight->setInitialCaption("Oświetlenie basenu");

  virtualRelay = new Supla::Control::VirtualRelay();
  virtualRelay->getChannel()->setChannelNumber(6);
  virtualRelay->setDefaultFunction(SUPLA_CHANNELFNC_POWERSWITCH);
  virtualRelay->setInitialCaption("Harmonogram pompy filtracyjnej");
  virtualRelay->setDefaultStateRestore();

  pinMode(RELAY_7_GPIO, OUTPUT);
  pinMode(RELAY_8_GPIO, OUTPUT);
  digitalWrite(RELAY_7_GPIO, LOW);
  digitalWrite(RELAY_8_GPIO, LOW);

  auto buttonLight = new Supla::Control::Button(LIGHT_BUTTON_GPIO, true, true);
  buttonLight->setHoldTime(1000);
  buttonLight->setMulticlickTime(300);

  auto atLight = new Supla::Control::ActionTrigger();
  atLight->setRelatedChannel(relayLight);
  atLight->attach(buttonLight);
  buttonLight->addAction(Supla::TOGGLE, relayLight, Supla::ON_PRESS);
  buttonLight->addAction(Supla::TOGGLE, relayLock, Supla::ON_HOLD);

  auto hvacOutput = new Supla::Control::InternalPinOutput(-1);
  hvacPool = new Supla::Control::HvacBase(hvacOutput);
  hvacPool->getChannel()->setChannelNumber(7);
  hvacPool->setInitialCaption("Termostat Basenu");
  hvacPool->setMainThermometerChannelNo(8);
  hvacPool->setDefaultTemperatureRoomMin(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 2000);
  hvacPool->setDefaultTemperatureRoomMax(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 4000);
  hvacPool->getChannel()->setHvacSetpointTemperatureHeat(3300);
  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);
  hvacPool->setTemperatureHisteresis(50);
  scanDS18B20();
  for (int i = 0; i < SENSOR_COUNT; i++) {
    dsSensors[i] = new ConfigurableDS18B20(i);
    dsSensors[i]->getChannel()->setChannelNumber(8 + 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);

  SuplaDevice.begin(21);
  if (suplaServer.getServerPtr()) {
    httpUpdater.setup(suplaServer.getServerPtr(), "/update");
    suplaServer.getServerPtr()->on("/smartpool", HTTP_GET, handleSmartPoolPage);
    suplaServer.getServerPtr()->on("/smartpool", HTTP_POST, handleSmartPoolSaveAction);
  }
}

// ======================================================
// LOOP
// ======================================================
void loop() {
  SuplaDevice.iterate();
  handleRelayLocks();

  static unsigned long lastTimeAndTempCheck = 0;
  if (millis() - lastTimeAndTempCheck >= 10000) {
    lastTimeAndTempCheck = millis();
    checkHvacSchedulePump();
    if (hvacPool) {
      bool hvacEnabled = hvacPool->getMode() != SUPLA_HVAC_MODE_OFF;
      digitalWrite(RELAY_7_GPIO, hvacEnabled ? HIGH : LOW);
      bool hvacHeating = false;
      double currentTemp = dsSensors[0]->getValue();
      double targetTemp = hvacPool->getTemperatureSetpointHeat() / 100.0;
      double hysteresis = hvacPool->getCurrentHysteresis(false) / 100.0;
      
      // Zabezpieczenie: Warunek uruchomienia sprawdzany tylko, gdy odczyt jest większy od 0.0
      if (hvacEnabled && currentTemp > 0.0 && currentTemp < (targetTemp - hysteresis)) {
        hvacHeating = true;
      }
      bool mainLockActive = (relayLock && relayLock->isOn());
      if (mainLockActive && lockHeating) hvacHeating = false;
      digitalWrite(RELAY_8_GPIO, hvacHeating ? HIGH : LOW);
    }
  }

  handleRelayDependencies();

  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);
  }
}
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/

Return to “Pomoc”