#ifndef MULTIVENT_DISABLE #include "modules/out_multivent.h" #include "Arduino.h" #include "options.h" #include "Streaming.h" #include "item.h" #include "main.h" #include "utils.h" void convert2float(aJsonObject * o) { if (!o) return; switch (o->type) { case aJson_Int: o->valuefloat = o->valueint; o->type = aJson_Float; break; } } void out_Multivent::getConfig() { gatesObj = NULL; acObj = NULL; if (!item || !item->itemArg || item->itemArg->type != aJson_Object) return; gatesObj = item->itemArg; if (gatesObj) acObj = aJson.getObjectItem(gatesObj, ""); } int out_Multivent::Setup() { abstractOut::Setup(); //getConfig(); //Allocate objects to store persistent data in config tree if (gatesObj) { aJsonObject * i = gatesObj->child; while (i) { if (i->name && *i->name) { getCreateObject(i,"fan",-1L); getCreateObject(i,"cmd",(long) CMD_OFF); getCreateObject(i,"out",-1L); aJsonObject * pidObj = aJson.getObjectItem(i, "pid"); if (pidObj && pidObj->type == aJson_Array && aJson.getArraySize(pidObj)>=3) { aJsonObject * setObj = getCreateObject(i,"set",(float) 20.0); convert2float(setObj); aJsonObject * valObj = getCreateObject(i,"val",(float) NAN); convert2float(valObj); aJsonObject * poObj = getCreateObject(i,"po", (float) -2.0); convert2float(poObj); int direction = DIRECT; float kP=getFloatFromJson(pidObj,"kP",0,1.0); if (kP<0) { kP=-kP; direction=REVERSE; } float kI=getFloatFromJson(pidObj,"kI",1,0.0); float kD=getFloatFromJson(pidObj,"kD",2,0.0); float dT=getFloatFromJson(pidObj,"dT",3,5.0); pidObj->valueint = (long int) new PID (&valObj->valuefloat, &poObj->valuefloat, &setObj->valuefloat, kP, kI, kD, P_ON_E,direction, getFloatFromJson(i,"AlarmTime",4,120.0)*1000UL, getFloatFromJson(i,"AlarmValue",5,0.0)); //((PID*) pidObj->valueint)->SetMode (AUTOMATIC); ((PID*) pidObj->valueint)->SetSampleTime(dT*1000.0); debugSerial << F ("VENT: PID P=")<setExt(0); setStatus(CST_INITIALIZED); return 1; } debugSerial << F ("VENT: config failed")<< endl; return 0; } int out_Multivent::Stop() { debugSerial << F ("VENT: De-Init") << endl; if (gatesObj) { aJsonObject * i = gatesObj->child; while (i) { if (i->name && *i->name) { aJsonObject * pidObj = aJson.getObjectItem(i, "pid"); if (pidObj && pidObj->valueint) { delete ((PID *) pidObj->valueint); pidObj->valueint = 0;//NULL; } } i=i->next; } } setStatus(CST_UNKNOWN); return 1; } int out_Multivent::isActive() { //debugSerial<<"VENT:active: "; if (gatesObj) { /* // metrics, collected from AC aJsonObject * a = aJson.getObjectItem(gatesObj, ""); if (!a) return 0; float acTemp = getFloatFromJson(a,"val",NAN); int actualCmd = getIntFromJson (a,"mode"); int actualMode = CMD_FAN; if (acTemp>30.0) actualMode = CMD_HEAT; else if (acTemp<15.0) actualMode = CMD_COOL; */ aJsonObject * i = gatesObj->child; while (i) { if (i->name && *i->name) { int cmd = getIntFromJson(i,"cmd"); switch (cmd) { case CMD_ON: case CMD_HEATCOOL: case CMD_FAN: case CMD_AUTO: case CMD_COOL: case CMD_HEAT: case CMD_DRY: //case CMD_OFF: return 1; break; } } i=i->next; }//while } // if gatesObj return 0; } void out_Multivent::stopAllzones(){ if (!gatesObj) return; debugSerial << F("VENT: Stop all zones. ")<child; while (i) { if (i->name && *i->name) { int cmd = getIntFromJson(i,"cmd"); switch (cmd) { case CMD_ON: case CMD_HEATCOOL: case CMD_FAN: case CMD_AUTO: case CMD_COOL: case CMD_HEAT: case CMD_DRY: //case CMD_OFF: setValToJson(i,"@preHaltcmd",cmd); // setPassiveMode(i, true); fanCtrl(itemCmd().Cmd(CMD_OFF).setSuffix(S_CMD),i->name,true); break; } } i=i->next; }//while } void out_Multivent::restoreAllzones(){ if (!gatesObj) return; debugSerial << F("VENT: Restore all zones. ")<child; while (i) { if (i->name && *i->name) { int preHaltcmd = getIntFromJson(i,"@preHaltcmd",CMD_OFF); if (preHaltcmd && (preHaltcmd != CMD_OFF)) //setPassiveMode(i, false); fanCtrl(itemCmd().Cmd(preHaltcmd).setSuffix(S_CMD),i->name,true); setValToJson(i,"@preHaltcmd",0); //reset preHaltcmd in any case } i=i->next; }//while }; #define assign_if_positive(var, source) {int x=source; if (x>=0) {var=x;}} int out_Multivent::Poll(short cause) { if (!acObj || !gatesObj) return 0; if (cause == POLLING_SLOW && item->getExt() && isTimeOver(item->getExt(),millisNZ(),60000L)) { item->setExt(0); aJsonObject * a = aJson.getObjectItem(acObj,"val"); if (a) a->type = aJson_NULL; //invalidate in 60 sec after measure } // metrics, collected from AC float acTemp = getFloatFromJson(acObj,"val",NAN); int acCmd = getIntFromJson (acObj,"mode"); // global params int boostTreshold = getIntFromJson (acObj,"boost",AC_BOOST_TRESHOLD); int actualMode = CMD_FAN; if (acTemp>30.0) actualMode = CMD_HEAT; else if (acTemp<15.0) actualMode = CMD_COOL; if (isnan(acTemp) && acCmd) actualMode = acCmd; aJsonObject * i = gatesObj->child; int balance = 0; bool ventRequested = false; //At least 1 ch requested FAN mode bool autoRequested = false; //At least 1 ch requested AUTO mode bool pidActive = false; bool pidComputed = false; int lastACfan = -1; while (i) { if (i->name && *i->name) { int cmd = getIntFromJson (i,"cmd"); float set = getFloatFromJson(i,"set"); float val = getFloatFromJson(i,"val"); int fan = getIntFromJson(i,"fan"); int execCmd = 0; bool weakMode=false; // kind of modes when we activating PID only if AC in active mode switch (cmd) { case CMD_HEATCOOL: { switch (acCmd) //TODO - release heat/cool then PID released it, not just set <> val { case CMD_HEAT: if (set>val) execCmd = CMD_HEAT; if (setval+AC_HEATCOOL_DEADBAND) execCmd = CMD_HEAT; break; default: if (set>val+AC_HEATCOOL_DEADBAND) execCmd = CMD_HEAT; if (set0) ventRequested = true; weakMode = true; execCmd = cmd; break; case CMD_AUTO: if (fan>0) autoRequested = true; weakMode = true; execCmd = cmd; break; case CMD_COOL: case CMD_HEAT: case CMD_OFF: execCmd = cmd; break; } bool passiveMode = getIntFromJson(i,"@pasv",0); aJsonObject * pidObj = aJson.getObjectItem(i, "pid"); aJsonObject * poObj = aJson.getObjectItem(i, "po"); aJsonObject * valObj = aJson.getObjectItem(i, "val"); if (pidObj && pidObj->valueint && poObj && poObj->type == aJson_Float && valObj && valObj->type == aJson_Float) { PID * p = (PID *) pidObj->valueint; /// Setup PID mode and direction based on real AC mode switch (actualMode) { //if air hot or cold - uses temp PID and block control by /fan case CMD_HEAT: if (weakMode || passiveMode) p->SetMode(AUTOMATIC); p->SetControllerDirection(DIRECT); break; case CMD_COOL: if (weakMode || passiveMode) p->SetMode(AUTOMATIC); p->SetControllerDirection(REVERSE); break; default: if ((passiveMode || weakMode || execCmd ==CMD_OFF) && p->GetMode() == AUTOMATIC) { p->SetMode(MANUAL); debugSerial<name<name,true)); } } if (!p->isOutdated() && (execCmd == CMD_HEAT || execCmd == CMD_COOL) && p->GetMode() == AUTOMATIC) pidActive = true; if (p->Compute()) { debugSerial<itemArr->name<<"/"<name <GetIn()<GetSet()<GetOut() <<" P:"<GetKp()<<" I:"<GetKi()<<" D:"<GetKd()<<((p->GetDirection())?" Rev ":" Dir ")<<((p->GetMode())?"A":"M"); if (p->isOutdated()) debugSerial << F(" "); debugSerial<valuefloat)) switch (execCmd) { case CMD_HEAT: if (actualMode==CMD_COOL) //close assign_if_positive(lastACfan, fanCtrl(itemCmd().Percents255(0).setSuffix(S_FAN),i->name,true)) else assign_if_positive(lastACfan, fanCtrl(itemCmd().Percents255(poObj->valuefloat).setSuffix(S_FAN),i->name,true)) balance+=poObj->valuefloat; pidComputed = true; break; case CMD_COOL: if (actualMode==CMD_HEAT) //close assign_if_positive(lastACfan, fanCtrl(itemCmd().Percents255(0).setSuffix(S_FAN),i->name,true)) else assign_if_positive(lastACfan, fanCtrl(itemCmd().Percents255(poObj->valuefloat).setSuffix(S_FAN),i->name,true)); balance-=poObj->valuefloat; pidComputed = true; break; default: switch (actualMode) { case CMD_HEAT: debugSerial<itemArr->name<<"/"<name<valuefloat <valuefloat).setSuffix(S_FAN),i->name,true)); break; case CMD_COOL: debugSerial<itemArr->name<<"/"<name<valuefloat <valuefloat).setSuffix(S_FAN),i->name,true)); break; case CMD_FAN: //no more hot or cold air ((PID *) pidObj->valueint)->SetMode(MANUAL); } } } else //PID not computed - maybe not in time, but we can use PID output as indicator of balance and boost if needed { if (p->GetMode() == AUTOMATIC && !isnan(poObj->valuefloat)) { balance+=(p->GetDirection() == DIRECT) ? poObj->valuefloat : -poObj->valuefloat; //if (execCmd == CMD_HEAT) balance+=poObj->valuefloat; //else if (execCmd == CMD_COOL) balance-=poObj->valuefloat; } } } } i=i->next; }//while if (pidComputed) //Active mode. PIDs in pasive mode do not triggered this flag { debugSerial<boostTreshold) setBoost(itemCmd().Cmd(CMD_HEAT).Int(AC_BOOST_HIGH_TEMP).setSuffix(S_SET)); else if (-balance>boostTreshold) setBoost(itemCmd().Cmd(CMD_COOL).Int(AC_BOOST_LOW_TEMP).setSuffix(S_SET)); else { if (abs(balance)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 } if (lastACfan>0 || (lastACfan == 0 && (!activeMode || noFurtherModes))) sendACcmd(itemCmd().Percents255(lastACfan).setSuffix(S_FAN)); //if PID already set some fan value - send it to AC return 1; }; int out_Multivent::getChanType() { return CH_THERMO; } void SubmitParameters(aJsonObject * callbackObj, const char * name, itemCmd value, bool doMapping){ if (callbackObj && callbackObj->type == aJson_Object) { aJsonObject * execObj = aJson.getObjectItem(callbackObj,name); if (execObj) { aJsonObject * mapObj = NULL; if (doMapping) mapObj = aJson.getObjectItem(execObj, "map"); executeCommand(execObj,-1,value.doReverseMapping(mapObj)); } } } void out_Multivent::setPassiveMode(aJsonObject* zone, bool mode) { bool passiveMode = getIntFromJson(zone,"@pasv",0); if (passiveMode != mode) { aJsonObject * cascadeObj=aJson.getObjectItem(zone, "cas"); //aJsonObject * cmdObj=aJson.getObjectItem(zone, "cmd"); //Set up passive mode debugSerial<SendStatusImmediate(itemCmd().Cmd(CMD_AUTO).setSuffix(S_FAN),FLAG_COMMAND,zone->name); //Send /fan->AUTO SubmitParameters(cascadeObj,"fan",itemCmd().Cmd(CMD_AUTO).setSuffix(S_FAN),false); setValToJson(zone,"fan",0); //reset fan level on passive mode ON } else { aJsonObject * fanObj=aJson.getObjectItem(zone, "fan"); if (!fanObj || fanObj->type!=aJson_Int) return; if (isNotRetainingStatus()) item->SendStatusImmediate(itemCmd().Percents255(fanObj->valueint).setSuffix(S_FAN),FLAG_PARAMETERS,zone->name); //Send /fan-># SubmitParameters(cascadeObj,"fan",itemCmd().Percents255(fanObj->valueint).setSuffix(S_FAN),true); } } } uint32_t out_Multivent::getFlag (aJsonObject* zone, uint32_t flag) { if (zone && (zone->type == aJson_Object)) { return (uint32_t) zone->valueint & flag & FLAG_MASK; } return 0; } void out_Multivent::setFlag (aJsonObject* zone, uint32_t flag) { if (zone && (zone->type == aJson_Object)) { zone->valueint |= flag & FLAG_MASK; } } void out_Multivent::clearFlag (aJsonObject* zone, uint32_t flag) { if (zone && (zone->type == aJson_Object)) { zone->valueint &= CMD_MASK | ~(flag & FLAG_MASK); } } int out_Multivent::Ctrl(itemCmd cmd, char* subItem , bool toExecute, bool authorized) { int result = fanCtrl(cmd,subItem, false); if (result>=0) { debugSerial << F("VENT: CTRL fan level: ")<setExt(millisNZ()); //setup validity interval setValToJson(acObj,"val",cmd.getFloat()); } return -1; case S_FAN: if (cmd.isValue()) { debugSerial << F("VENT: ")<child; // Pass 1 - calculate summ air value, max value etc int activeV = 0; int totalV = 0; int maxV=0; int maxRequestedV=0; int maxPercent=0; bool exitAfterSendingStatus = false; while (i) { aJsonObject * fanObj=aJson.getObjectItem(i, "fan"); aJsonObject * cmdObj=aJson.getObjectItem(i, "cmd"); aJsonObject * cascadeObj=aJson.getObjectItem(i, "cas"); //aJsonObject * setObj=aJson.getObjectItem(i, "set"); aJsonObject * pidObj=aJson.getObjectItem(i, "pid"); if (fanObj && cmdObj && fanObj->type==aJson_Int && cmdObj->type==aJson_Int) { int V = getIntFromJson(i,"V",60); bool passiveMode = getIntFromJson(i,"@pasv",0); int requestedV = 0; if (subItem && !strcmp (i->name,subItem)) { long sendFlags = 0; switch (suffixCode) { case S_FAN: if (getFlag(i,FLAG_FREEZED)) {debugSerial<valueint == CMD_OFF && turnbyfan) { cmd.Cmd(turnbyfan); debugSerial<<"VENT: generating cmd by fan: "<valueint != CMD_OFF && turnbyfan) { cmd.Cmd(CMD_OFF); debugSerial<<"VENT: Turning OFF by fan"<valueint = cmd.getInt(); // if (!passiveMode) sendFlags |= FLAG_PARAMETERS; // //if (isNotRetainingStatus()) item->SendStatusImmediate(cmd,FLAG_PARAMETERS|FLAG_COMMAND,i->name); } else if (cmd.getCmd() == CMD_AUTO) { setPassiveMode(i,true); //Setup flag passiveMode = true; cmd.Cmd(CMD_OFF); cmd.setSuffix(S_CMD); } else if (cmd.getCmd() == CMD_OFF) { setPassiveMode(i,false); passiveMode = false; } if (!cmd.isCommand()) break; // if have command in FAN suffix - continue processing debugSerial<<"VENT: cmd in FAN suffix, process as command. cmd="<valueint != CMD_OFF && cmdObj->valueint != CMD_HALT) break; cmd.Percents255(fanObj->valueint); cmd.setSuffix(S_FAN); sendFlags |= FLAG_COMMAND | FLAG_PARAMETERS; //cmdObj->valueint = cmd.getCmd(); cmdObj->valueint = getIntFromJson(i,"@precmd",CMD_FAN); debugSerial<<"VENT: Turning ON. cmd:"<valueint<valueint); setPassiveMode(i,false); } switch (cmd.getCmd()) { case CMD_ON: checkMinFanLevel = true; break; case CMD_OFF: if (getFlag(i,FLAG_FREEZED)) {debugSerial<valueint != CMD_OFF) setValToJson(i,"@precmd",cmdObj->valueint); //saving previous mode cmd.Percents255(0); cmd.setSuffix(S_FAN); debugSerial<<"VENT: Turning OFF. saving cmd:"<valueint<valueint = CMD_OFF; enablePid(pidObj,false); break; /* case CMD_ENABLE: if (pidObj && pidObj->valueint) ((PID *) pidObj->valueint)->SetMode(AUTOMATIC); sendFlags |= FLAG_FLAGS; setPassiveMode(i,false); break; case CMD_DISABLE: if (pidObj && pidObj->valueint) ((PID *) pidObj->valueint)->SetMode(MANUAL); sendFlags |= FLAG_FLAGS; setPassiveMode(i,false); break; */ case CMD_FREEZE: setFlag(i,FLAG_FREEZED); return -1; case CMD_UNFREEZE: clearFlag(i,FLAG_FREEZED); return -1; case CMD_COOL: case CMD_DRY: enablePid(pidObj,true,REVERSE); sendFlags |= FLAG_COMMAND; cmdObj->valueint = cmd.getCmd(); setPassiveMode(i,false); checkMinFanLevel = true; break; case CMD_HEAT: enablePid(pidObj,true,DIRECT); case CMD_HEATCOOL: enablePid(pidObj,true); sendFlags |= FLAG_COMMAND; cmdObj->valueint = cmd.getCmd(); setPassiveMode(i,false); checkMinFanLevel = true; break; case CMD_FAN: sendFlags |= FLAG_PARAMETERS; //experimental 30/03/26 cmd.Percents255(fanObj->valueint); cmd.setSuffix(S_FAN); // continue case CMD_AUTO: enablePid(pidObj,false); sendFlags |= FLAG_COMMAND; cmdObj->valueint = cmd.getCmd(); setPassiveMode(i,false); checkMinFanLevel = true; break; //todo - halt-rest-xon-xoff } if (isNotRetainingStatus() && checkMinFanLevel && (fanObj->valueint<20)) { fanObj->valueint=30; cmd.Percents255(30); sendFlags |= FLAG_PARAMETERS; } } break; case S_SET: if (cmd.isValue()) { setValToJson(i,"set",cmd.getFloat()); sendFlags |= FLAG_PARAMETERS; exitAfterSendingStatus = true; //if setpoint updated - send status immediately to update PID and sending status } else return -1; break; case S_VAL: if (cmd.isValue()) { aJsonObject * pidObj = aJson.getObjectItem(i, "pid"); if (pidObj && pidObj->valueint) { ((PID *) pidObj->valueint)->SetVal(cmd.getFloat()); debugSerial<name<SendStatusImmediate(itemCmd().Cmd(cmd).setSuffix(S_CMD),FLAG_COMMAND,i->name); if (sendFlags & FLAG_PARAMETERS ) item->SendStatusImmediate(cmd,FLAG_PARAMETERS,i->name); if (sendFlags & FLAG_FLAGS) item->SendStatusImmediate(cmd,FLAG_FLAGS,i->name); } if (cascadeObj) { if (sendFlags & FLAG_COMMAND) SubmitParameters(cascadeObj,"cmd",itemCmd().Cmd(cmd).setSuffix(S_CMD).setArgType(0),true); if (sendFlags & FLAG_PARAMETERS) switch (cmd.getSuffix()) { case S_SET: SubmitParameters(cascadeObj,"set",cmd,true); break; case S_FAN: SubmitParameters(cascadeObj,"fan",cmd,true); break; } } } // subitem if (exitAfterSendingStatus) return -1; //if setpoint updated - send status immediately to update PID and sending status if ((cmdObj->valueint != CMD_OFF && cmdObj->valueint != -1) || passiveMode) { requestedV=V*fanObj->valueint; activeV+=requestedV; if (fanObj->valueint>maxPercent ) { maxRequestedV=requestedV; maxV=V; maxPercent=fanObj->valueint; } } totalV+=V; } i=i->next; } if (!totalV) return 0; int fanV=activeV/totalV; debugSerial << F("VENT: Total V:")<child; //Pass 2: re-distribute airflow while (i) { int V = getIntFromJson(i,"V",60); aJsonObject * outObj=aJson.getObjectItem(i, "out"); aJsonObject * fanObj=aJson.getObjectItem(i, "fan"); aJsonObject * cmdObj=aJson.getObjectItem(i, "cmd"); bool passiveMode = getIntFromJson(i,"@pasv",0); if (outObj && fanObj && cmdObj && outObj->type==aJson_Int && fanObj->type==aJson_Int && cmdObj->type==aJson_Int && V) { long int out = 0; if (((cmdObj->valueint != CMD_OFF && cmdObj->valueint != -1) || passiveMode) && maxRequestedV) { int requestedV=V*fanObj->valueint; out = (( long)requestedV*255L)/(( long)V)*( long)maxV/( long)maxRequestedV; debugSerial<name<valueint)) { //report out executeCommand(i,-1,itemCmd().Percents255(out)); outObj->valueint=out; } } i=i->next; } return fanV; } void out_Multivent::enablePid(aJsonObject* pidObj, int enable, int direction ) { if (pidObj && pidObj->valueint) { ((PID *) pidObj->valueint)->SetMode(enable); if (direction != -1) ((PID *) pidObj->valueint)->SetControllerDirection(direction); } } bool out_Multivent::pidEnabled(aJsonObject* pidObj) { return ((pidObj && pidObj->valueint) && (((PID *) pidObj->valueint)->GetMode() ==AUTOMATIC)); } void out_Multivent::checkACcmd (int acCmd) { if (!acObj) return; int lastCmd = getIntFromJson(acObj,"@lastCmd"); int prevACcmd = getIntFromJson(acObj,"mode"); int lastFan = getIntFromJson(acObj,"@lastFan",-1); if (!prevACcmd) { debugSerial<<"VENT: AC MODE received initially "<getCmd()<0); switch (cmd.getSuffix()) { case S_FAN: if (!(supress & AC_SUPPRESS_FAN) || lastFan != cmd.getInt()) { debugSerial<<"VENT: sendACcmd. fan="; cmd.debugOut(); debugSerial< 0 && !isActiveNow) { int preCmd = getIntFromJson(acObj,"@preCmd",CMD_FAN); setValToJson(acObj,"@lastCmd",preCmd); } break; case S_SET: if (!(supress & AC_SUPPRESS_SET) || lastSet != cmd.getFloat()) { debugSerial<<"VENT: sendACcmd. set="; cmd.debugOut(); debugSerial<=AC_BOOST_HIGH_TEMP) preTemp = AC_PRESET_TEMP; sendACcmd(itemCmd().Cmd(0).Int(preTemp).setSuffix(S_SET)); clearFlag(acObj,FLAG_ACTION_NEEDED); } void out_Multivent::notifyState(itemCmd state) { char val[16]; state.toString(val,sizeof(val),FLAG_COMMAND); publishTopic(item->itemArr->name,val,"/$state"); } #endif