From 0c06ba1032cbf77c22e77e8044107b16da127842 Mon Sep 17 00:00:00 2001 From: Vitali Date: Sat, 8 Aug 2026 01:33:48 +0700 Subject: [PATCH] =?UTF-8?q?feat(webhook):=20=D1=81=D0=BB=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B2=20status=20=D1=87=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D1=83=D1=8E=20=D1=84=D1=80=D0=B0=D0=B7=D1=83?= =?UTF-8?q?=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В теле уведомления status теперь содержит ту же формулировку, что видит оператор в панели: «Насосы стоят, подачу держит резерв». Прежние машиночитаемые значения переехали в новое поле state (ALARM / OK / STARTED / STOPPED), чтобы сценарий на сервере ветвился по нему, а не разбирал русский текст. ЛОМАЮЩЕЕ ИЗМЕНЕНИЕ: если сценарий в n8n сравнивал status с "ALARM", условие надо переключить на state. Формулировки собраны в одном месте прошивки (statusKind / statusWord / statusSub / statusPhrase) и оттуда попадают и в webhook, и в панель через новые поля k, w, s в /status. Раньше та же логика была продублирована в JS панели; два набора фраз разошлись бы при первой правке. Побочно JS сократился почти вдвое. Прошито и проверено на приборе: /status отдаёт {"k":"ok","w":"Подача есть","s":"работает насос 1"}, /config по-прежнему отдаёт только имя хоста без токена. Co-Authored-By: Claude Opus 5 --- pump_controller_8_2_OLED_DONE/MANUAL.md | 16 +++-- pump_controller_8_2_OLED_DONE/README.md | 38 +++++++++--- .../pump_controller_8_2_OLED_DONE.ino | 60 +++++++++++++++++-- pump_controller_8_2_OLED_DONE/web_page.h | 18 ++---- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/pump_controller_8_2_OLED_DONE/MANUAL.md b/pump_controller_8_2_OLED_DONE/MANUAL.md index 1fe28d1..248ec71 100644 --- a/pump_controller_8_2_OLED_DONE/MANUAL.md +++ b/pump_controller_8_2_OLED_DONE/MANUAL.md @@ -217,12 +217,18 @@ ALARM:OFF BACKUP:OFF При изменении ситуации прибор отправляет сообщение на настроенный адрес: -| Событие | Что означает | +В сообщении приходит **та же фраза, что показывает панель** — читать можно +без расшифровки кодов: + +| Ситуация | Текст в сообщении | |---|---| -| `MAIN PUMPS` / `ALARM` | Оба основных насоса стоят | -| `MAIN PUMPS` / `OK` | Работа восстановлена | -| `EMERGENCY PUMP` / `STARTED` | Запустился резервный насос | -| `EMERGENCY PUMP` / `STOPPED` | Резервный насос остановился | +| Оба основных стоят, резерв качает | «Насосы стоят, подачу держит резерв» | +| Оба основных стоят, резерва нет | «Насосы стоят, подачи нет» | +| Работает резерв, основные в порядке | «Работает резерв, основные насосы в работе» | +| Работа восстановлена | «Подача есть, работает насос 1» | + +Рядом с текстом передаётся служебное поле для автоматики: `MAIN PUMPS` или +`EMERGENCY PUMP` и `ALARM` / `OK` / `STARTED` / `STOPPED`. Уведомления отправляются один раз на каждое изменение, а не повторяются. Если в момент события связи не было — уведомление **не будет** отправлено diff --git a/pump_controller_8_2_OLED_DONE/README.md b/pump_controller_8_2_OLED_DONE/README.md index 71de6c0..789c1d2 100644 --- a/pump_controller_8_2_OLED_DONE/README.md +++ b/pump_controller_8_2_OLED_DONE/README.md @@ -214,18 +214,37 @@ pE = стоит -> реле «Резерв» выключено `POST` на `settings.webhookUrl`, `Content-Type: application/json`: ```json -{"event":"MAIN PUMPS","status":"ALARM","device":"Wemos_D1_Pump"} +{"event":"MAIN PUMPS","state":"ALARM", + "status":"Насосы стоят, подачу держит резерв","device":"Pumps"} ``` -| `event` | `status` | +| Поле | Назначение | |---|---| -| `MAIN PUMPS` | `ALARM` / `OK` | -| `EMERGENCY PUMP` | `STARTED` / `STOPPED` | +| `event` | Канал: `MAIN PUMPS` или `EMERGENCY PUMP` | +| `state` | Машиночитаемое: `ALARM` / `OK` / `STARTED` / `STOPPED` | +| `status` | **Человеческая фраза**, та же что в панели | +| `device` | Имя прибора из настроек | -Поле `device` берётся из настройки `settings.deviceName` (задаётся через веб-панель, -по умолчанию `Wemos_D1_Pump`). Значение отфильтровано при вводе до -`[A-Za-z0-9_-]`, поэтому экранирование в JSON не требуется — сломать тело -запроса ему нечем. +Фраза в `status` собирается функцией `statusPhrase()` из текущего состояния на +момент отправки — это те же слова, что видит оператор в панели: + +| Ситуация | `status` | +|---|---| +| Оба основных стоят, резерв качает | `Насосы стоят, подачу держит резерв` | +| Оба основных стоят, резерва нет | `Насосы стоят, подачи нет` | +| Работает резерв, основные в порядке | `Работает резерв, основные насосы в работе` | +| Норма | `Подача есть, работает насос 1` | + +`state` оставлен рядом намеренно: сценарий на сервере должен ветвиться по нему, +а не разбирать русский текст. **Если сценарий раньше сравнивал `status` с +`"ALARM"`, его нужно переключить на `state`** — это ломающее изменение. + +Формулировки живут в одном месте, в прошивке. Панель получает их готовыми через +`/status` (поля `k`, `w`, `s`) и не собирает сама — иначе два набора фраз +разошлись бы при первой правке. + +Фразы не содержат кавычек и обратных слэшей, `device` отфильтрован при вводе, +поэтому экранирование в JSON не требуется нигде. TLS-соединение поднимается через `WiFiClientSecure` с `setInsecure()` — сертификат сервера **не проверяется**. Достаточно для отправки в доверенную @@ -417,7 +436,8 @@ Wi-Fi настройки из `/settings.json` не затрагивает, и Ответы: ```json -/status {"p1":1,"p2":0,"pE":0,"rMain":0,"rEmg":0,"rdy":1} +/status {"p1":1,"p2":0,"pE":0,"rMain":0,"rEmg":0,"rdy":1, + "k":"ok","w":"Подача есть","s":"работает насос 1"} /config {"device":"Nasosnaya_1","alarm":5,"emerg":0, "host":"n8n.example.com","fs":1,"ip":"192.168.1.128"} ``` diff --git a/pump_controller_8_2_OLED_DONE/pump_controller_8_2_OLED_DONE.ino b/pump_controller_8_2_OLED_DONE/pump_controller_8_2_OLED_DONE.ino index ba581bc..b780ed1 100644 --- a/pump_controller_8_2_OLED_DONE/pump_controller_8_2_OLED_DONE.ino +++ b/pump_controller_8_2_OLED_DONE/pump_controller_8_2_OLED_DONE.ino @@ -410,7 +410,7 @@ static bool parseWebhookHost(const char* url, String &host) { return true; } -void sendPostWebhook(String eventName, String status) { +void sendPostWebhook(String eventName, String state) { if (settings.webhookUrl[0] == '\0') { Serial.println(F("[HTTP] Webhook URL не задан — отправка пропущена")); return; @@ -467,9 +467,14 @@ void sendPostWebhook(String eventName, String status) { } http.addHeader("Content-Type", "application/json"); - // settings.deviceName отфильтрован при вводе до [A-Za-z0-9_-], поэтому экранирование - // в JSON не требуется — сломать тело запроса ему нечем. - String body = "{\"event\":\"" + eventName + "\",\"status\":\"" + status + + // status — человеческая фраза, та же что в панели: «Насосы стоят, подачу + // держит резерв». state оставлен машиночитаемым (ALARM / OK / STARTED / + // STOPPED), чтобы сценарий на сервере ветвился по нему, а не разбирал текст. + // settings.deviceName отфильтрован при вводе до [A-Za-z0-9_-], фразы кавычек + // и обратных слэшей не содержат — экранирование в JSON нигде не нужно. + String body = "{\"event\":\"" + eventName + + "\",\"state\":\"" + state + + "\",\"status\":\"" + statusPhrase() + "\",\"device\":\"" + String(settings.deviceName) + "\"}"; int code = http.POST(body); @@ -531,6 +536,46 @@ void updatePumpState(Pump &p) { p.lastReading = currentReading; } +// ============================================================ +// Словесное описание состояния +// ============================================================ +// Единственное место, где живёт формулировка. Её берут и веб-панель (через +// /status), и тело webhook — иначе два набора фраз разошлись бы при первой +// же правке. Фразы не содержат кавычек и обратных слэшей, поэтому безопасно +// вставляются в JSON без экранирования. + +// Вид состояния: им панель выбирает цвет плашки. +static const char* statusKind() { + if (!systemReady) return "init"; + if (!p1.stableState && !p2.stableState) return "flt"; + if (pE.stableState) return "warn"; + return "ok"; +} + +// Главное слово — крупная строка в панели и начало фразы в уведомлении. +static const __FlashStringHelper* statusWord() { + if (!systemReady) return F("Запуск"); + if (!p1.stableState && !p2.stableState) return F("Насосы стоят"); + if (pE.stableState) return F("Работает резерв"); + return F("Подача есть"); +} + +// Уточнение — вторая строка в панели и хвост фразы в уведомлении. +static const __FlashStringHelper* statusSub() { + if (!systemReady) return F("идёт проверка датчиков"); + if (!p1.stableState && !p2.stableState) { + return pE.stableState ? F("подачу держит резерв") : F("подачи нет"); + } + if (pE.stableState) return F("основные насосы в работе"); + if (p1.stableState && p2.stableState) return F("работают оба насоса"); + return p1.stableState ? F("работает насос 1") : F("работает насос 2"); +} + +// Готовая фраза целиком: «Насосы стоят, подачу держит резерв». +static String statusPhrase() { + return String(statusWord()) + ", " + String(statusSub()); +} + // ============================================================ // ВЕБ-ПАНЕЛЬ // ============================================================ @@ -630,7 +675,12 @@ void setup() { json += "\"pE\":" + String(pE.stableState ? "1" : "0") + ","; json += "\"rMain\":" + String(relayMainOn ? "1" : "0") + ","; json += "\"rEmg\":" + String(relayEmergOn ? "1" : "0") + ","; - json += "\"rdy\":" + String(systemReady ? "1" : "0"); + json += "\"rdy\":" + String(systemReady ? "1" : "0") + ","; + // Формулировки приходят готовыми, а не собираются в браузере: та же + // фраза уходит в webhook, и второй набор строк в JS неизбежно бы разошёлся. + json += "\"k\":\"" + String(statusKind()) + "\","; + json += "\"w\":\"" + String(statusWord()) + "\","; + json += "\"s\":\"" + String(statusSub()) + "\""; json += "}"; server.send(200, "application/json", json); }); diff --git a/pump_controller_8_2_OLED_DONE/web_page.h b/pump_controller_8_2_OLED_DONE/web_page.h index 41d5995..5533781 100644 --- a/pump_controller_8_2_OLED_DONE/web_page.h +++ b/pump_controller_8_2_OLED_DONE/web_page.h @@ -128,19 +128,11 @@ lamp("l2",d.p2,d.p2?"работает":"стоит"); lamp("lE",d.pE,d.pE?"работает":"ожидание"); lamp("o1",d.rMain,d.rMain?"замкнут":"разомкнут"); lamp("o2",d.rEmg,d.rEmg?"замкнут":"разомкнут"); -var k,w,s; -if(!d.rdy){k="init";w="Запуск";s="идёт проверка датчиков"} -else if(!d.p1&&!d.p2){k="flt";w="Насосы стоят"; -s=d.pE?"подачу держит резерв":"подачи нет"} -else if(d.pE){k="warn";w="Работает резерв";s="основные насосы в работе"} -else{k="ok";w="Подача есть"; -s=d.p1&&d.p2?"работают оба насоса":(d.p1?"работает насос 1":"работает насос 2")} -var st=document.getElementById("st"); -st.className="st "+k; -document.getElementById("word").textContent=w; -document.getElementById("sub").textContent=s; -document.title=w+(dev?" · "+dev:""); -fav(F[k])} +document.getElementById("st").className="st "+d.k; +document.getElementById("word").textContent=d.w; +document.getElementById("sub").textContent=d.s; +document.title=d.w+(dev?" · "+dev:""); +fav(F[d.k])} function poll(){fetch("/status").then(function(r){return r.json()}).then(draw).catch(function(){})} fetch("/config").then(function(r){return r.json()}).then(function(c){ dev=c.device;document.getElementById("dev").textContent=c.device;