SmartPool - pomoc przy kodzie ;)

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

Post

Lector wrote: Fri May 22, 2026 9:26 pm Po odłączeniu zasilania teraz moduł przechodzi w tryb pierwszego uruchomienia - parowania.
jakby nie zapisywał danych serwera Supla.
Na "oko" może ten generowany html nie jest poprawny. Pierwszy z brzegu "DS18B20AssignmentParameter robi 3x otwarcie div i 2x zamknięcie.

Weź sobie otwórz stronę konfiguracyjną na kompie, daj "pokaż źródła", skopiuj wszystko i wklej do jakiegoś walidatora html online.

Łatwiej byłoby też coś pomóc, gdybyś logi wstawił z opisem co robisz, czego oczekujesz i co się dzieje.
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

Coś tam poprawiłem, szkoda ze nie ma ochotnika do testów.

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>

// ======================================================
// STORAGE
// ======================================================
#include <supla/storage/storage.h>
#include <supla/storage/config.h>
#include <supla/storage/littlefs_config.h>

// ======================================================
// WWW
// ======================================================
#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>

// ======================================================
// OTA
// ======================================================
#ifdef ARDUINO_ARCH_ESP32
#include <HTTPUpdateServer.h>
#else
#include <ESP8266HTTPUpdateServer.h>
#endif

// ======================================================
// DS18B20
// ======================================================
#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;
Supla::Device::StatusLed statusLed(STATUS_LED_GPIO, false);
Supla::EspWebServer suplaServer;

#ifdef ARDUINO_ARCH_ESP32
HTTPUpdateServer httpUpdater;
#else
ESP8266HTTPUpdateServer httpUpdater;
#endif

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::HvacBase* hvacPool = nullptr;

bool depChlorine = false;
bool depUV = false;

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

unsigned long heatPumpStartTime = 0; 
bool heatPumpRunningInAuto = false;

// Scan function for 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++; 
    }
  }
}

// ======================================================
// DS18B20 CUSTOM CLASS
// ======================================================
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];
    for (int i = 0; i < 8; i++) {
      snprintf(key, sizeof(key), "ds_%d_byte_%d", sensorNum, i);
      uint8_t b = 0;
      cfg->getUInt8(key, &b);
      deviceAddress[i] = b;
    }
  }
 protected:
  int sensorNum;
  DeviceAddress deviceAddress;
};

ConfigurableDS18B20* dsSensors[SENSOR_COUNT];

// ======================================================
// FUNKCJA OBSŁUGI ZAPISU FORMULARZA SMARTPOOL (POST /smartpool)
// ======================================================
void handleSmartPoolSaveAction() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;

  auto cfg = Supla::Storage::ConfigInstance();
  if (cfg) {
    // 1. Blokady
    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);

    // 2. Zależności pompy
    cfg->setUInt8("dep_chlorine", server->hasArg("dep_chlorine") ? 1 : 0);
    cfg->setUInt8("dep_uv", server->hasArg("dep_uv") ? 1 : 0);

    // 3. Przypisanie czujników
    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 byteKey[20];
        for (int i = 0; i < 8; i++) {
          snprintf(byteKey, sizeof(byteKey), "ds_%d_byte_%d", t, i);
          cfg->setUInt8(byteKey, (selectedIdx >= 0 && selectedIdx < discoveredCount) ? discoveredAddresses[selectedIdx][i] : 0);
        }
      }
    }

    // 4. Pompa ogrzewania
    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", "");
}

