MultiAC small fix + SPRINKLER chanell type (initial, unverivied)

This commit is contained in:
2026-05-01 19:57:51 +03:00
parent e42053ab1b
commit 04d709a1e8
7 changed files with 960 additions and 3 deletions

View File

@@ -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 - текущее время или обьем полива данной зоны
```

View File

@@ -68,6 +68,8 @@ e-mail anklimov@gmail.com
#include "modules/out_humidifier.h"
#endif
#include "modules/out_sprinkler.h"
#ifdef CANDRV
#include <candriver.h>
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 ;

View File

@@ -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

View File

@@ -2717,6 +2717,12 @@ infoSerial<<F("\n(+)MERCURY");
infoSerial<<F("\n(-)MERCURY");
#endif
#ifdef SPRINKLER_ENABLE
infoSerial<<F("\n(+)SPRINKLER");
#else
infoSerial<<F("\n(-)SPRINKLER");
#endif
#ifdef CANDRV
infoSerial<<F("\n(+)CAN");
#else

View File

@@ -398,8 +398,8 @@ int out_Multivent::Poll(short cause)
{
resetBoost();
if (autoRequested) sendACcmd(itemCmd().Cmd(CMD_AUTO));
else if (ventRequested) sendACcmd(itemCmd().Cmd(CMD_FAN));
if (autoRequested && lastACfan>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
}

View File

@@ -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

View File

@@ -0,0 +1,59 @@
#pragma once
#include "options.h"
#ifdef SPRINKLER_ENABLE
#include <abstractout.h>
#include <item.h>
#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