#1 Проекти » котел FerroliBlueSens +CloudMQTT+Мобільний додаток Delphi 13 FMX » Вчора 23:02:08

phosaly
відповідей: 0

zobrazenna-viber-2026-09-02-21-09-26-824.jpg
zobrazenna-viber-2026-09-02-21-09-30-688.jpg
zobrazenna-viber-2026-09-02-22-00-50-362.jpg

// ---- Режим "Крива опалення" (погодозалежне регулювання) ----

ESP8266 «Котел» (opentherm_mqtt.ino) — документація
Плата виконує роль шлюзу: спілкується з котлом по протоколу OpenTherm, а з Delphi-застосунком — через MQTT (брокер CloudAMQP).

1. Підключення пінів (NodeMCU)
Пін    GPIO    Призначення    Напрямок
D7    GPIO13    OpenTherm-адаптер — IN    вхід (переривання)
D8    GPIO15    OpenTherm-адаптер — OUT    вихід
D5    GPIO14    Реле насоса    вихід (RELAY_ON=LOW / RELAY_OFF=HIGH)
D3    GPIO0    Кнопка скидання WiFi (вбудована FLASH-кнопка)    вхід, INPUT_PULLUP
3.3V / GND    —    Живлення OpenTherm-адаптера та реле    —

⚠️ D3 (GPIO0) зайнятий кнопкою WiFi-reset — не використовуйте його для реле чи інших периферійних пристроїв (це "strapping"-пін, критичний для завантаження ESP8266).

2. Загальна логіка роботи
setup()
├─ connectWiFi()     — WiFiManager: підключення / точка доступу "Boiler"
├─ ot.begin(...)     — ініціалізація бібліотеки OpenTherm
├─ pinMode(RELAYPUMP, OUTPUT) — реле в безпечний стан (вимкнено)
└─ mqtt.setServer/setCallback

loop()  (виконується постійно)
├─ mqtt.loop()               — обробка вхідних MQTT-повідомлень
├─ якщо минув OT_INTERVAL (1с)  → otCycle()   — обмін з котлом
│    └─ раз на кілька циклів    → readExtParamsStep()
└─ якщо минув PUB_INTERVAL      → publishState() — публікація стану в MQTT
3. Опис функцій
connectWiFi()

Піднімає WiFi через WiFiManager. Якщо при старті утримати кнопку WiFi-reset (D3/GPIO0) — скидає збережені налаштування мережі й відкриває точку доступу "Boiler" для повторного налаштування.

connectMQTT()

Підключається до брокера (goose.rmq2.cloudamqp.com), публікує online (LWT — offline при розриві), підписується на всі командні топіки (set/enable, set/temp, set/room_temp, pump/set, set/curve_mode) і одразу публікує поточні retained-стани (pump, curve_mode).

mqttCallback(topic, payload, length)

Єдина точка прийому команд від Delphi. За назвою топіка визначає, яке поле змінити:

Топік    Дія
home/boiler/set/enable    boiler.enabled = ...
home/boiler/set/temp    boiler.setTemp (перевірка діапазону 20–80°C)
home/boiler/set/room_temp    boiler.roomSetTemp (діапазон 5–30°C)
home/boiler/pump/set    setPump(msg == "1")
home/boiler/set/curve_mode    вмикає/вимикає режим «Крива опалення»
home/sensor/temp / temp_o    кеш кімнатної/зовнішньої температури (для кривої)
setPump(bool on)

Керує реле насоса: digitalWrite(RELAYPUMP, ...), зберігає стан у pumpState, публікує підтверджений статус у home/boiler/pump (retained).

otCycle()

Головний цикл обміну з котлом по OpenTherm (виконується раз на секунду):

Встановлює статус (CH увімкнено/вимкнено, чи є помилка, чи горить пальник).
Розраховує й надсилає уставку температури теплоносія — вручну (boiler.setTemp) або автоматично за кривою (computeHeatingCurve), якщо активний curveMode.
Надсилає уставку кімнатної температури.
Читає поточну температуру подачі й обратки.
Читає код і прапорці несправності (ASF-флаги).
computeHeatingCurve(desired, in, out)

Погодозалежний розрахунок температури теплоносія за квадратичною апроксимацією класичного «температурного графіка», з поправками на бажану кімнатну температуру та фактичне відхилення термостата.

readExtParamsStep()

Читає розширені параметри OpenTherm по одному за виклик (не всі одразу), щоб не блокувати loop() на кілька секунд: тиск у системі (ID18), температура вихлопу, зовнішня температура котла (ID27), рівень модуляції пальника (ID17), макс./мін. потужність (ID15).

publishState()

Публікує повний стан котла в MQTT:

окремі топіки (temp, temp_return, flame, fault_code, fault_flags) — для швидкого доступу;
один великий JSON у home/boiler/status — всі поля разом (enabled, temp, set_temp, room_set, pump, розширені OpenTherm- параметри, curve_mode, curve_calc_temp).
handleInterrupt()