// ======================================================
// GENEROWANIE NOWOCZESNEJ PODSTRONY SMARTPOOL (GET /smartpool)
// ======================================================
void handleSmartPoolPage() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;

  auto cfg = Supla::Storage::ConfigInstance();
  String html = "";

  html += "<!DOCTYPE html><html lang=\"pl\"><head><meta charset=\"UTF-8\">"
          "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">"
          "<title>SmartPool Settings</title>"
          "<style>"
          "*, ::before, ::after { box-sizing: border-box; }"
          "html:focus-within { scroll-behavior: smooth; }"
          "body, html, h1, h3, p { margin: 0; padding: 0; font-family: HelveticaNeue, \"Helvetica Neue\", Helvetica, Arial, sans-serif; }"
          "body { background: #00d151; min-height: 100vh; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; font-size: 14px; font-weight: 400; color: #fff; padding-top: 40px; }"
          ".wrapper { width: 100%; max-width: 500px; padding: 20px 10px; }"
          ".box { background: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3); margin-bottom: 20px; color: #000; }"
          "h1 { font-weight: 300; font-size: 23px; margin-bottom: 20px; text-align: left; letter-spacing: 1px; }"
          ".box h3 { margin-top: 0; margin-bottom: 20px; font-weight: 300; font-size: 23px; text-align: left; color: #000; border-bottom: 1px solid #f0f0f0; padding-bottom: 10px; }"
          "select, input[type=number] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 14px; background: #fff; color: #333; outline: none; box-sizing: border-box; }"
          "select:focus, input[type=number]:focus { border-color: #00d151; }"
          "input[type=number] { text-align: center; }"
          ".sp-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }"
          ".sp-switch input { opacity: 0; width: 0; height: 0; }"
          ".sp-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 24px; }"
          ".sp-slider:before { position: absolute; content: \"\"; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }"
          "input:checked + .sp-slider { background-color: #00d151; }"
          "input:checked + .sp-slider:before { transform: translateX(20px); }"
          ".sp-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #f4f4f4; }"
          ".sp-row:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }"
          ".sp-row label { color: #333; font-weight: 500; margin: 0; cursor: pointer; }"
          ".ds-table { width: 100%; border-collapse: collapse; }"
          ".ds-table td { padding: 8px 0; vertical-align: middle; }"
          ".ds-label { font-weight: 500; color: #333; width: 45%; }"
          ".btn-group { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }"
          "button, .btn-link { background: #000; width: 100%; border: none; border-radius: 10px; padding: 14px; text-transform: uppercase; color: #fff; font-size: 16px; font-weight: bold; cursor: pointer; box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3); text-align: center; text-decoration: none; display: block; font-family: inherit; box-sizing: border-box; }"
          "#logo { display: block; margin: 0 auto 10px; height: 130px; width: auto; fill: #000; }"
          ".alert { background: #fff; color: #00d151; padding: 14px; border-radius: 10px; margin-bottom: 20px; text-align: center; font-weight: bold; box-shadow: 0 5px 6px rgba(0,0,0,0.2); font-size: 15px; transition: opacity 0.5s ease; opacity: 1; }"
          ".section-subtitle { margin: 15px 0 8px 0; font-weight: bold; color: #00d151; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }"
          "@media screen and (max-width: 530px) { .wrapper { padding: 20px 6vw; } }"
          "</style></head><body><div class=\"wrapper\">";

  // Wstrzyknięcie oryginalnego logotypu Supli
  html += "<svg id=\"logo\" version=\"1.1\" viewBox=\"0 0 200 200\" xml:space=\"preserve\">"
          "<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\"></path>"
          "</svg>";

  html += "<h1>SMARTPOOL</h1>";

  if (server->hasArg("saved")) {
    html += "<div id=\"savedAlert\" class=\"alert\">Ustawienia zostały pomyślnie zapisane!</div>";
  }

  html += "<form action=\"/smartpool\" method=\"POST\">";

  // Boks 1: Przypisanie czujników
  html += "<div class=\"box\"><h3>Czujniki DS18B20</h3><table class=\"ds-table\">";
  for (int t = 0; t < SENSOR_COUNT; t++) {
    uint8_t selectedIdx = 255;
    if (cfg) { char key[20]; snprintf(key, sizeof(key), "ds_idx_%d", t); cfg->getUInt8(key, &selectedIdx); }

    html += "<tr><td class=\"ds-label\">" + String(SENSOR_NAMES[t]) + "</td><td>";
    html += "<select name=\"ds_assign_" + String(t) + "\">";
    html += "<option value=\"255\"" + String(selectedIdx == 255 ? " selected" : "") + ">Nie przypisany</option>";
    for (int i = 0; i < discoveredCount; i++) {
      String idStr = "ID: ";
      for(int b=0; b<4; b++) { char hex[4]; snprintf(hex, sizeof(hex), "%02X", discoveredAddresses[i][b]); idStr += hex; }
      html += "<option value=\"" + String(i) + "\"" + String(selectedIdx == i ? " selected" : "") + ">" + idStr + "...</option>";
    }
    html += "</select></td></tr>";
  }
  html += "</table></div>";

  // Helper lambda dla przełączników
  auto addToggleSwitch = [](String name, String label, uint8_t val) {
    return "<div class=\"sp-row\">"
           "<label for=\"" + name + "\">" + label + "</label>"
           "<label class=\"sp-switch\">"
           "<input type=\"checkbox\" id=\"" + name + "\" name=\"" + name + "\" value=\"1\"" + String(val ? " checked" : "") + ">"
           "<span class=\"sp-slider\"></span>"
           "</label></div>";
  };

  // Boks 2: Blokady główne
  uint8_t l_pump = 0, l_chlor = 0, l_heat = 0, l_uv = 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); }
  
  html += "<div class=\"box\"><h3>Zabezpieczenie - blokada</h3>";
  html += addToggleSwitch("lock_pump", "Blokuj pompę filtracyjną przy STOP", l_pump);
  html += addToggleSwitch("lock_chlor", "Blokuj generator chloru/ozonu przy STOP", l_chlor);
  html += addToggleSwitch("lock_heat", "Blokuj pompę ogrzewania przy STOP", l_heat);
  html += addToggleSwitch("lock_uv", "Blokuj lampę UV-C przy STOP", l_uv);
  html += "</div>";

  // Boks 3: Zależności pompy
  uint8_t chlorine = 0, uv = 0;
  if (cfg) { cfg->getUInt8("dep_chlorine", &chlorine); cfg->getUInt8("dep_uv", &uv); }
  html += "<div class=\"box\"><h3>Zależności pracy urządzeń</h3>";
  html += addToggleSwitch("dep_chlorine", "Generator chloru wymaga przepływu", chlorine);
  html += addToggleSwitch("dep_uv", "Lampa UV-C wymaga przepływu", uv);
  html += "</div>";

  // Boks 4: Automatyka ogrzewania
  uint8_t h_on_t1 = 0, h_on_t2 = 0, h_off_t1 = 0, h_off_t2 = 0; int32_t h_on_diff = 0, h_off_diff = 0, h_min_time = 0;
  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>Automatyka ogrzewania</h3>";
  html += "<div class=\"section-subtitle\">Warunek startu pompy</div>";
  html += "<div style=\"display:flex; align-items:center; gap:8px; margin-bottom:15px;\"><select name=\"heat_on_t1\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">&lt;</span><select name=\"heat_on_t2\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">o</span>";
  html += "<input type=\"number\" name=\"heat_on_diff\" style=\"width:65px;\" value=\"" + String(h_on_diff) + "\"></div>";

  html += "<div class=\"section-subtitle\">Warunek stopu pompy</div>";
  html += "<div style=\"display:flex; align-items:center; gap:8px; margin-bottom:20px;\"><select name=\"heat_off_t1\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">&lt;</span><select name=\"heat_off_t2\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">o</span>";
  html += "<input type=\"number\" name=\"heat_off_diff\" style=\"width:65px;\" value=\"" + String(h_off_diff) + "\"></div>";

  html += "<div style=\"display:flex; align-items:center; justify-content:space-between; border-top:1px solid #f0f0f0; padding-top:15px;\">"
          "<label style=\"color:#333; font-weight:500;\">Minimalny czas pracy (min.)</label>"
          "<input type=\"number\" name=\"heat_min_time\" style=\"width:75px;\" value=\"" + String(h_min_time) + "\"></div></div>";

  // Przyciski akcji
  html += "<div class=\"btn-group\">";
  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 += "</div>";
  html += "</form></div>";

  // Skrypt JavaScript ukrywający komunikat
  html += "<script>"
          "setTimeout(function() {"
          "  var alertEl = document.getElementById('savedAlert');"
          "  if (alertEl) {"
          "    alertEl.style.opacity = '0';"
          "    setTimeout(function() { alertEl.style.display = 'none'; }, 500);"
          "  }"
          "}, 3000);"
          "</script>";

  html += "</body></html>";

  server->send(200, "text/html", html);
}

