feat(webhook): слать в status человеческую фразу вместо кода
В теле уведомления 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
||||||
|
|
||||||
Уведомления отправляются один раз на каждое изменение, а не повторяются.
|
Уведомления отправляются один раз на каждое изменение, а не повторяются.
|
||||||
Если в момент события связи не было — уведомление **не будет** отправлено
|
Если в момент события связи не было — уведомление **не будет** отправлено
|
||||||
|
|||||||
@@ -214,18 +214,37 @@ pE = стоит -> реле «Резерв» выключено
|
|||||||
`POST` на `settings.webhookUrl`, `Content-Type: application/json`:
|
`POST` на `settings.webhookUrl`, `Content-Type: application/json`:
|
||||||
|
|
||||||
```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` |
|
| `event` | Канал: `MAIN PUMPS` или `EMERGENCY PUMP` |
|
||||||
| `EMERGENCY PUMP` | `STARTED` / `STOPPED` |
|
| `state` | Машиночитаемое: `ALARM` / `OK` / `STARTED` / `STOPPED` |
|
||||||
|
| `status` | **Человеческая фраза**, та же что в панели |
|
||||||
|
| `device` | Имя прибора из настроек |
|
||||||
|
|
||||||
Поле `device` берётся из настройки `settings.deviceName` (задаётся через веб-панель,
|
Фраза в `status` собирается функцией `statusPhrase()` из текущего состояния на
|
||||||
по умолчанию `Wemos_D1_Pump`). Значение отфильтровано при вводе до
|
момент отправки — это те же слова, что видит оператор в панели:
|
||||||
`[A-Za-z0-9_-]`, поэтому экранирование в JSON не требуется — сломать тело
|
|
||||||
запроса ему нечем.
|
| Ситуация | `status` |
|
||||||
|
|---|---|
|
||||||
|
| Оба основных стоят, резерв качает | `Насосы стоят, подачу держит резерв` |
|
||||||
|
| Оба основных стоят, резерва нет | `Насосы стоят, подачи нет` |
|
||||||
|
| Работает резерв, основные в порядке | `Работает резерв, основные насосы в работе` |
|
||||||
|
| Норма | `Подача есть, работает насос 1` |
|
||||||
|
|
||||||
|
`state` оставлен рядом намеренно: сценарий на сервере должен ветвиться по нему,
|
||||||
|
а не разбирать русский текст. **Если сценарий раньше сравнивал `status` с
|
||||||
|
`"ALARM"`, его нужно переключить на `state`** — это ломающее изменение.
|
||||||
|
|
||||||
|
Формулировки живут в одном месте, в прошивке. Панель получает их готовыми через
|
||||||
|
`/status` (поля `k`, `w`, `s`) и не собирает сама — иначе два набора фраз
|
||||||
|
разошлись бы при первой правке.
|
||||||
|
|
||||||
|
Фразы не содержат кавычек и обратных слэшей, `device` отфильтрован при вводе,
|
||||||
|
поэтому экранирование в JSON не требуется нигде.
|
||||||
|
|
||||||
TLS-соединение поднимается через `WiFiClientSecure` с `setInsecure()` —
|
TLS-соединение поднимается через `WiFiClientSecure` с `setInsecure()` —
|
||||||
сертификат сервера **не проверяется**. Достаточно для отправки в доверенную
|
сертификат сервера **не проверяется**. Достаточно для отправки в доверенную
|
||||||
@@ -417,7 +436,8 @@ Wi-Fi настройки из `/settings.json` не затрагивает, и
|
|||||||
Ответы:
|
Ответы:
|
||||||
|
|
||||||
```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,
|
/config {"device":"Nasosnaya_1","alarm":5,"emerg":0,
|
||||||
"host":"n8n.example.com","fs":1,"ip":"192.168.1.128"}
|
"host":"n8n.example.com","fs":1,"ip":"192.168.1.128"}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ static bool parseWebhookHost(const char* url, String &host) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendPostWebhook(String eventName, String status) {
|
void sendPostWebhook(String eventName, String state) {
|
||||||
if (settings.webhookUrl[0] == '\0') {
|
if (settings.webhookUrl[0] == '\0') {
|
||||||
Serial.println(F("[HTTP] Webhook URL не задан — отправка пропущена"));
|
Serial.println(F("[HTTP] Webhook URL не задан — отправка пропущена"));
|
||||||
return;
|
return;
|
||||||
@@ -467,9 +467,14 @@ void sendPostWebhook(String eventName, String status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.addHeader("Content-Type", "application/json");
|
http.addHeader("Content-Type", "application/json");
|
||||||
// settings.deviceName отфильтрован при вводе до [A-Za-z0-9_-], поэтому экранирование
|
// status — человеческая фраза, та же что в панели: «Насосы стоят, подачу
|
||||||
// в JSON не требуется — сломать тело запроса ему нечем.
|
// держит резерв». state оставлен машиночитаемым (ALARM / OK / STARTED /
|
||||||
String body = "{\"event\":\"" + eventName + "\",\"status\":\"" + status +
|
// STOPPED), чтобы сценарий на сервере ветвился по нему, а не разбирал текст.
|
||||||
|
// settings.deviceName отфильтрован при вводе до [A-Za-z0-9_-], фразы кавычек
|
||||||
|
// и обратных слэшей не содержат — экранирование в JSON нигде не нужно.
|
||||||
|
String body = "{\"event\":\"" + eventName +
|
||||||
|
"\",\"state\":\"" + state +
|
||||||
|
"\",\"status\":\"" + statusPhrase() +
|
||||||
"\",\"device\":\"" + String(settings.deviceName) + "\"}";
|
"\",\"device\":\"" + String(settings.deviceName) + "\"}";
|
||||||
int code = http.POST(body);
|
int code = http.POST(body);
|
||||||
|
|
||||||
@@ -531,6 +536,46 @@ void updatePumpState(Pump &p) {
|
|||||||
p.lastReading = currentReading;
|
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 += "\"pE\":" + String(pE.stableState ? "1" : "0") + ",";
|
||||||
json += "\"rMain\":" + String(relayMainOn ? "1" : "0") + ",";
|
json += "\"rMain\":" + String(relayMainOn ? "1" : "0") + ",";
|
||||||
json += "\"rEmg\":" + String(relayEmergOn ? "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 += "}";
|
json += "}";
|
||||||
server.send(200, "application/json", json);
|
server.send(200, "application/json", json);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -128,19 +128,11 @@ lamp("l2",d.p2,d.p2?"работает":"стоит");
|
|||||||
lamp("lE",d.pE,d.pE?"работает":"ожидание");
|
lamp("lE",d.pE,d.pE?"работает":"ожидание");
|
||||||
lamp("o1",d.rMain,d.rMain?"замкнут":"разомкнут");
|
lamp("o1",d.rMain,d.rMain?"замкнут":"разомкнут");
|
||||||
lamp("o2",d.rEmg,d.rEmg?"замкнут":"разомкнут");
|
lamp("o2",d.rEmg,d.rEmg?"замкнут":"разомкнут");
|
||||||
var k,w,s;
|
document.getElementById("st").className="st "+d.k;
|
||||||
if(!d.rdy){k="init";w="Запуск";s="идёт проверка датчиков"}
|
document.getElementById("word").textContent=d.w;
|
||||||
else if(!d.p1&&!d.p2){k="flt";w="Насосы стоят";
|
document.getElementById("sub").textContent=d.s;
|
||||||
s=d.pE?"подачу держит резерв":"подачи нет"}
|
document.title=d.w+(dev?" · "+dev:"");
|
||||||
else if(d.pE){k="warn";w="Работает резерв";s="основные насосы в работе"}
|
fav(F[d.k])}
|
||||||
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])}
|
|
||||||
function poll(){fetch("/status").then(function(r){return r.json()}).then(draw).catch(function(){})}
|
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){
|
fetch("/config").then(function(r){return r.json()}).then(function(c){
|
||||||
dev=c.device;document.getElementById("dev").textContent=c.device;
|
dev=c.device;document.getElementById("dev").textContent=c.device;
|
||||||
|
|||||||
Reference in New Issue
Block a user