Апаратний обробник переривання OpenTherm-бібліотеки (ICACHE_RAM_ATTR — виконується з RAM для швидкодії, обов'язково для переривань на ESP8266).

4. MQTT-топіки (підсумково)

ESP → Delphi (публікує, retained): home/boiler/status (JSON), home/boiler/temp, home/boiler/temp_return, home/boiler/flame, home/boiler/pump, home/boiler/curve_mode, home/boiler/online

Delphi → ESP (команди, підписка): home/boiler/set/enable, home/boiler/set/temp, home/boiler/set/room_temp, home/boiler/pump/set, home/boiler/set/curve_mode

#2 Проекти » котел FerroliBlueSens +CloudMQTT+Мобільний додаток Delphi 13 FMX » Вчора 22:46:05

phosaly
відповідей: 0

zobrazenna-viber-2026-09-02-21-09-26-824.jpg
zobrazenna-viber-2026-09-02-21-09-30-688.jpg
zobrazenna-viber-2026-09-01-11-43-42-171.jpg
zobrazenna-viber-2026-09-02-22-00-50-362.jpg

// ---- Режим "Крива опалення" (погодозалежне регулювання) ----

ESP8266 «Котел» (opentherm_mqtt.ino) — документація

Плата виконує роль шлюзу: спілкується з котлом по протоколу OpenTherm, а з Delphi-застосунком — через MQTT (брокер CloudAMQP).

1. Підключення пінів (NodeMCU)
Пін    GPIO    Призначення    Напрямок
D7    GPIO13    OpenTherm-адаптер — IN    вхід (переривання)
D8    GPIO15    OpenTherm-адаптер — OUT    вихід
D5    GPIO14    Реле насоса    вихід (RELAY_ON=LOW / RELAY_OFF=HIGH)
D3    GPIO0    Кнопка скидання WiFi (вбудована FLASH-кнопка)    вхід, INPUT_PULLUP
3.3V / GND    —    Живлення OpenTherm-адаптера та реле
2. Загальна логіка роботи
setup()
├─ connectWiFi()     — WiFiManager: підключення / точка доступу "Boiler"
├─ ot.begin(...)     — ініціалізація бібліотеки OpenTherm
├─ pinMode(RELAYPUMP, OUTPUT) — реле в безпечний стан (вимкнено)
└─ mqtt.setServer/setCallback

loop()  (виконується постійно)
├─ mqtt.loop()               — обробка вхідних MQTT-повідомлень
├─ якщо минув OT_INTERVAL (1с)  → otCycle()   — обмін з котлом
│    └─ раз на кілька циклів    → readExtParamsStep()
3. Опис функцій
connectWiFi()

Піднімає WiFi через WiFiManager. Якщо при старті утримати кнопку WiFi-reset (D3/GPIO0) — скидає збережені налаштування мережі й відкриває точку доступу "Boiler" для повторного налаштування.

connectMQTT()

Підключається до брокера (goose.rmq2.cloudamqp.com), публікує online (LWT — offline при розриві), підписується на всі командні топіки (set/enable, set/temp, set/room_temp, pump/set, set/curve_mode) і одразу публікує поточні retained-стани (pump, curve_mode).

mqttCallback(topic, payload, length)

Єдина точка прийому команд від Delphi. За назвою топіка визначає, яке поле змінити:

Топік    Дія
home/boiler/set/enable    boiler.enabled = ...
home/boiler/set/temp    boiler.setTemp (перевірка діапазону 20–80°C)
home/boiler/set/room_temp    boiler.roomSetTemp (діапазон 5–30°C)
home/boiler/pump/set    setPump(msg == "1")
home/boiler/set/curve_mode    вмикає/вимикає режим «Крива опалення»
home/sensor/temp / temp_o    кеш кімнатної/зовнішньої температури (для кривої)
setPump(bool on)

Керує реле насоса: digitalWrite(RELAYPUMP, ...), зберігає стан у pumpState, публікує підтверджений статус у home/boiler/pump (retained).

otCycle()

Головний цикл обміну з котлом по OpenTherm (виконується раз на секунду):

Встановлює статус (CH увімкнено/вимкнено, чи є помилка, чи горить пальник).
Розраховує й надсилає уставку температури теплоносія — вручну (boiler.setTemp) або автоматично за кривою (computeHeatingCurve), якщо активний curveMode.
Надсилає уставку кімнатної температури.
Читає поточну температуру подачі й обратки.
Читає код і прапорці несправності (ASF-флаги).
computeHeatingCurve(desired, in, out)

Погодозалежний розрахунок температури теплоносія за квадратичною апроксимацією класичного «температурного графіка», з поправками на бажану кімнатну температуру та фактичне відхилення термостата.

readExtParamsStep()

Читає розширені параметри OpenTherm по одному за виклик (не всі одразу), щоб не блокувати loop() на кілька секунд: тиск у системі (ID18), температура вихлопу, зовнішня температура котла (ID27), рівень модуляції пальника (ID17), макс./мін. потужність (ID15).

publishState()

Публікує повний стан котла в MQTT:

окремі топіки (temp, temp_return, flame, fault_code, fault_flags) — для швидкого доступу;
один великий JSON у home/boiler/status — всі поля разом (enabled, temp, set_temp, room_set, pump, розширені OpenTherm- параметри, curve_mode, curve_calc_temp).
handleInterrupt()

Апаратний обробник переривання OpenTherm-бібліотеки (ICACHE_RAM_ATTR — виконується з RAM для швидкодії, обов'язково для переривань на ESP8266).

4. MQTT-топіки (підсумково)

ESP → Delphi (публікує, retained): home/boiler/status (JSON), home/boiler/temp, home/boiler/temp_return, home/boiler/flame, home/boiler/pump, home/boiler/curve_mode, home/boiler/online

Delphi → ESP (команди, підписка): home/boiler/set/enable, home/boiler/set/temp, home/boiler/set/room_temp, home/boiler/pump/set, home/boiler/set/curve_mode

Прикладний код:
unit OTTypes;

{ ============================================================
  Типы и топики для OpenTherm MQTT проекта
  Delphi 13 Florence — FMX Android
  ============================================================ }

interface

uses
  System.SysUtils, System.Generics.Collections;

const


  TOPIC_SENSOR_TEMPDHT= 'home/sensor/temperature';
  TOPIC_SENSOR_HUM    = 'home/sensor/humidity';
  TOPIC_SENSOR_TEMPDHT_O= 'home/sensor/temperature_o';
  TOPIC_SENSOR_HUM_O    = 'home/sensor/humidity_o';

  TOPIC_SENSOR_TEMPDS = 'home/sensor/temperature_ds';
  TOPIC_SENSOR_STATUS = 'home/sensor/status';
  //Топики датчика газа MQ-2 ---
  TOPIC_SENSOR_GAS       = 'home/sensor/gas';       // сырое значение АЦП (0-1023)
  TOPIC_SENSOR_GAS_ALARM = 'home/sensor/gas_alarm'; // 0/1 - превышен порог

  // ---- Лампа (реле на 2-му ESP8266, тому ж, що і DHT) ----
  TOPIC_SENSOR_LAMP     = 'home/sensor/lamp';      // READ  — ESP публікує стан
  TOPIC_SENSOR_LAMP_SET = 'home/sensor/lamp/set';  // WRITE — Delphi надсилає команду
  // ---- Насос (реле на 2-му ESP8266, тому ж, що і DHT) ----
  TOPIC_SENSOR_PUMP     = 'home/sensor/pump';      // READ  — ESP публікує стан
  TOPIC_SENSOR_PUMP_SET=  'home/sensor/pump/set';  // WRITE — Delphi надсилає команду

  // ---- Режим (термостатopenTherm ESP8266, тому ж, що і ) ----
  TOPIC_SENSOR_MODEON     = 'home/sensor/modeon';      // READ  — ESP публікує стан
  TOPIC_SENSOR_MODEON_SET = 'home/sensor/modeon/set';  // WRITE — Delphi надсилає команду



  // ---- MQTT топики (READ — ESP публикует) ----
  TOPIC_STATUS      = 'home/boiler/status';
  TOPIC_TEMP        = 'home/boiler/temp';
  TOPIC_TEMP_RETURN = 'home/boiler/temp_return';
  TOPIC_FLAME       = 'home/boiler/flame';
  TOPIC_ONLINE      = 'home/boiler/online';
  TOPIC_FAULT_CODE  = 'home/boiler/fault_code';   // OEM fault code (0-255), напр. 38
  TOPIC_FAULT_FLAGS = 'home/boiler/fault_flags';  // біти ASF (сервіс/блок./тиск/полум'я/тяга/перегрів)
  TOPIC_BOILER_PUMP     =  'home/boiler/pump';      // READ  — ESP публікує стан
  TOPIC_BOILER_PUMP_SET =  'home/boiler/pump/set';  // WRITE — Delphi надсилає команду

  // ---- MQTT топики (WRITE — Delphi пишет) ----
  TOPIC_SET_ENABLE  = 'home/boiler/set/enable';
  TOPIC_SET_TEMP    = 'home/boiler/set/temp';
  TOPIC_SET_ROOM    = 'home/boiler/set/room_temp';

  // ---- Режим "Крива опалення" (погодозалежне регулювання) ----
  TOPIC_SET_CURVE_MODE = 'home/boiler/set/curve_mode'; // WRITE — 0/1
  TOPIC_CURVE_MODE     = 'home/boiler/curve_mode';     // READ  — поточний стан (retained)

  // ---- Лимиты ----
  TEMP_BOILER_MIN   = 20.0;
  TEMP_BOILER_MAX   = 80.0;
  TEMP_ROOM_MIN     = 5.0;
  TEMP_ROOM_MAX     = 30.0;
  // ---- Термостат кімнати (гістерезис) ----
  // Котел вимикається, коли кімнатна темп. (з DHT, ESP8266 №2) досягає
  // уставки (RoomSet), і вмикається знову, коли впаде на цю величину
  // нижче уставки. Без гістерезису котел клацав би вкл/викл щосекунди
  // на самій межі уставки.
  OnOffTempBoiler   = 1.5;

  // ---- График — максимум точек в истории ----
  HISTORY_MAX_POINTS = 60;  // 60 точек × 5 сек = 5 минут истории

type
  { Запись истории температуры }
  TTemperaturePoint = record
    Time  : TDateTime;
    Value : Double;
  end;

  { Полное состояние котла }
  TBoilerState = record
    Online      : Boolean;

  end;

  { Полное состояние датчики та реле }
  TSensorRelayState = record
  end;


  { Расписание — одна запись }
  TScheduleEntry = record
    DayOfWeek   : Integer;  // 1=Пн .. 7=Вс
    TimeStart   : TTime;
    TempTarget  : Double;
    Enabled     : Boolean;
  end;

  TScheduleList = TList<TScheduleEntry>;

{ Утилиты }
function FerroliFaultText(ACode: Integer): string;

implementation

uses
  System.JSON;

procedure TBoilerState.Reset;
begin
  Online      := False;
  OutsideTempSource := 'off';
end;

procedure TSensorRelayState.Reset;
begin
end;

function TempToColor(ATemp: Double): Cardinal;
begin
  if ATemp < 30 then
    Result := $FF4FC3F7   // синий
  else if ATemp < 50 then
    Result := $FF81C784   // зелёный
  else if ATemp < 65 then
    Result := $FFFFB74D   // оранжевый
  else
    Result := $FFEF5350;  // красный
end;

function FormatTemp(ATemp: Double): string;
begin
  Result := FormatFloat('0.0', ATemp);
end;

function BoilerStateFromJSON(const AJSON: string): TBoilerState;
var
  J: TJSONObject;
  V: TJSONValue;
begin
  Result.Reset;
  J := TJSONObject.ParseJSONValue(AJSON) as TJSONObject;
  if J = nil then Exit;
  try
    // ---- Крива опалення ----
    if J.TryGetValue('curve_mode',      V) then Result.CurveMode     := V.GetValue<Boolean>;
    if J.TryGetValue('curve_calc_temp', V) then Result.CurveCalcTemp := V.GetValue<Double>;
    if J.TryGetValue('outside_temp_source', V) then Result.OutsideTempSource := V.GetValue<string>;

    Result.LastUpdate := Now;
  finally
    J.Free;
  end;
end;

function SensorRelayFromJSON(const AJSON: string): TSensorRelayState;
var
  J: TJSONObject;
  V: TJSONValue;
begin
  Result.Reset;
  J := TJSONObject.ParseJSONValue(AJSON) as TJSONObject;
  if J = nil then Exit;
  try
    if J.TryGetValue('temperature', V) then Result.temperature := V.GetValue<Double>;
  finally
    J.Free;
  end;
end;

// ------------------------------------------------------------------
// ASF flags (Data-ID 5, старший байт) — стандартні OpenTherm біти,
// однакові для будь-якого котла.
// ------------------------------------------------------------------
function ASFFlagsText(AFlags: Integer): string;
var Parts: TArray<string>;
begin
  SetLength(Parts, 0);
  if (AFlags and $01) <> 0 then Parts := Parts + ['потрібне сервісне обслуговування'];
  if Length(Parts) = 0 then
    Result := ''
  else
    Result := string.Join(', ', Parts);
end;

// ------------------------------------------------------------------
// OEM fault code (Data-ID 5, молодший байт) → текст помилки Ferroli.
//
// !!! УВАГА: OpenTherm гарантує лише те, що котёл віддає своє власне
// (виробниче) число коду помилки в цьому байті — специфікація НЕ
// визначає, що це число обов'язково збігається з тим "Fxx", яке котёл
// друкує на дисплеї. У більшості виробників (в т.ч. зазвичай Ferroli)
// це число дійсно дорівнює номеру з дисплея (тобто код 38 у цьому
// байті = "F38" на панелі), але це потрібно ОБОВ'ЯЗКОВО перевірити
// на своєму котлі: спровокуйте (чи дочекайтесь) відому помилку і
// звірте faultCode із MQTT з тим, що показує панель котла.
//
// Таблицю нижче потрібно доповнити з сервісного мануалу саме на
// Ferroli Bluesens 4 (розділ "Codici di errore" / "Fault codes") —
// нижче лише каркас із кількома типовими для газових котлів кодами.
// ------------------------------------------------------------------
function FerroliFaultText(ACode: Integer): string;
begin
  case ACode of
    0:  Result := 'Помилок немає';
    04: Result := 'Проблемы с дымоходом Детальніше';
    05: Result := 'Неисправность вентилятор Детальніше';
    08: Result := 'Перегрев теплообменника Детальніше';
    10: Result := 'Несправність датчика температури подачі (NTC)';
    11: Result := 'Неисправность датчика температуры ГВС';
    12: Result := 'Несправність датчика тиску води / пресостата';
    13: Result := 'Недостатній тиск повітря (несправність димовідведення)';
    14: Result := 'Неисправность датчика температуры на выходе ГВС';
    20: Result := 'Несправність датчика зворотної води (NTC)';
    34: Result := 'Низкое напряжение';
    35: Result := 'Паразитне полум''я / несправність контролю полум''я';
    37: Result := 'Низкое давление в системе отопления';
    38: Result := 'Втрата полум''я під час роботи';
    39: Result := 'Потеря связи с выносной метеостанцией';
    40: Result := 'Низький тиск води в системі опалення';
    41: Result := 'Перегрів теплоносія / спрацював запобіжний термостат';
  else
    Result := Format('Код помилки %d — див. сервісний мануал Ferroli Bluesens', [ACode]);
  end;
end;

end.

#3 Проекти » котел FerroliBlueSens +CloudMQTT+Мобільний додаток Delphi 13 FMX » Вчора 22:44:41

phosaly
відповідей: 0

zobrazenna-viber-2026-09-02-21-09-26-824.jpg
zobrazenna-viber-2026-09-02-21-09-30-688.jpg
zobrazenna-viber-2026-09-01-11-43-42-171.jpg
zobrazenna-viber-2026-09-02-22-00-50-362.jpg

ESP8266 «Котел» (opentherm_mqtt.ino) — документація

Плата виконує роль шлюзу: спілкується з котлом по протоколу OpenTherm, а з Delphi-застосунком — через MQTT (брокер CloudAMQP).


// ---- Режим "Крива опалення" (погодозалежне регулювання) ----
krivaja-otoplenija.png
1. Підключення пінів (NodeMCU)
Пін    GPIO    Призначення    Напрямок
D7    GPIO13    OpenTherm-адаптер — IN    вхід (переривання)
D8    GPIO15    OpenTherm-адаптер — OUT    вихід
D5    GPIO14    Реле насоса    вихід (RELAY_ON=LOW / RELAY_OFF=HIGH)
D3    GPIO0    Кнопка скидання WiFi (вбудована FLASH-кнопка)    вхід, INPUT_PULLUP
3.3V / GND    —    Живлення OpenTherm-адаптера та реле
2. Загальна логіка роботи
setup()
├─ connectWiFi()     — WiFiManager: підключення / точка доступу "Boiler"
├─ ot.begin(...)     — ініціалізація бібліотеки OpenTherm
├─ pinMode(RELAYPUMP, OUTPUT) — реле в безпечний стан (вимкнено)
└─ mqtt.setServer/setCallback

loop()  (виконується постійно)
├─ mqtt.loop()               — обробка вхідних MQTT-повідомлень
├─ якщо минув OT_INTERVAL (1с)  → otCycle()   — обмін з котлом
│    └─ раз на кілька циклів    → readExtParamsStep()
3. Опис функцій
connectWiFi()

Піднімає WiFi через WiFiManager. Якщо при старті утримати кнопку WiFi-reset (D3/GPIO0) — скидає збережені налаштування мережі й відкриває точку доступу "Boiler" для повторного налаштування.

connectMQTT()

Підключається до брокера (goose.rmq2.cloudamqp.com), публікує online (LWT — offline при розриві), підписується на всі командні топіки (set/enable, set/temp, set/room_temp, pump/set, set/curve_mode) і одразу публікує поточні retained-стани (pump, curve_mode).

mqttCallback(topic, payload, length)

Єдина точка прийому команд від Delphi. За назвою топіка визначає, яке поле змінити:

Топік    Дія
home/boiler/set/enable    boiler.enabled = ...
home/boiler/set/temp    boiler.setTemp (перевірка діапазону 20–80°C)
home/boiler/set/room_temp    boiler.roomSetTemp (діапазон 5–30°C)
home/boiler/pump/set    setPump(msg == "1")
home/boiler/set/curve_mode    вмикає/вимикає режим «Крива опалення»
home/sensor/temp / temp_o    кеш кімнатної/зовнішньої температури (для кривої)
setPump(bool on)

Керує реле насоса: digitalWrite(RELAYPUMP, ...), зберігає стан у pumpState, публікує підтверджений статус у home/boiler/pump (retained).

otCycle()

Головний цикл обміну з котлом по OpenTherm (виконується раз на секунду):

Встановлює статус (CH увімкнено/вимкнено, чи є помилка, чи горить пальник).
Розраховує й надсилає уставку температури теплоносія — вручну (boiler.setTemp) або автоматично за кривою (computeHeatingCurve), якщо активний curveMode.
Надсилає уставку кімнатної температури.
Читає поточну температуру подачі й обратки.
Читає код і прапорці несправності (ASF-флаги).
computeHeatingCurve(desired, in, out)

Погодозалежний розрахунок температури теплоносія за квадратичною апроксимацією класичного «температурного графіка», з поправками на бажану кімнатну температуру та фактичне відхилення термостата.

readExtParamsStep()

Читає розширені параметри OpenTherm по одному за виклик (не всі одразу), щоб не блокувати loop() на кілька секунд: тиск у системі (ID18), температура вихлопу, зовнішня температура котла (ID27), рівень модуляції пальника (ID17), макс./мін. потужність (ID15).

publishState()

Публікує повний стан котла в MQTT:

окремі топіки (temp, temp_return, flame, fault_code, fault_flags) — для швидкого доступу;
один великий JSON у home/boiler/status — всі поля разом (enabled, temp, set_temp, room_set, pump, розширені OpenTherm- параметри, curve_mode, curve_calc_temp).
handleInterrupt()

Апаратний обробник переривання OpenTherm-бібліотеки (ICACHE_RAM_ATTR — виконується з RAM для швидкодії, обов'язково для переривань на ESP8266).

4. MQTT-топіки (підсумково)

ESP → Delphi (публікує, retained): home/boiler/status (JSON), home/boiler/temp, home/boiler/temp_return, home/boiler/flame, home/boiler/pump, home/boiler/curve_mode, home/boiler/online

Delphi → ESP (команди, підписка): home/boiler/set/enable, home/boiler/set/temp, home/boiler/set/room_temp, home/boiler/pump/set, home/boiler/set/curve_mode

прикладний код :
unit OTTypes;

{ ============================================================
  Типы и топики для OpenTherm MQTT проекта
  Delphi 13 Florence — FMX Android
  ============================================================ }

interface

uses
  System.SysUtils, System.Generics.Collections;

const


  TOPIC_SENSOR_TEMPDHT= 'home/sensor/temperature';
  TOPIC_SENSOR_HUM    = 'home/sensor/humidity';
  TOPIC_SENSOR_TEMPDHT_O= 'home/sensor/temperature_o';
  TOPIC_SENSOR_HUM_O    = 'home/sensor/humidity_o';

  TOPIC_SENSOR_TEMPDS = 'home/sensor/temperature_ds';
  TOPIC_SENSOR_STATUS = 'home/sensor/status';
  //Топики датчика газа MQ-2 ---
  TOPIC_SENSOR_GAS       = 'home/sensor/gas';       // сырое значение АЦП (0-1023)
  TOPIC_SENSOR_GAS_ALARM = 'home/sensor/gas_alarm'; // 0/1 - превышен порог

  // ---- Лампа (реле на 2-му ESP8266, тому ж, що і DHT) ----
  TOPIC_SENSOR_LAMP     = 'home/sensor/lamp';      // READ  — ESP публікує стан
  TOPIC_SENSOR_LAMP_SET = 'home/sensor/lamp/set';  // WRITE — Delphi надсилає команду
  // ---- Насос (реле на 2-му ESP8266, тому ж, що і DHT) ----
  TOPIC_SENSOR_PUMP     = 'home/sensor/pump';      // READ  — ESP публікує стан
  TOPIC_SENSOR_PUMP_SET=  'home/sensor/pump/set';  // WRITE — Delphi надсилає команду

  // ---- Режим (термостатopenTherm ESP8266, тому ж, що і ) ----
  TOPIC_SENSOR_MODEON     = 'home/sensor/modeon';      // READ  — ESP публікує стан
  TOPIC_SENSOR_MODEON_SET = 'home/sensor/modeon/set';  // WRITE — Delphi надсилає команду



  // ---- MQTT топики (READ — ESP публикует) ----
  TOPIC_STATUS      = 'home/boiler/status';
  TOPIC_TEMP        = 'home/boiler/temp';
  TOPIC_TEMP_RETURN = 'home/boiler/temp_return';
  TOPIC_FLAME       = 'home/boiler/flame';
  TOPIC_ONLINE      = 'home/boiler/online';
  TOPIC_FAULT_CODE  = 'home/boiler/fault_code';   // OEM fault code (0-255), напр. 38
  TOPIC_FAULT_FLAGS = 'home/boiler/fault_flags';  // біти ASF (сервіс/блок./тиск/полум'я/тяга/перегрів)
  TOPIC_BOILER_PUMP     =  'home/boiler/pump';      // READ  — ESP публікує стан
  TOPIC_BOILER_PUMP_SET =  'home/boiler/pump/set';  // WRITE — Delphi надсилає команду

  // ---- MQTT топики (WRITE — Delphi пишет) ----
  TOPIC_SET_ENABLE  = 'home/boiler/set/enable';
  TOPIC_SET_TEMP    = 'home/boiler/set/temp';
  TOPIC_SET_ROOM    = 'home/boiler/set/room_temp';

  // ---- Режим "Крива опалення" (погодозалежне регулювання) ----
  TOPIC_SET_CURVE_MODE = 'home/boiler/set/curve_mode'; // WRITE — 0/1
  TOPIC_CURVE_MODE     = 'home/boiler/curve_mode';     // READ  — поточний стан (retained)

  // ---- Лимиты ----
  TEMP_BOILER_MIN   = 20.0;
  TEMP_BOILER_MAX   = 80.0;
  TEMP_ROOM_MIN     = 5.0;
  TEMP_ROOM_MAX     = 30.0;
  // ---- Термостат кімнати (гістерезис) ----
  // Котел вимикається, коли кімнатна темп. (з DHT, ESP8266 №2) досягає
  // уставки (RoomSet), і вмикається знову, коли впаде на цю величину
  // нижче уставки. Без гістерезису котел клацав би вкл/викл щосекунди
  // на самій межі уставки.
  OnOffTempBoiler   = 1.5;

  // ---- График — максимум точек в истории ----
  HISTORY_MAX_POINTS = 60;  // 60 точек × 5 сек = 5 минут истории

type
  { Запись истории температуры }
  TTemperaturePoint = record
    Time  : TDateTime;
    Value : Double;
  end;

  { Полное состояние котла }
  TBoilerState = record
    Online      : Boolean;
    Enabled     : Boolean;
    Flame       : Boolean;
    CHActive    : Boolean;
    Fault       : Boolean;
    FaultCode   : Integer;  // OEM fault code (0-255) — те саме число, що котёл
                             // показує на дисплеї як Fxx (0 = помилки немає)
    FaultFlags  : Integer;  // байт ASF-флагів, див. ASFFlagsText()
    TempCurrent : Double;   // текущая темп. котла
    TempReturn  : Double;   // темп. обратки
    TempSet     : Double;   // уставка котла
    RoomSet     : Double;   // уставка комнаты
    pump        : Boolean;  // стан насос (реле на тому ж ESP8266, що насос)
    LastUpdate  : TDateTime;

    // ---- Додаткові OpenTherm Data-ID (читаються рідше, раз в ~10-15 сек) ----
    ChPressure    : Double;  // ID18  — тиск води в системі опалення, бар
    TExhaust      : Double;  // ID33  — температура вихлопних газів котла, °C
    TOutside      : Double;  // ID27  — зовнішня температура (датчик на котлі), °C
    HasOutsideSensor: Boolean; // чи котёл дав валідну відповідь на ID27
    RelModLevel   : Double;  // ID17  — відносний рівень модуляції пальника, %
    MaxTSetUB     : Double;  // ID50  — верхня межа кривої нагріву (uboundary), °C
    MaxTSetLB     : Double;  // ID50  — нижня межа кривої нагріву (lboundary), °C
    MaxCapacity   : Double;  // ID15  — максимальна потужність котла, кВт
    MinModLevel   : Double;  // ID15  — мінімальний рівень модуляції котла, %
    MaxTSetLimit  : Double;  // ID57  — максимальна установка температури котла, °C
    ExtParamsUpdate : TDateTime; // час останнього оновлення додаткових параметрів

    // ---- Режим "Крива опалення" (погодозалежне регулювання) ----
    CurveMode     : Boolean; // True = котёл сам рахує температуру теплоносія за кривою
    CurveCalcTemp : Double;  // розрахована за кривою температура контуру опалення, °C
    OutsideTempSource : string; // 'id27' | 'external' | 'none' | 'off' — реальне джерело зовн. темп.

    procedure Reset;
  end;

  { Полное состояние датчики та реле }
  TSensorRelayState = record
    temperature : Double;   // кімнатна темп. з DHT (окремий ESP8266 №1)
    Humidity    : Double;   // вологість з DHT
    temperature_o : Double;   // кімнатна темп. з DHT (окремий ESP8266 №1)
    Humidity_o    : Double;   // вологість з DHT
    Gas         : Double;   // газ
    temperature_ds : Double;   // ds18b20 температура
    SensorOnline: Boolean;  // онлайн-статус датчика (окремий від котла)
    LampOn      : Boolean;  // стан лампи (реле на тому ж ESP8266, що DHT)
    pumpOn      : Boolean;  // стан насос (реле на тому ж ESP8266, що насос)
    ModeOn      : Boolean;  // стан режим (реле на тому ж ESP8266, що режим насос)
    gas_alarm   : Boolean;
    LastUpdate  : TDateTime;
    procedure Reset;
  end;


  { Расписание — одна запись }
  TScheduleEntry = record
    DayOfWeek   : Integer;  // 1=Пн .. 7=Вс
    TimeStart   : TTime;
    TempTarget  : Double;
    Enabled     : Boolean;
  end;

  TScheduleList = TList<TScheduleEntry>;

{ Утилиты }

function ASFFlagsText(AFlags: Integer): string;
function FerroliFaultText(ACode: Integer): string;

implementation

uses
  System.JSON;

procedure TBoilerState.Reset;
begin
  Online      := False;
  Enabled     := False;
  Flame       := False;
  CHActive    := False;
  Fault       := False;
  pump        := False;
  FaultCode   := 0;
  FaultFlags  := 0;
  TempCurrent := 0;
  TempReturn  := 0;
  TempSet     := 60;
  RoomSet     := 21;
  LastUpdate  := 0;

  ChPressure      := 0;
  TExhaust        := 0;
  TOutside        := 0;
  HasOutsideSensor:= False;
  RelModLevel     := 0;
  MaxTSetUB       := 0;
  MaxTSetLB       := 0;
  MaxCapacity     := 0;
  MinModLevel     := 0;
  MaxTSetLimit    := 0;
  ExtParamsUpdate := 0;

  CurveMode     := False;
  CurveCalcTemp := 0;
  OutsideTempSource := 'off';
end;

procedure TSensorRelayState.Reset;
begin

end;

function TempToColor(ATemp: Double): Cardinal;
begin

end;

function FormatTemp(ATemp: Double): string;
begin
  Result := FormatFloat('0.0', ATemp);
end;

function BoilerStateFromJSON(const AJSON: string): TBoilerState;
var
  J: TJSONObject;
  V: TJSONValue;
begin
  Result.Reset;
  J := TJSONObject.ParseJSONValue(AJSON) as TJSONObject;
  if J = nil then Exit;
  try
    if J.TryGetValue('enabled',     V) then Result.Enabled     := V.GetValue<Boolean>;

    // ---- Додаткові OpenTherm параметри (ID 18,33,27,17,50,15,57) ----

    if J.TryGetValue('ext_params_valid', V) and V.GetValue<Boolean> then
      Result.ExtParamsUpdate := Now;

    // ---- Крива опалення ----
    if J.TryGetValue('curve_mode',      V) then Result.CurveMode     := V.GetValue<Boolean>;
    if J.TryGetValue('curve_calc_temp', V) then Result.CurveCalcTemp := V.GetValue<Double>;
    if J.TryGetValue('outside_temp_source', V) then Result.OutsideTempSource := V.GetValue<string>;

    Result.LastUpdate := Now;
  finally
    J.Free;
  end;
end;

function SensorRelayFromJSON(const AJSON: string): TSensorRelayState;
var
  J: TJSONObject;
  V: TJSONValue;
begin
  Result.Reset;
  J := TJSONObject.ParseJSONValue(AJSON) as TJSONObject;
  if J = nil then Exit;
  try
    if J.TryGetValue('temperature', V) then Result.temperature := V.GetValue<Double>;
    if J.TryGetValue('gas', V)         then Result.gas         := V.GetValue<Double>;
    if J.TryGetValue('temperature_ds', V)   then Result.temperature_ds:= V.GetValue<Double>;
    if J.TryGetValue('temperature_o', V) then Result.temperature_o := V.GetValue<Double>;
    if J.TryGetValue('status', V)      then Result.SensorOnline:= V.GetValue<Boolean>;
    if J.TryGetValue('lamp', V)        then Result.LampOn      := V.GetValue<Boolean>;
    if J.TryGetValue('pumpon', V)      then Result.pumpOn      := V.GetValue<Boolean>;
    if J.TryGetValue('modeon', V)      then Result.ModeOn      := V.GetValue<Boolean>;
    if J.TryGetValue('gas_alarm', V)   then Result.gas_alarm   := V.GetValue<Boolean>;
    Result.LastUpdate := Now;
  finally
    J.Free;
  end;
end;

// ------------------------------------------------------------------
// ASF flags (Data-ID 5, старший байт) — стандартні OpenTherm біти,
// однакові для будь-якого котла.
// ------------------------------------------------------------------
function ASFFlagsText(AFlags: Integer): string;
var Parts: TArray<string>;
begin
  SetLength(Parts, 0);
  if (AFlags and $01) <> 0 then Parts := Parts + ['потрібне сервісне обслуговування'];
  if (AFlags and $02) <> 0 then Parts := Parts + ['блокування (потрібен ручний скид)'];
  if (AFlags and $04) <> 0 then Parts := Parts + ['низький тиск води'];
  if (AFlags and $08) <> 0 then Parts := Parts + ['несправність полум''я/газу'];
  if (AFlags and $10) <> 0 then Parts := Parts + ['несправність тяги/повітря'];
  if (AFlags and $20) <> 0 then Parts := Parts + ['перегрів води'];
  if Length(Parts) = 0 then
    Result := ''
  else
    Result := string.Join(', ', Parts);
end;

// ------------------------------------------------------------------
// OEM fault code (Data-ID 5, молодший байт) → текст помилки Ferroli.
//
// !!! УВАГА: OpenTherm гарантує лише те, що котёл віддає своє власне
// (виробниче) число коду помилки в цьому байті — специфікація НЕ
// визначає, що це число обов'язково збігається з тим "Fxx", яке котёл
// друкує на дисплеї. У більшості виробників (в т.ч. зазвичай Ferroli)
// це число дійсно дорівнює номеру з дисплея (тобто код 38 у цьому
// байті = "F38" на панелі), але це потрібно ОБОВ'ЯЗКОВО перевірити
// на своєму котлі: спровокуйте (чи дочекайтесь) відому помилку і
// звірте faultCode із MQTT з тим, що показує панель котла.
//
// Таблицю нижче потрібно доповнити з сервісного мануалу саме на
// Ferroli Bluesens 4 (розділ "Codici di errore" / "Fault codes") —
// нижче лише каркас із кількома типовими для газових котлів кодами.
// ------------------------------------------------------------------
function FerroliFaultText(ACode: Integer): string;
begin
  case ACode of
    0:  Result := 'Помилок немає';
    04: Result := 'Проблемы с дымоходом Детальніше';
    05: Result := 'Неисправность вентилятор Детальніше';
    08: Result := 'Перегрев теплообменника Детальніше';
    10: Result := 'Несправність датчика температури подачі (NTC)';
    11: Result := 'Неисправность датчика температуры ГВС';
    12: Result := 'Несправність датчика тиску води / пресостата';
    13: Result := 'Недостатній тиск повітря (несправність димовідведення)';
    14: Result := 'Неисправность датчика температуры на выходе ГВС';
    20: Result := 'Несправність датчика зворотної води (NTC)';
    34: Result := 'Низкое напряжение';
    35: Result := 'Паразитне полум''я / несправність контролю полум''я';
    37: Result := 'Низкое давление в системе отопления';
    38: Result := 'Втрата полум''я під час роботи';
    39: Result := 'Потеря связи с выносной метеостанцией';
    40: Result := 'Низький тиск води в системі опалення';
    41: Result := 'Перегрів теплоносія / спрацював запобіжний термостат';
  else
    Result := Format('Код помилки %d — див. сервісний мануал Ferroli Bluesens', [ACode]);
  end;
end;

end.

Підвал форуму