// ======================================================
// KLASA GENERUJĄCA LINK W GŁÓWNYM FORMULARZU
// ======================================================
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>");
  }
};

// ======================================================
// MAIN RELAY LOGIC
// ======================================================
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());

  if (mainLockActive) {
    if (lockPump && relayPump && relayPump->isOn()) { relayPump->turnOff(); relayPump->getChannel()->setNewValue(false); }
    if (lockChlorine && relayChlorine && relayChlorine->isOn()) { relayChlorine->turnOff(); relayChlorine->getChannel()->setNewValue(false); }
    if (lockHeating && relayHeatPump && relayHeatPump->isOn()) { relayHeatPump->turnOff(); relayHeatPump->getChannel()->setNewValue(false); heatPumpRunningInAuto = false; }
    if (lockUV && relayUV && relayUV->isOn()) { relayUV->turnOff(); relayUV->getChannel()->setNewValue(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;
  }

  if (hvacPool && relayHeatPump) {
    if (mainLockActive && lockHeating) {
      if (relayHeatPump->isOn()) { relayHeatPump->turnOff(); relayHeatPump->getChannel()->setNewValue(false); }
      heatPumpRunningInAuto = false;
      return;
    }

    bool hvacEnabled = hvacPool->getMode() != SUPLA_HVAC_MODE_OFF;
    bool hvacHeating = false;
    double currentTemp = dsSensors[0]->getValue(); 
    double targetTemp = hvacPool->getTemperatureSetpointHeat() / 100.0;
    double hysteresis = hvacPool->getCurrentHysteresis(false) / 100.0;

    if (hvacEnabled && currentTemp > -100 && currentTemp < (targetTemp - hysteresis)) { hvacHeating = true; }

    if (hvacEnabled && hvacHeating) {
      uint8_t on_t1_idx = 0, on_t2_idx = 0, off_t1_idx = 0, off_t2_idx = 0;
      int32_t on_diff = 0, off_diff = 0, min_time_min = 0;
      if (cfg) {
        cfg->getUInt8("heat_on_t1", &on_t1_idx); cfg->getUInt8("heat_on_t2", &on_t2_idx); cfg->getInt32("heat_on_diff", &on_diff);
        cfg->getUInt8("heat_off_t1", &off_t1_idx); cfg->getUInt8("heat_off_t2", &off_t2_idx); cfg->getInt32("heat_off_diff", &off_diff);
        cfg->getInt32("heat_min_time", &min_time_min);
      }
      double t_on1 = dsSensors[on_t1_idx]->getValue(); double t_on2 = dsSensors[on_t2_idx]->getValue();
      double t_off1 = dsSensors[off_t1_idx]->getValue(); double t_off2 = dsSensors[off_t2_idx]->getValue();
      unsigned long minTimeMs = (unsigned long)min_time_min * 60000;
      bool currentPumpState = relayHeatPump->isOn();

      if (!currentPumpState) {
        if (t_on1 > -100 && t_on2 > -100 && (t_on1 < (t_on2 - on_diff))) { relayHeatPump->turnOn(); heatPumpStartTime = millis(); heatPumpRunningInAuto = true; }
      } 
      else if (currentPumpState && heatPumpRunningInAuto) {
        if (millis() - heatPumpStartTime >= minTimeMs) {
          if (t_off1 > -100 && t_off2 > -100 && (t_off1 < (t_off2 + off_diff))) { relayHeatPump->turnOff(); heatPumpRunningInAuto = false; }
        }
      }
    } 
    else {
      if (heatPumpRunningInAuto && relayHeatPump->isOn()) {
        int32_t min_time_min = 0; if (cfg) cfg->getInt32("heat_min_time", &min_time_min);
        unsigned long minTimeMs = (unsigned long)min_time_min * 60000;
        if (millis() - heatPumpStartTime >= minTimeMs) { relayHeatPump->turnOff(); heatPumpRunningInAuto = false; }
      } else if (!hvacEnabled) {
        heatPumpRunningInAuto = false;
      }
    }
  }
}

// ======================================================
// SETUP
// ======================================================
void setup() {
  Serial.begin(115200); delay(1000); Serial.println("\nSTART"); delay(500);
  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);

  // Rejestracja linku w menu głównym
  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");

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

  // Poprawna konfiguracja przycisku i ActionTriggera
  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);

  scanDS18B20();
  for (int i = 0; i < SENSOR_COUNT; i++) {
    dsSensors[i] = new ConfigurableDS18B20(i);
    dsSensors[i]->getChannel()->setChannelNumber(7 + i);
    dsSensors[i]->setInitialCaption(SENSOR_NAMES[i]);
  }

  auto hvacOutput = new Supla::Control::InternalPinOutput(-1);
  hvacPool = new Supla::Control::HvacBase(hvacOutput);
  hvacPool->getChannel()->setChannelNumber(6);
  hvacPool->setInitialCaption("Termostat Basenu");
  hvacPool->setMainThermometerChannelNo(7); 
  hvacPool->setDefaultTemperatureRoomMin(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 2000);
  hvacPool->setDefaultTemperatureRoomMax(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 4000);
  hvacPool->setTemperatureSetpointHeat(3300);
  hvacPool->setTemperatureHisteresis(10);
  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);

  auto buttonCfg = new Supla::Control::Button(BUTTON_CFG_GPIO, true, true);
  buttonCfg->configureAsConfigButton(&SuplaDevice);

  SuplaDevice.setSuplaCACert(suplaCACert);
  SuplaDevice.setSupla3rdPartyCACert(supla3rdCACert);
  
  SuplaDevice.begin(21);

  // Bezpieczne przypisanie endpointów i HTTPUpdateServer PO wystartowaniu Supli
  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();

  // Nie obciążamy procesora ciągłym sprawdzaniem i odczytem Dallas (raz na 10 sekund)
  static unsigned long lastDependencyCheck = 0;
  if (millis() - lastDependencyCheck >= 10000) {
    lastDependencyCheck = millis();
    handleRelayDependencies();
  }

  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;
    if (hvacEnabled && currentTemp > -100 && currentTemp < (targetTemp - hysteresis)) { hvacHeating = true; }
    digitalWrite(RELAY_8_GPIO, hvacHeating ? HIGH : LOW);
  }

  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);
  }
}
Nie wiem dlaczego termostat nie ustawia się na 33 tak jak podałem w kodzie.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
vajera
Posts: 7286
Joined: Wed Oct 31, 2018 7:58 am
Location: Biedrusko
Has thanked: 289 times
Been thanked: 161 times

