From 04d709a1e8ab9c4a9db5c34ce4d634ead5c8e5f1 Mon Sep 17 00:00:00 2001 From: "anklimov@gmail.com" Date: Fri, 1 May 2026 19:57:51 +0300 Subject: [PATCH] MultiAC small fix + SPRINKLER chanell type (initial, unverivied) --- documentation/Sprinkler_module.md | 181 ++++++++ lighthub/item.cpp | 8 + lighthub/item.h | 3 +- lighthub/main.cpp | 6 + lighthub/modules/out_multivent.cpp | 4 +- lighthub/modules/out_sprinkler.cpp | 702 +++++++++++++++++++++++++++++ lighthub/modules/out_sprinkler.h | 59 +++ 7 files changed, 960 insertions(+), 3 deletions(-) create mode 100644 documentation/Sprinkler_module.md create mode 100644 lighthub/modules/out_sprinkler.cpp create mode 100644 lighthub/modules/out_sprinkler.h diff --git a/documentation/Sprinkler_module.md b/documentation/Sprinkler_module.md new file mode 100644 index 0000000..fad002e --- /dev/null +++ b/documentation/Sprinkler_module.md @@ -0,0 +1,181 @@ +# Данный модуль реализует многозональную систему полива + +## Система состоит из следующих компонент: + +* Накопительный водяной бак. Снабжен двумя поплавками. Максимум воды и минимум воды. Заведены на входы wMax и wMin +* Насос полива высокого давления. Запитан из бака. Включается реле, подключенным к выходу rPump. Датчик тока для контроля того, что насос включен, заведен на вход fbPump +* Набор клапанов зон полива. Подключены через оптореле к выходам, заданным в параметре pin соответствующей зоны полива. +* Клапан налива из водопровода. Подключен через оптореле к выходу vIn +* Насос дренажного колодца. Приоритетный источник для наполнения бака полива. Когда система полива находится в ждущем или активном режиме, бак пытается максимально наполнится из дренажного колодца. Насос дренажа имеет поплавковый выключатель, отключающий насос при осушении дренажного колодца. Насос включается реле, которое подключено к выходу rDren. Для контроля того, что насос включен и момента осушения колодца, используется датчик тока, который подключен в входу fbDren +* Опциональный водосчетчик. Контакты подключены к входу wCtr + +## Система налива воды реализована при помощи конечного автомата со следующими состояниями: + +* SP\_UNKNOWN +* SP\_OFF +* SP\_DREN\_ON - дренажный насос включен +* SP\_DREN\_OPERATE - дренажный насос работает +* SP\_DREN\_EMPTY - дренажный насос выключился встроенным поплавком - колодец пуст +* SP\_VIN - включено наполнение из водопровода +* SP\_FULL - бак наполнен + +граф переходов конечного автомата системы налива воды + +Состояние SP\_* | Условие перехода | перейти в состояние | выполнить при переходе | +|------|--------|-------|---------| +INIT | true | OFF | выключить клапана и насосы | +OFF | vMax && !FREEZE | FULL | выключить vIN, rDren| +OFF | ! vMax && !FREEZE | DREN\_ON|включить rDren| +DREN\_ON|fbDren (насос дренажа реально работает)|DREN\_OPERATE| +DREN\_ON|таймаут 10 сек|DREN\_EMPTY| +DREN\_EMPTY|включен цикл полива и бак не полон|VIN|включить клапан vIN для набора бака из водопровода| +DREN\_OPERATE|fbDren (насос дренажа более не работает)|DREN\_EMPTY|| +VIN|fbDren|DREN\_OPERATE|вылючить клапан vIN для набора бака из водопровода| +VIN, DREN\_OPERATE|vMax|FULL | выключить vIN, rDren| +VIN|таймаут 1200 сек | FAULT\_VIN| выключить vIN| +DREN\_OPERATE|таймаут 1200 сек |FAULT_DREN|| + + + +## Конфигурирование: + + +``` + +"items": +{ +"sprinkler":[23, + + { + "":{ + "vIn:7, + "wMax":15, + "wMin:17, + "rDren":6, + "fbDren":12, + "rPump":5, + "fbPump":11, + "wCtr":19 + }, + "garden":{"pin":13,"set":60,"val":15,"cmd":2}, + "backyard":{"pin":14,"set":60,"val":15,"cmd":2}, + "trees":{"pin":15,"set":60,"val":15,"cmd":2} + + }] +} + +``` + +## Алгоритм работы +в настройки зон полива задаем интенсивность для каждой зоны. + +Это можно сделать как в конфиге так динамически, (стандартными механизмами управления по MQTT, HTTP, CAN) + +Рассмотрим на примере MQTT: + +**топик** ```root/name/sprinkler/garden/set -> 60``` + +Задаем обьем полива 60 отсчетов счетчика воды (если счетчик не сконфигурирован - 60 секунд) + +Контроллер должен передать это значение в выходной топик ```root/name/s_out/sprinkrer/garden/set``` и оно будет восстановлено при перезагрузке контроллера + +отработанный обьем воды или время будет сохраняться в параметре "val" каждой зоны (параметр будет автоматически увеличиваться при работе зоны, передаваться в соответствующий зоне топик для мониторинга и восстановления в случае перезагрузки контроллера) + +**Пример топика:** ```root/s_out/sprinkler/garden/val``` + +Когда данный параметр достигнет значения, заданного в параметре "set" контроллер завершит полив данной зоны и перейдет к следующей. + +Для сброса счетчиков можно использовать как непосредственную установку значения параметра "val" для каждой зоны так и команду RESET, отправленную в нужную зону или в объект sprinkler через суффикс /cmd. + +В последнем случае, контроллер итерационно сбросит счетчики в значение 0 для каждой зоны полива. + +**Пример:** ```root/name/sprinkler/cmd -> RESET``` + + +## Управление + +### Включение/выключение полива конкретной зоны: + +**Включить** ```root/name/sprinkler/garden/cmd -> ON``` + +**Выключить** ```root/name/sprinkler/garden/cmd -> OFF``` + + + +### Включение/выключение цикла полива: + +**Включить** ```root/name/sprinkler/cmd -> ON``` +Система начнет или продолжит цикл полива, переходя от зоны к зоне по мере завершения работы с каждой предыдущей зоной. После завершения работы со всеми зонами, sprinkler перейдет в состояние OFF + +Перед включением полива, система убедится что бак наполнен или до-наполнит его до максимума из водопровода. + + + +**Выключить** ```root/name/sprinkler/cmd -> OFF``` +Система немедленно остановит текущий цикл полива (закроет клапаны зон, выключит насос полива) + + +Аналогично, будут работать команды XON и XOFF, с одним исключением, что команда XON может быть запрещена и игнорироваться если активирован режим DISABLE. Это базовая функция контроллера и не относится к функционалу данного модуля. Но может быть использована, например, для запрета полива на определенное время после выпадения осадков + +**Пример** + +``` +root/name/sprinkler/ctrl -> DISABLE +root/name/sprinkler/cmd -> XON //Будет проигнорировано + +root/name/sprinkler/ctrl -> ENABLE +root/name/sprinkler/cmd -> XON //А вот теперь сработает + +``` + +Даже в выключенном состоянии (OFF) , система полива работает в дежурном режиме, поддерживая максимальный уровень воды в баке за счет немедленной перекачки из дренажного колодца + +При попытке включения системы после завершения дневного задания по поливу всех зон (параметр val для всех зон достиг параметра set), система сразу перейдет в состояние OFF + + + +### Полная блокировка системы полива (в зимнее время) +Ддя перевода канала полива в полностью заблокированное состояние и обратно импользуется системная команда FREEZE/UNFREEZE соответственно + +В режиме FREEZE полностью заблокирована обработка всех команд, кроме UNFREEZE, заблокирован автомат пополнения бака из дренажного насоса и выключены насосы и все клапана + +Рекомендуется задать флаг FREEZE в конфигурации канала (см документ ...) , чтобы избежать разблокировки при утере значений топика /clrl и перезагрузки системы + +Также, на вход /val обЪекта sprinkler можно подать значение уличной температуры. И если значения будут ниже нуля, система автоматически перейдет в режим FREEZE + +**Пример** + +``` +root/name/sprinkler/ctrl -> FREEZE +root/name/sprinkrer/cmd -> ON //Будет проигнорировано + +root/name/sprinkler/ctrl -> UNFREEZE +root/name/sprinkrer/cmd -> ON //А вот теперь сработает + +root/name/sprinkler/val -> -1 //система перейдет в режим FREEZE + + +``` + +### Передача статусных значений + + +**Примеры выдачи в топики:** + +``` +root/s_out/sprinkler/$fbPump - ON/OFF признак того, что включен основной насос (от датчика тока) +root/s_out/sprinkler/$fbDren - ON/OFF признак того, что включен дренажный насос (от датчика тока) +root/s_out/sprinkler/$state - состояние конечного автомата Системы налива воды +root/s_out/sprinkler/$wMax - ON/OFF достигнут максимум воды в баке (от поплавкового датчика) +root/s_out/sprinkler/$wMin - ON/OFF достигнут минимум воды в баке (от поплавкового датчика) +root/s_out/sprinkler/$rDren - ON/OFF включено реле дренажного насоса +root/s_out/sprinkler/$rPump - ON/OFF включено реле основного насоса +root/s_out/sprinkler/set - значение счетчика воды (восстанавливается при перезагрузке из данного топика) +root/s_out/sprinkler/$vIN - ON/OFF - признак открытия клапана налива бака из водопровода + +root/s_out/sprinkler/garden/set - требуемый обьем (или время) полива зоны +root/s_out/sprinkler/garden/cmd - ON или OFF - признак включения полива зоны +root/s_out/sprinkler/garden/$state - ON или OFF - признак того что зона поливается в настоящее время +root/s_out/sprinkler/garden/val - текущее время или обьем полива данной зоны + +``` \ No newline at end of file diff --git a/lighthub/item.cpp b/lighthub/item.cpp index 5f7bfb1..62d0e9b 100644 --- a/lighthub/item.cpp +++ b/lighthub/item.cpp @@ -68,6 +68,8 @@ e-mail anklimov@gmail.com #include "modules/out_humidifier.h" #endif +#include "modules/out_sprinkler.h" + #ifdef CANDRV #include extern canDriver LHCAN; @@ -2886,6 +2888,12 @@ switch (itemType) break; #endif +#ifdef SPRINKLER_ENABLE + case CH_SPRINKLER: + driver = new out_sprinkler ; + break; +#endif + #ifndef COUNTER_DISABLE case CH_COUNTER: driver = new out_counter ; diff --git a/lighthub/item.h b/lighthub/item.h index 816f22d..1d131b4 100644 --- a/lighthub/item.h +++ b/lighthub/item.h @@ -68,7 +68,8 @@ const suffixstr suffix_P[] PROGMEM = #define CH_COUNTER 20 #define CH_HUMIDIFIER 21 #define CH_MERCURY 22 -#define CH_MAX 22 +#define CH_SPRINKLER 23 +#define CH_MAX 23 #define POLLING_SLOW 1 #define POLLING_FAST 2 diff --git a/lighthub/main.cpp b/lighthub/main.cpp index 977079e..e06c48b 100644 --- a/lighthub/main.cpp +++ b/lighthub/main.cpp @@ -2717,6 +2717,12 @@ infoSerial<0) sendACcmd(itemCmd().Cmd(CMD_AUTO)); + else if (ventRequested && lastACfan>0) sendACcmd(itemCmd().Cmd(CMD_FAN)); else noFurtherModes = true; //No AUTO or FAN mode requested - so we can skip sending command to AC at all and save some energy on it } diff --git a/lighthub/modules/out_sprinkler.cpp b/lighthub/modules/out_sprinkler.cpp new file mode 100644 index 0000000..6c79cca --- /dev/null +++ b/lighthub/modules/out_sprinkler.cpp @@ -0,0 +1,702 @@ +#ifdef SPRINKLER_ENABLE + +#include "modules/out_sprinkler.h" +#include "Arduino.h" +#include "options.h" +#include "Streaming.h" + +#include "item.h" +#include "main.h" +#include "utils.h" + +bool out_sprinkler::getConfig() +{ + gatesObj = NULL; + vinPin = drenPin = pumpPin = -1; + wMaxPin = wMinPin = fbDrenPin = fbPumpPin = wCtrPin = -1; + lastWctrState = false; + + if (!item || !item->itemArg) return false; + + aJsonObject * arg = item->itemArg; + if (arg->type == aJson_Array && aJson.getArraySize(arg) > 1) + { + aJsonObject * second = aJson.getArrayItem(arg, 1); + if (second && second->type == aJson_Object) gatesObj = second; + } + else if (arg->type == aJson_Object) + { + gatesObj = arg; + } + + if (!gatesObj) return false; + + aJsonObject * rootCfg = aJson.getObjectItem(gatesObj, ""); + if (!rootCfg) rootCfg = gatesObj; + + vinPin = getIntFromJson(rootCfg, "vIn", -1); + drenPin = getIntFromJson(rootCfg, "rDren", -1); + pumpPin = getIntFromJson(rootCfg, "rPump", -1); + wMaxPin = getIntFromJson(rootCfg, "wMax", -1); + wMinPin = getIntFromJson(rootCfg, "wMin", -1); + fbDrenPin = getIntFromJson(rootCfg, "fbDren", -1); + fbPumpPin = getIntFromJson(rootCfg, "fbPump", -1); + wCtrPin = getIntFromJson(rootCfg, "wCtr", -1); + + return true; +} + +static bool isValidControlPin(short pin) +{ + return (pin >= 0 && pin < PINS_COUNT && !isProtectedPin(pin)); +} + +void out_sprinkler::setOutput(short pin, bool value) +{ + if (!isValidControlPin(pin)) return; + digitalWrite(pin, value ? HIGH : LOW); +} + +int out_sprinkler::Setup() +{ + abstractOut::Setup(); + + if (!getConfig()) + { + debugSerial << F("SPRINKLER: config failed") << endl; + return 0; + } + + if (isValidControlPin(vinPin)) { pinMode(vinPin, OUTPUT); digitalWrite(vinPin, LOW); } + if (isValidControlPin(drenPin)) { pinMode(drenPin, OUTPUT); digitalWrite(drenPin, LOW); } + if (isValidControlPin(pumpPin)) { pinMode(pumpPin, OUTPUT); digitalWrite(pumpPin, LOW); } + + aJsonObject * zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + short pin = getIntFromJson(zone, "pin", -1); + if (isValidControlPin(pin)) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } + getCreateObject(zone, "cmd", (long)CMD_OFF); + getCreateObject(zone, "val", (long)0); + getCreateObject(zone, "set", (long)0); + getCreateObject(zone, "@active", (long)0); + } + zone = zone->next; + } + + getCreateObject(gatesObj, "@state", (long)SP_UNKNOWN); + getCreateObject(gatesObj, "@timer", (long)0); + getCreateObject(gatesObj, "@flowTimer", (long)0); + getCreateObject(gatesObj, "@wCtrLast", (long)0); + + if (wCtrPin >= 0 && wCtrPin < PINS_COUNT) pinMode(wCtrPin, INPUT); + if (wMaxPin >= 0 && wMaxPin < PINS_COUNT) pinMode(wMaxPin, INPUT); + if (wMinPin >= 0 && wMinPin < PINS_COUNT) pinMode(wMinPin, INPUT); + if (fbDrenPin >= 0 && fbDrenPin < PINS_COUNT) pinMode(fbDrenPin, INPUT); + if (fbPumpPin >= 0 && fbPumpPin < PINS_COUNT) pinMode(fbPumpPin, INPUT); + + lastWctrState = (wCtrPin >= 0) ? getPinVal(wCtrPin) : false; + + item->setExt(millisNZ()); + setStatus(CST_INITIALIZED); + notifyState(SP_UNKNOWN); + return 1; +} + +int out_sprinkler::Stop() +{ + debugSerial << F("SPRINKLER: stop") << endl; + turnOffAllZones(); + pump(false); + setOutput(vinPin, false); + setOutput(drenPin, false); + setOutput(pumpPin, false); + setStatus(CST_UNKNOWN); + return 1; +} + +int out_sprinkler::Status() +{ + if (!item || !gatesObj) return 0; + + bool wMax = (wMaxPin >= 0) ? getPinVal(wMaxPin) : false; + bool wMin = (wMinPin >= 0) ? getPinVal(wMinPin) : false; + bool fbDren = (fbDrenPin >= 0) ? getPinVal(fbDrenPin) : false; + bool fbPump = (fbPumpPin >= 0) ? getPinVal(fbPumpPin) : false; + + publishBooleanState("$wMax", wMax); + publishBooleanState("$wMin", wMin); + publishBooleanState("$fbDren", fbDren); + publishBooleanState("$fbPump", fbPump); + + int state = getIntFromJson(gatesObj, "@state", SP_UNKNOWN); + publishNumericState("$state", state); + + return 1; +} + +bool out_sprinkler::isFreeze() +{ + if (!item) return false; + return ((item->getFlag(FLAG_FREEZED) & FLAG_FREEZED) == FLAG_FREEZED); +} + +bool out_sprinkler::isNeedPump(bool steelNeed) +{ + if (!gatesObj) return false; + if (!steelNeed && (!item || item->getCmd() != CMD_ON)) return false; + + aJsonObject * zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + if (getIntFromJson(zone, "@active", 0)) return true; + int cmd = getIntFromJson(zone, "cmd", CMD_OFF); + if (cmd == CMD_ON) + { + long setVal = getIntFromJson(zone, "set", 0); + long valVal = getIntFromJson(zone, "val", 0); + if (valVal < setVal) return true; + } + } + zone = zone->next; + } + return false; +} + +void out_sprinkler::pump(bool state) +{ + if (!isValidControlPin(pumpPin)) return; + setOutput(pumpPin, state); + publishBooleanState("$rPump", state); +} + +void out_sprinkler::turnOffAllZones() +{ + if (!gatesObj) return; + aJsonObject * zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + short pin = getIntFromJson(zone, "pin", -1); + setOutput(pin, false); + if (getIntFromJson(zone, "@active", 0)) + { + setZoneActive(zone, false); + } + } + zone = zone->next; + } +} + +void out_sprinkler::turnOffValves() +{ + turnOffAllZones(); + setOutput(vinPin, false); + setOutput(drenPin, false); +} + +void out_sprinkler::notifyState(short state) +{ + if (!gatesObj) return; + aJsonObject * stateObj = getCreateObject(gatesObj, "@state", (long)state); + if (stateObj) stateObj->valueint = state; + publishNumericState("$state", state); +} + +int out_sprinkler::shutdown(sprinklerState nextState) +{ + if (!gatesObj) return 0; + + switch (nextState) + { + case SP_OFF: + case SP_FULL: + setOutput(drenPin, false); + setOutput(vinPin, false); + break; + case SP_DREN_ON: + case SP_DREN_OPERATE: + setOutput(drenPin, true); + setOutput(vinPin, false); + break; + case SP_VIN: + setOutput(vinPin, true); + setOutput(drenPin, false); + break; + case SP_DREN_EMPTY: + setOutput(drenPin, false); + setOutput(vinPin, false); + break; + case SP_FAULT_VIN: + setOutput(vinPin, false); + break; + case SP_FAULT_DREN: + setOutput(drenPin, false); + break; + case SP_UNKNOWN: + setOutput(drenPin, false); + setOutput(vinPin, false); + break; + } + + publishBooleanState("$rDren", nextState == SP_DREN_ON || nextState == SP_DREN_OPERATE); + publishBooleanState("$vIN", nextState == SP_VIN); + notifyState(nextState); + return 1; +} + +inline aJsonObject * out_sprinkler::getZone(const char * name) +{ + if (!gatesObj || !name || !*name) return NULL; + aJsonObject * zone = aJson.getObjectItem(gatesObj, name); + if (zone && zone->type == aJson_Object) return zone; + return NULL; +} + +inline aJsonObject * out_sprinkler::findNextZone() +{ + if (!gatesObj) return NULL; + + aJsonObject * zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + if (getIntFromJson(zone, "@active", 0)) return zone; + } + zone = zone->next; + } + + zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + int cmd = getIntFromJson(zone, "cmd", CMD_OFF); + long setVal = getIntFromJson(zone, "set", 0); + long valVal = getIntFromJson(zone, "val", 0); + if (cmd == CMD_ON && valVal < setVal) return zone; + } + zone = zone->next; + } + return NULL; +} + +void out_sprinkler::setZoneActive(aJsonObject * zone, bool active) +{ + if (!zone) return; + setValToJson(zone, "@active", (long)(active ? 1 : 0)); + + char subItem[48]; + snprintf(subItem, sizeof(subItem), "%s/$state", zone->name); + item->SendStatusImmediate(itemCmd().Cmd(active ? CMD_ON : CMD_OFF).setSuffix(S_CMD), FLAG_COMMAND, subItem); +} + +void out_sprinkler::updateZoneValue(aJsonObject * zone, long value) +{ + if (!zone) return; + long current = getIntFromJson(zone, "val", 0); + current += value; + setValToJson(zone, "val", current); + item->SendStatusImmediate(itemCmd().Int(current).setSuffix(S_VAL), FLAG_PARAMETERS, zone->name); +} + +void out_sprinkler::publishBooleanState(const char * subItem, bool state) +{ + if (!item) return; + item->SendStatusImmediate(itemCmd().Cmd(state ? CMD_ON : CMD_OFF).setSuffix(S_CMD), FLAG_COMMAND, (char *)subItem); +} + +void out_sprinkler::publishNumericState(const char * subItem, long value) +{ + if (!item) return; + item->SendStatusImmediate(itemCmd().Int(value).setSuffix(S_SET), FLAG_PARAMETERS, (char *)subItem); +} + +int out_sprinkler::Poll(short cause) +{ + if (!item || !gatesObj) return 0; + + bool freeze = isFreeze(); + bool wMax = (wMaxPin >= 0) ? getPinVal(wMaxPin) : false; + bool wMin = (wMinPin >= 0) ? getPinVal(wMinPin) : false; + bool fbDren = (fbDrenPin >= 0) ? getPinVal(fbDrenPin) : false; + bool fbPump = (fbPumpPin >= 0) ? getPinVal(fbPumpPin) : false; + + publishBooleanState("$wMax", wMax); + publishBooleanState("$wMin", wMin); + publishBooleanState("$fbDren", fbDren); + publishBooleanState("$fbPump", fbPump); + + uint32_t now = millisNZ(); + int state = getIntFromJson(gatesObj, "@state", SP_UNKNOWN); + uint32_t timer = (uint32_t)getIntFromJson(gatesObj, "@timer", 0); + + if (freeze) + { + shutdown(SP_OFF); + turnOffValves(); + pump(false); + return 0; + } + + switch (state) + { + case SP_UNKNOWN: + case SP_OFF: + if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + notifyState(state); + } + else + { + state = SP_DREN_ON; + getCreateObject(gatesObj, "@timer", (long)now)->valueint = now; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_ON); + } + break; + + case SP_DREN_ON: + if (fbDren) + { + state = SP_DREN_OPERATE; + getCreateObject(gatesObj, "@timer", (long)now)->valueint = now; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_OPERATE); + } + else if (isTimeOver(timer, now, DRENAGE_TIME)) + { + state = SP_DREN_EMPTY; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_EMPTY); + } + break; + + case SP_DREN_OPERATE: + if (!fbDren) + { + state = SP_DREN_EMPTY; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_EMPTY); + } + else if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FULL); + } + else if (isTimeOver(timer, now, 1200000UL)) + { + state = SP_FAULT_DREN; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FAULT_DREN); + } + break; + + case SP_DREN_EMPTY: + if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FULL); + } + else if (item->getCmd() == CMD_ON) + { + state = SP_VIN; + getCreateObject(gatesObj, "@timer", (long)now)->valueint = now; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_VIN); + } + break; + + case SP_VIN: + if (fbDren) + { + state = SP_DREN_OPERATE; + getCreateObject(gatesObj, "@timer", (long)now)->valueint = now; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_OPERATE); + } + else if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FULL); + } + else if (isTimeOver(timer, now, 1200000UL)) + { + state = SP_FAULT_VIN; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FAULT_VIN); + } + break; + + case SP_FULL: + if (!wMax) + { + state = SP_DREN_ON; + getCreateObject(gatesObj, "@timer", (long)now)->valueint = now; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_DREN_ON); + } + break; + + case SP_FAULT_VIN: + if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FULL); + } + break; + + case SP_FAULT_DREN: + if (wMax) + { + state = SP_FULL; + getCreateObject(gatesObj, "@state", (long)state)->valueint = state; + shutdown(SP_FULL); + } + break; + } + + bool tankReady = (state == SP_FULL || wMax); + bool needPump = false; + aJsonObject * currentZone = NULL; + + if (item->getCmd() == CMD_ON && !tankReady) + { + turnOffAllZones(); + pump(false); + return 0; + } + + if (item->getCmd() == CMD_ON && tankReady) + { + currentZone = findNextZone(); + if (currentZone) + { + long setVal = getIntFromJson(currentZone, "set", 0); + long valVal = getIntFromJson(currentZone, "val", 0); + + if (!getIntFromJson(currentZone, "@active", 0)) + { + turnOffAllZones(); + setZoneActive(currentZone, true); + short zonePin = getIntFromJson(currentZone, "pin", -1); + setOutput(zonePin, true); + getCreateObject(gatesObj, "@flowTimer", (long)now)->valueint = now; + } + + if (wCtrPin >= 0) + { + bool curr = getPinVal(wCtrPin); + if (curr && !lastWctrState) + { + updateZoneValue(currentZone, 1); + } + lastWctrState = curr; + } + else + { + uint32_t flowTimer = (uint32_t)getIntFromJson(gatesObj, "@flowTimer", now); + if (isTimeOver(flowTimer, now, 1000UL)) + { + updateZoneValue(currentZone, 1); + getCreateObject(gatesObj, "@flowTimer", (long)now)->valueint = now; + } + } + + long setVal2 = getIntFromJson(currentZone, "set", 0); + long valVal2 = getIntFromJson(currentZone, "val", 0); + if (setVal2 > 0 && valVal2 >= setVal2) + { + setOutput(getIntFromJson(currentZone, "pin", -1), false); + setZoneActive(currentZone, false); + setValToJson(currentZone, "cmd", (long)CMD_OFF); + item->SendStatusImmediate(itemCmd().Cmd(CMD_OFF).setSuffix(S_CMD), FLAG_COMMAND, currentZone->name); + currentZone = findNextZone(); + } + + if (currentZone) + { + needPump = true; + } + } + } + + if (!needPump) + { + pump(false); + if (item->getCmd() == CMD_ON) + { + aJsonObject * resultZone = findNextZone(); + if (!resultZone) + { + item->setCmd(CMD_OFF); + item->SendStatus(FLAG_COMMAND); + } + } + } + else + { + pump(true); + } + + return 0; +} + +int out_sprinkler::Ctrl(itemCmd cmd, char* subItem, bool toExecute, bool authorized) +{ + if (!item || !gatesObj) return 0; + int suffixCode = cmd.isCommand() ? S_CMD : cmd.getSuffix(); + + if (subItem && *subItem) + { + aJsonObject * zone = getZone(subItem); + if (!zone) return 0; + + switch (suffixCode) + { + case S_SET: + if (toExecute) + { + long value = cmd.getInt(); + setValToJson(zone, "set", value); + item->SendStatusImmediate(itemCmd().Int(value).setSuffix(S_SET), FLAG_PARAMETERS, subItem); + } + return 1; + + case S_VAL: + if (toExecute) + { + long value = cmd.getInt(); + setValToJson(zone, "val", value); + item->SendStatusImmediate(itemCmd().Int(value).setSuffix(S_VAL), FLAG_PARAMETERS, subItem); + } + return 1; + + case S_CMD: + default: + switch (cmd.getCmd()) + { + case CMD_ON: + if (toExecute) + { + setValToJson(zone, "cmd", (long)CMD_ON); + item->SendStatusImmediate(itemCmd().Cmd(CMD_ON).setSuffix(S_CMD), FLAG_COMMAND, subItem); + } + return 1; + + case CMD_OFF: + if (toExecute) + { + setValToJson(zone, "cmd", (long)CMD_OFF); + setZoneActive(zone, false); + setOutput(getIntFromJson(zone, "pin", -1), false); + item->SendStatusImmediate(itemCmd().Cmd(CMD_OFF).setSuffix(S_CMD), FLAG_COMMAND, subItem); + } + return 1; + + case CMD_RESET: + if (toExecute) + { + setValToJson(zone, "val", (long)0); + item->SendStatusImmediate(itemCmd().Int(0).setSuffix(S_VAL), FLAG_PARAMETERS, subItem); + } + return 1; + + default: + return 0; + } + } + } + + switch (suffixCode) + { + case S_CMD: + switch (cmd.getCmd()) + { + case CMD_ON: + return 1; + + case CMD_OFF: + turnOffAllZones(); + pump(false); + return 1; + + case CMD_RESET: + { + aJsonObject * zone = gatesObj->child; + while (zone) + { + if (zone->name && *zone->name && zone->type == aJson_Object) + { + setValToJson(zone, "val", (long)0); + item->SendStatusImmediate(itemCmd().Int(0).setSuffix(S_VAL), FLAG_PARAMETERS, zone->name); + } + zone = zone->next; + } + } + return 1; + + default: + break; + } + break; + + case S_SET: + if (toExecute) + { + long value = cmd.getInt(); + if (value < 0) + { + item->setFlag(FLAG_FREEZED); + item->SendStatus(FLAG_FLAGS); + } + else if (isFreeze()) + { + item->clearFlag(FLAG_FREEZED); + item->SendStatus(FLAG_FLAGS); + } + } + return 1; + + case S_VAL: + if (toExecute) + { + long value = cmd.getInt(); + if (value < 0) + { + item->setFlag(FLAG_FREEZED); + item->SendStatus(FLAG_FLAGS); + } + else if (isFreeze()) + { + item->clearFlag(FLAG_FREEZED); + item->SendStatus(FLAG_FLAGS); + } + } + return 1; + + default: + break; + } + + return 0; +} + +int out_sprinkler::getChanType() +{ + return CH_RELAY; +} + +#endif // SPRINKLER_ENABLE \ No newline at end of file diff --git a/lighthub/modules/out_sprinkler.h b/lighthub/modules/out_sprinkler.h new file mode 100644 index 0000000..8c7795a --- /dev/null +++ b/lighthub/modules/out_sprinkler.h @@ -0,0 +1,59 @@ + +#pragma once +#include "options.h" +#ifdef SPRINKLER_ENABLE + +#include +#include + +#define DRENAGE_TIME 10000 + + +enum sprinklerState { + SP_UNKNOWN = 0, + SP_OFF = 1, + SP_DREN_ON = 2, + SP_DREN_OPERATE = 3, + SP_DREN_EMPTY = 4, + SP_VIN = 5, + SP_FULL = 6, + SP_FAULT_VIN = -1, + SP_FAULT_DREN = -2 +}; + +class out_sprinkler : public abstractOut { +public: + + //out_sprinkler(){ /*NO getConfig() here due Poll() optimization*/ }; + bool getConfig(); + + int Setup() override; + int Poll(short cause) override; + int Stop() override; + int Status() override; + + int getChanType() override; + int Ctrl(itemCmd cmd, char* subItem=NULL, bool toExecute=true, bool authorized = false) override; + +protected: + aJsonObject * gatesObj; + short vinPin, drenPin, pumpPin; + short wMaxPin, wMinPin, fbDrenPin, fbPumpPin, wCtrPin; + bool lastWctrState; + + void pump(bool state); + void setOutput(short pin, bool value); + bool isNeedPump(bool steelNeed=false); + void turnOffValves(); + void turnOffAllZones(); + aJsonObject * getZone(const char * name); + aJsonObject * findNextZone(); + void setZoneActive(aJsonObject * zone, bool active); + void updateZoneValue(aJsonObject * zone, long value); + void publishBooleanState(const char * subItem, bool state); + void publishNumericState(const char * subItem, long value); + bool isFreeze(); + void notifyState(short state); + int shutdown(sprinklerState nextState); +}; +#endif \ No newline at end of file