Post

Lector wrote: Thu May 28, 2026 7:03 pm Coś tam poprawiłem, szkoda ze nie ma ochotnika do testów.
Pomógłbym, ale wszystkie wolne chwile poświęcam aktualnie na poszukiwanie błędu w kodzie bramki Z2S :(
Nie wiem dlaczego termostat nie ustawia się na 33 tak jak podałem w kodzie.
To nie zadziała przed SuplaDevice.begin(..), spróbuj przenieść ustawianie tej wartości do loop() - w zadanych odstępach czasowych albo jednorazowo z użyciem jakiejś flagi.
Bramka Zigbee <=> SUPLA
Więcej informacji tutaj:
https://forum.supla.org/viewforum.php?f=127
FAQ https://forum.supla.org/viewtopic.php?t=17277
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Mała zmiana interfejsu ;)
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
veeroos
Posts: 1081
Joined: Sun Mar 20, 2022 9:30 am
Location: Głogów
Has thanked: 1 time
Been thanked: 9 times

Post

Jeszcze nie przyglądałem się kodowi, ale nie zadziała w termostacie czasami ustawienie flagi?:

Code: Select all

  hvac->getChannel()->setHvacSetpointTemperatureHeat(2100);
Ja tak ustawiam wstępnie temperaturę (21 stopni) w jednym z moich kodów, robię to w setupie
Zamel Mew-01, Zamel PEW-01, Zamel SBW-01, Zamel THW-01, Zamel SRW-01, Zamel mSLW-02, Auraton BOX + Indor Sensor, Airly Gate By Veeroos, Zigbee to Supla Gateway + sensors, Zamel SGW-01 and more

https://github.com/v33r005
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

veeroos wrote: Fri May 29, 2026 3:25 am Jeszcze nie przyglądałem się kodowi, ale nie zadziała w termostacie czasami ustawienie flagi?:

Code: Select all

  hvac->getChannel()->setHvacSetpointTemperatureHeat(2100);
Ja tak ustawiam wstępnie temperaturę (21 stopni) w jednym z moich kodów, robię to w setupie
U mnie zazwyczaj jest to 21 stopni.

O dzięki, teraz to działa.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
Zibi_007
Posts: 3985
Joined: Tue Oct 31, 2023 10:06 pm
Has thanked: 10 times
Been thanked: 31 times

Post

Lector wrote: Thu May 28, 2026 7:03 pm Coś tam poprawiłem, szkoda ze nie ma ochotnika do testów.

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>

// ======================================================
// STORAGE
// ======================================================
#include <supla/storage/storage.h>
#include <supla/storage/config.h>
#include <supla/storage/littlefs_config.h>

// ======================================================
// WWW
// ======================================================
#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>

// ======================================================
// OTA
// ======================================================
#ifdef ARDUINO_ARCH_ESP32
#include <HTTPUpdateServer.h>
#else
#include <ESP8266HTTPUpdateServer.h>
#endif

// ======================================================
// DS18B20
// ======================================================
#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;
Supla::Device::StatusLed statusLed(STATUS_LED_GPIO, false);
Supla::EspWebServer suplaServer;

#ifdef ARDUINO_ARCH_ESP32
HTTPUpdateServer httpUpdater;
#else
ESP8266HTTPUpdateServer httpUpdater;
#endif

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::HvacBase* hvacPool = nullptr;

bool depChlorine = false;
bool depUV = false;

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

unsigned long heatPumpStartTime = 0; 
bool heatPumpRunningInAuto = false;

// Scan function for 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++; 
    }
  }
}

// ======================================================
// DS18B20 CUSTOM CLASS
// ======================================================
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];
    for (int i = 0; i < 8; i++) {
      snprintf(key, sizeof(key), "ds_%d_byte_%d", sensorNum, i);
      uint8_t b = 0;
      cfg->getUInt8(key, &b);
      deviceAddress[i] = b;
    }
  }
 protected:
  int sensorNum;
  DeviceAddress deviceAddress;
};

ConfigurableDS18B20* dsSensors[SENSOR_COUNT];

// ======================================================
// FUNKCJA OBSŁUGI ZAPISU FORMULARZA SMARTPOOL (POST /smartpool)
// ======================================================
void handleSmartPoolSaveAction() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;

  auto cfg = Supla::Storage::ConfigInstance();
  if (cfg) {
    // 1. Blokady
    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);

    // 2. Zależności pompy
    cfg->setUInt8("dep_chlorine", server->hasArg("dep_chlorine") ? 1 : 0);
    cfg->setUInt8("dep_uv", server->hasArg("dep_uv") ? 1 : 0);

    // 3. Przypisanie czujników
    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 byteKey[20];
        for (int i = 0; i < 8; i++) {
          snprintf(byteKey, sizeof(byteKey), "ds_%d_byte_%d", t, i);
          cfg->setUInt8(byteKey, (selectedIdx >= 0 && selectedIdx < discoveredCount) ? discoveredAddresses[selectedIdx][i] : 0);
        }
      }
    }

    // 4. Pompa ogrzewania
    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", "");
}

// ======================================================
// GENEROWANIE NOWOCZESNEJ PODSTRONY SMARTPOOL (GET /smartpool)
// ======================================================
void handleSmartPoolPage() {
  auto server = suplaServer.getServerPtr();
  if (!server) return;

  auto cfg = Supla::Storage::ConfigInstance();
  String html = "";

  html += "<!DOCTYPE html><html lang=\"pl\"><head><meta charset=\"UTF-8\">"
          "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">"
          "<title>SmartPool Settings</title>"
          "<style>"
          "*, ::before, ::after { box-sizing: border-box; }"
          "html:focus-within { scroll-behavior: smooth; }"
          "body, html, h1, h3, p { margin: 0; padding: 0; font-family: HelveticaNeue, \"Helvetica Neue\", Helvetica, Arial, sans-serif; }"
          "body { background: #00d151; min-height: 100vh; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; font-size: 14px; font-weight: 400; color: #fff; padding-top: 40px; }"
          ".wrapper { width: 100%; max-width: 500px; padding: 20px 10px; }"
          ".box { background: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3); margin-bottom: 20px; color: #000; }"
          "h1 { font-weight: 300; font-size: 23px; margin-bottom: 20px; text-align: left; letter-spacing: 1px; }"
          ".box h3 { margin-top: 0; margin-bottom: 20px; font-weight: 300; font-size: 23px; text-align: left; color: #000; border-bottom: 1px solid #f0f0f0; padding-bottom: 10px; }"
          "select, input[type=number] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 14px; background: #fff; color: #333; outline: none; box-sizing: border-box; }"
          "select:focus, input[type=number]:focus { border-color: #00d151; }"
          "input[type=number] { text-align: center; }"
          ".sp-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }"
          ".sp-switch input { opacity: 0; width: 0; height: 0; }"
          ".sp-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 24px; }"
          ".sp-slider:before { position: absolute; content: \"\"; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }"
          "input:checked + .sp-slider { background-color: #00d151; }"
          "input:checked + .sp-slider:before { transform: translateX(20px); }"
          ".sp-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #f4f4f4; }"
          ".sp-row:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }"
          ".sp-row label { color: #333; font-weight: 500; margin: 0; cursor: pointer; }"
          ".ds-table { width: 100%; border-collapse: collapse; }"
          ".ds-table td { padding: 8px 0; vertical-align: middle; }"
          ".ds-label { font-weight: 500; color: #333; width: 45%; }"
          ".btn-group { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }"
          "button, .btn-link { background: #000; width: 100%; border: none; border-radius: 10px; padding: 14px; text-transform: uppercase; color: #fff; font-size: 16px; font-weight: bold; cursor: pointer; box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3); text-align: center; text-decoration: none; display: block; font-family: inherit; box-sizing: border-box; }"
          "#logo { display: block; margin: 0 auto 10px; height: 130px; width: auto; fill: #000; }"
          ".alert { background: #fff; color: #00d151; padding: 14px; border-radius: 10px; margin-bottom: 20px; text-align: center; font-weight: bold; box-shadow: 0 5px 6px rgba(0,0,0,0.2); font-size: 15px; transition: opacity 0.5s ease; opacity: 1; }"
          ".section-subtitle { margin: 15px 0 8px 0; font-weight: bold; color: #00d151; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }"
          "@media screen and (max-width: 530px) { .wrapper { padding: 20px 6vw; } }"
          "</style></head><body><div class=\"wrapper\">";

  // Wstrzyknięcie oryginalnego logotypu Supli
  html += "<svg id=\"logo\" version=\"1.1\" viewBox=\"0 0 200 200\" xml:space=\"preserve\">"
          "<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\"></path>"
          "</svg>";

  html += "<h1>SMARTPOOL</h1>";

  if (server->hasArg("saved")) {
    html += "<div id=\"savedAlert\" class=\"alert\">Ustawienia zostały pomyślnie zapisane!</div>";
  }

  html += "<form action=\"/smartpool\" method=\"POST\">";

  // Boks 1: Przypisanie czujników
  html += "<div class=\"box\"><h3>Czujniki DS18B20</h3><table class=\"ds-table\">";
  for (int t = 0; t < SENSOR_COUNT; t++) {
    uint8_t selectedIdx = 255;
    if (cfg) { char key[20]; snprintf(key, sizeof(key), "ds_idx_%d", t); cfg->getUInt8(key, &selectedIdx); }

    html += "<tr><td class=\"ds-label\">" + String(SENSOR_NAMES[t]) + "</td><td>";
    html += "<select name=\"ds_assign_" + String(t) + "\">";
    html += "<option value=\"255\"" + String(selectedIdx == 255 ? " selected" : "") + ">Nie przypisany</option>";
    for (int i = 0; i < discoveredCount; i++) {
      String idStr = "ID: ";
      for(int b=0; b<4; b++) { char hex[4]; snprintf(hex, sizeof(hex), "%02X", discoveredAddresses[i][b]); idStr += hex; }
      html += "<option value=\"" + String(i) + "\"" + String(selectedIdx == i ? " selected" : "") + ">" + idStr + "...</option>";
    }
    html += "</select></td></tr>";
  }
  html += "</table></div>";

  // Helper lambda dla przełączników
  auto addToggleSwitch = [](String name, String label, uint8_t val) {
    return "<div class=\"sp-row\">"
           "<label for=\"" + name + "\">" + label + "</label>"
           "<label class=\"sp-switch\">"
           "<input type=\"checkbox\" id=\"" + name + "\" name=\"" + name + "\" value=\"1\"" + String(val ? " checked" : "") + ">"
           "<span class=\"sp-slider\"></span>"
           "</label></div>";
  };

  // Boks 2: Blokady główne
  uint8_t l_pump = 0, l_chlor = 0, l_heat = 0, l_uv = 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); }
  
  html += "<div class=\"box\"><h3>Zabezpieczenie - blokada</h3>";
  html += addToggleSwitch("lock_pump", "Blokuj pompę filtracyjną przy STOP", l_pump);
  html += addToggleSwitch("lock_chlor", "Blokuj generator chloru/ozonu przy STOP", l_chlor);
  html += addToggleSwitch("lock_heat", "Blokuj pompę ogrzewania przy STOP", l_heat);
  html += addToggleSwitch("lock_uv", "Blokuj lampę UV-C przy STOP", l_uv);
  html += "</div>";

  // Boks 3: Zależności pompy
  uint8_t chlorine = 0, uv = 0;
  if (cfg) { cfg->getUInt8("dep_chlorine", &chlorine); cfg->getUInt8("dep_uv", &uv); }
  html += "<div class=\"box\"><h3>Zależności pracy urządzeń</h3>";
  html += addToggleSwitch("dep_chlorine", "Generator chloru wymaga przepływu", chlorine);
  html += addToggleSwitch("dep_uv", "Lampa UV-C wymaga przepływu", uv);
  html += "</div>";

  // Boks 4: Automatyka ogrzewania
  uint8_t h_on_t1 = 0, h_on_t2 = 0, h_off_t1 = 0, h_off_t2 = 0; int32_t h_on_diff = 0, h_off_diff = 0, h_min_time = 0;
  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>Automatyka ogrzewania</h3>";
  html += "<div class=\"section-subtitle\">Warunek startu pompy</div>";
  html += "<div style=\"display:flex; align-items:center; gap:8px; margin-bottom:15px;\"><select name=\"heat_on_t1\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">&lt;</span><select name=\"heat_on_t2\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">o</span>";
  html += "<input type=\"number\" name=\"heat_on_diff\" style=\"width:65px;\" value=\"" + String(h_on_diff) + "\"></div>";

  html += "<div class=\"section-subtitle\">Warunek stopu pompy</div>";
  html += "<div style=\"display:flex; align-items:center; gap:8px; margin-bottom:20px;\"><select name=\"heat_off_t1\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">&lt;</span><select name=\"heat_off_t2\" style=\"flex:2;\">";
  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 style=\"font-weight:bold; color:#666;\">o</span>";
  html += "<input type=\"number\" name=\"heat_off_diff\" style=\"width:65px;\" value=\"" + String(h_off_diff) + "\"></div>";

  html += "<div style=\"display:flex; align-items:center; justify-content:space-between; border-top:1px solid #f0f0f0; padding-top:15px;\">"
          "<label style=\"color:#333; font-weight:500;\">Minimalny czas pracy (min.)</label>"
          "<input type=\"number\" name=\"heat_min_time\" style=\"width:75px;\" value=\"" + String(h_min_time) + "\"></div></div>";

  // Przyciski akcji
  html += "<div class=\"btn-group\">";
  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 += "</div>";
  html += "</form></div>";

  // Skrypt JavaScript ukrywający komunikat
  html += "<script>"
          "setTimeout(function() {"
          "  var alertEl = document.getElementById('savedAlert');"
          "  if (alertEl) {"
          "    alertEl.style.opacity = '0';"
          "    setTimeout(function() { alertEl.style.display = 'none'; }, 500);"
          "  }"
          "}, 3000);"
          "</script>";

  html += "</body></html>";

  server->send(200, "text/html", html);
}

// ======================================================
// KLASA GENERUJĄCA LINK W GŁÓWNYM FORMULARZU
// ======================================================
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>");
  }
};

// ======================================================
// MAIN RELAY LOGIC
// ======================================================
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());

  if (mainLockActive) {
    if (lockPump && relayPump && relayPump->isOn()) { relayPump->turnOff(); relayPump->getChannel()->setNewValue(false); }
    if (lockChlorine && relayChlorine && relayChlorine->isOn()) { relayChlorine->turnOff(); relayChlorine->getChannel()->setNewValue(false); }
    if (lockHeating && relayHeatPump && relayHeatPump->isOn()) { relayHeatPump->turnOff(); relayHeatPump->getChannel()->setNewValue(false); heatPumpRunningInAuto = false; }
    if (lockUV && relayUV && relayUV->isOn()) { relayUV->turnOff(); relayUV->getChannel()->setNewValue(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;
  }

  if (hvacPool && relayHeatPump) {
    if (mainLockActive && lockHeating) {
      if (relayHeatPump->isOn()) { relayHeatPump->turnOff(); relayHeatPump->getChannel()->setNewValue(false); }
      heatPumpRunningInAuto = false;
      return;
    }

    bool hvacEnabled = hvacPool->getMode() != SUPLA_HVAC_MODE_OFF;
    bool hvacHeating = false;
    double currentTemp = dsSensors[0]->getValue(); 
    double targetTemp = hvacPool->getTemperatureSetpointHeat() / 100.0;
    double hysteresis = hvacPool->getCurrentHysteresis(false) / 100.0;

    if (hvacEnabled && currentTemp > -100 && currentTemp < (targetTemp - hysteresis)) { hvacHeating = true; }

    if (hvacEnabled && hvacHeating) {
      uint8_t on_t1_idx = 0, on_t2_idx = 0, off_t1_idx = 0, off_t2_idx = 0;
      int32_t on_diff = 0, off_diff = 0, min_time_min = 0;
      if (cfg) {
        cfg->getUInt8("heat_on_t1", &on_t1_idx); cfg->getUInt8("heat_on_t2", &on_t2_idx); cfg->getInt32("heat_on_diff", &on_diff);
        cfg->getUInt8("heat_off_t1", &off_t1_idx); cfg->getUInt8("heat_off_t2", &off_t2_idx); cfg->getInt32("heat_off_diff", &off_diff);
        cfg->getInt32("heat_min_time", &min_time_min);
      }
      double t_on1 = dsSensors[on_t1_idx]->getValue(); double t_on2 = dsSensors[on_t2_idx]->getValue();
      double t_off1 = dsSensors[off_t1_idx]->getValue(); double t_off2 = dsSensors[off_t2_idx]->getValue();
      unsigned long minTimeMs = (unsigned long)min_time_min * 60000;
      bool currentPumpState = relayHeatPump->isOn();

      if (!currentPumpState) {
        if (t_on1 > -100 && t_on2 > -100 && (t_on1 < (t_on2 - on_diff))) { relayHeatPump->turnOn(); heatPumpStartTime = millis(); heatPumpRunningInAuto = true; }
      } 
      else if (currentPumpState && heatPumpRunningInAuto) {
        if (millis() - heatPumpStartTime >= minTimeMs) {
          if (t_off1 > -100 && t_off2 > -100 && (t_off1 < (t_off2 + off_diff))) { relayHeatPump->turnOff(); heatPumpRunningInAuto = false; }
        }
      }
    } 
    else {
      if (heatPumpRunningInAuto && relayHeatPump->isOn()) {
        int32_t min_time_min = 0; if (cfg) cfg->getInt32("heat_min_time", &min_time_min);
        unsigned long minTimeMs = (unsigned long)min_time_min * 60000;
        if (millis() - heatPumpStartTime >= minTimeMs) { relayHeatPump->turnOff(); heatPumpRunningInAuto = false; }
      } else if (!hvacEnabled) {
        heatPumpRunningInAuto = false;
      }
    }
  }
}

// ======================================================
// SETUP
// ======================================================
void setup() {
  Serial.begin(115200); delay(1000); Serial.println("\nSTART"); delay(500);
  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);

  // Rejestracja linku w menu głównym
  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");

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

  // Poprawna konfiguracja przycisku i ActionTriggera
  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);

  scanDS18B20();
  for (int i = 0; i < SENSOR_COUNT; i++) {
    dsSensors[i] = new ConfigurableDS18B20(i);
    dsSensors[i]->getChannel()->setChannelNumber(7 + i);
    dsSensors[i]->setInitialCaption(SENSOR_NAMES[i]);
  }

  auto hvacOutput = new Supla::Control::InternalPinOutput(-1);
  hvacPool = new Supla::Control::HvacBase(hvacOutput);
  hvacPool->getChannel()->setChannelNumber(6);
  hvacPool->setInitialCaption("Termostat Basenu");
  hvacPool->setMainThermometerChannelNo(7); 
  hvacPool->setDefaultTemperatureRoomMin(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 2000);
  hvacPool->setDefaultTemperatureRoomMax(SUPLA_CHANNELFNC_HVAC_THERMOSTAT, 4000);
  hvacPool->setTemperatureSetpointHeat(3300);
  hvacPool->setTemperatureHisteresis(10);
  hvacPool->setTemperatureHisteresisMin(10);
  hvacPool->setTemperatureHisteresisMax(1000);

  auto buttonCfg = new Supla::Control::Button(BUTTON_CFG_GPIO, true, true);
  buttonCfg->configureAsConfigButton(&SuplaDevice);

  SuplaDevice.setSuplaCACert(suplaCACert);
  SuplaDevice.setSupla3rdPartyCACert(supla3rdCACert);
  
  SuplaDevice.begin(21);

  // Bezpieczne przypisanie endpointów i HTTPUpdateServer PO wystartowaniu Supli
  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();

  // Nie obciążamy procesora ciągłym sprawdzaniem i odczytem Dallas (raz na 10 sekund)
  static unsigned long lastDependencyCheck = 0;
  if (millis() - lastDependencyCheck >= 10000) {
    lastDependencyCheck = millis();
    handleRelayDependencies();
  }

  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;
    if (hvacEnabled && currentTemp > -100 && currentTemp < (targetTemp - hysteresis)) { hvacHeating = true; }
    digitalWrite(RELAY_8_GPIO, hvacHeating ? HIGH : LOW);
  }

  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);
  }
}
Nie wiem dlaczego termostat nie ustawia się na 33 tak jak podałem w kodzie.
To nie tak, że nikt nie chce pomóc w testowaniu. Chyba po prostu niewiele osób z Suplą ma basen całoroczny. Swój (15 m³) złożyłem na jesieni i od kilku dni (jak wróciłem) nie mogę się doprosić aby ktoś mi pomógł go rozłożyć i wszystko podłączyć (pompy, filtr, solary, itp.) a mam kontuzję ręki i sam nie dam rady. Chyba odpuszczę i kupię rodzinie karnety na basen :lol:
Ale jakbym jednak go złożył, to z chęcią przetestuję Twój soft 8-)
User avatar
Lector
Posts: 2470
Joined: Fri Nov 17, 2017 2:26 pm
Location: Poznań
Has thanked: 13 times
Been thanked: 25 times

Post

Nie mam całorocznego mam Intex XTR 17200 litrów na stelażu.
Jak masz również na stelażu to nie zwijają na zimę :) jak ja.
Poprzedni przez zwijanie popękał, później latałem, latki odpadały. Ale i tak ładnych lat mi służył i też już później nie chowałem.
Obecny 4 rok bez zwijania, woda bez wymiany 4 rok.
PXL_20260524_103808967.jpg
A tak wygląda po zimie, jak go odkrywam.
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

No i w końcu udało mi się zrobić prawdziwe blokowanie kanałów bez pstrykania przekaźnikiem :)
Mam nadzieje ze jutro znajdę czas na sprawdzenie tego na żywym organizmie łącznie z logiką temperatur.

Jak testy wyjdą pomyślnie to podam kod - prawdopodobnie w nowym wątku.
Niespełniony automatyk. :mrgreen:
https://3d-lamp.photos/
https://pool.lector.top/
User avatar
Zibi_007
Posts: 3985
Joined: Tue Oct 31, 2023 10:06 pm
Has thanked: 10 times
Been thanked: 31 times

Post

Lector wrote: Fri May 29, 2026 6:13 pm Nie mam całorocznego mam Intex XTR 17200 litrów na stelażu.
Jak masz również na stelażu to nie zwijają na zimę :) jak ja.
Poprzedni przez zwijanie popękał, później latałem, latki odpadały. Ale i tak ładnych lat mi służył i też już później nie chowałem.
Obecny 4 rok bez zwijania, woda bez wymiany 4 rok.
PXL_20260524_103808967.jpg
A tak wygląda po zimie, jak go odkrywam.
Nie, no spoko. Trzy lata nie zwijałem. Mam okrągły. Poza sezonem na wierzchu była czarna folia bąbelkowa i pokrowiec, podparty w środku tak, aby utworzył taki namiot, dzięki któremu deszcz i śnieg spływały na boki (poza basen). Po zimie też wyglądał nieźle. Mam odkurzacz basenowy, więc utrzymanie czystości nie jest problemem. Parametry wody pod kontrolą, więc problemu w sezonie też nie ma.
Zwinąć musiałem, bo przebudowywałem taras, na którym stał. Teraz nie ma chętnych na rozłożenie i podłączenie, natomiast kąpać się będą chcieli wszyscy ;-)
Image Wiesz, że Supla obsługuje Zigbee? Wstąp do Klubu Promila: https://forum.supla.org/viewtopic.php?t=18018
Image Szukasz nowego GG? Jest tutaj: https://forum.supla.org/viewtopic.php?t=18036

Return to “Pomoc”