Last active
June 21, 2025 15:55
-
-
Save bbartling/14824ea508ab05b1bdd6b1fba54031d8 to your computer and use it in GitHub Desktop.
Modernized Optimal Start Efforts based on PNNL white paper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tridium.kitControl.energy; | |
import java.text.DecimalFormat; | |
import javax.baja.log.Log; | |
import javax.baja.nre.annotations.NiagaraActions; | |
import javax.baja.nre.annotations.NiagaraProperties; | |
import javax.baja.nre.annotations.NiagaraType; | |
import javax.baja.status.BStatus; | |
import javax.baja.status.BStatusBoolean; | |
import javax.baja.status.BStatusNumeric; | |
import javax.baja.status.BStatusString; | |
import javax.baja.sys.Action; | |
import javax.baja.sys.BAbsTime; | |
import javax.baja.sys.BComplex; | |
import javax.baja.sys.BComponent; | |
import javax.baja.sys.BFacets; | |
import javax.baja.sys.BRelTime; | |
import javax.baja.sys.BTime; | |
import javax.baja.sys.Clock; | |
import javax.baja.sys.Clock.Ticket; | |
import javax.baja.sys.Context; | |
import javax.baja.sys.Property; | |
import javax.baja.sys.Slot; | |
import javax.baja.sys.Sys; | |
import javax.baja.sys.Type; | |
@NiagaraType | |
@NiagaraProperties({@javax.baja.nre.annotations.NiagaraProperty(name="heatCoolMode", type="BStatusBoolean", defaultValue="new BStatusBoolean(false)", flags=10, facets={@javax.baja.nre.annotations.Facet("BFacets.makeBoolean(\"coolMode\", \"heatMode\")")}), @javax.baja.nre.annotations.NiagaraProperty(name="parameterResetTime", type="BAbsTime", defaultValue="BAbsTime.NULL", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="startEnable", type="BStatusBoolean", defaultValue="new BStatusBoolean(false)", flags=8), @javax.baja.nre.annotations.NiagaraProperty(name="stopEnable", type="BStatusBoolean", defaultValue="new BStatusBoolean(false)", flags=8), @javax.baja.nre.annotations.NiagaraProperty(name="scheduleStatus", type="BStatusBoolean", defaultValue="new BStatusBoolean(false)", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="nextEventTime", type="BAbsTime", defaultValue="BAbsTime.NULL", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="nextEventValue", type="BStatusBoolean", defaultValue="new BStatusBoolean(false)", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="outsideTemp", type="BStatusNumeric", defaultValue="new BStatusNumeric()", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="spaceTemp", type="BStatusNumeric", defaultValue="new BStatusNumeric()", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="startTimeCommand", type="BStatusBoolean", defaultValue="new BStatusBoolean(false, BStatus.nullStatus)", flags=74), @javax.baja.nre.annotations.NiagaraProperty(name="stopTimeCommand", type="BStatusBoolean", defaultValue="new BStatusBoolean(false, BStatus.nullStatus)", flags=74), @javax.baja.nre.annotations.NiagaraProperty(name="message", type="BStatusString", defaultValue="new BStatusString(\"\")", flags=10), @javax.baja.nre.annotations.NiagaraProperty(name="upperComfortLimit", type="float", defaultValue="77.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="lowerComfortLimit", type="float", defaultValue="68.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="dynamicParameterAdjust", type="boolean", defaultValue="true"), @javax.baja.nre.annotations.NiagaraProperty(name="oldParameterMultiplier", type="int", defaultValue="2"), @javax.baja.nre.annotations.NiagaraProperty(name="earliestStartTime", type="BTime", defaultValue="BTime.make(0, 0, 10)"), @javax.baja.nre.annotations.NiagaraProperty(name="earliestStopTime", type="BTime", defaultValue="BTime.make(16, 0, 0)"), @javax.baja.nre.annotations.NiagaraProperty(name="drifttimePerDegreeCoolingUserDefined", type="float", defaultValue="0.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="drifttimePerDegreeHeatingUserDefined", type="float", defaultValue="0.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="runtimePerDegreeCoolingUserDefined", type="float", defaultValue="0.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="runtimePerDegreeHeatingUserDefined", type="float", defaultValue="0.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="drifttimePerDegreeCooling", type="float", defaultValue="10.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="drifttimePerDegreeHeating", type="float", defaultValue="10.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="runtimePerDegreeCooling", type="float", defaultValue="10.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="runtimePerDegreeHeating", type="float", defaultValue="10.0f"), @javax.baja.nre.annotations.NiagaraProperty(name="lastStartTime", type="BAbsTime", defaultValue="BAbsTime.NULL", flags=2), @javax.baja.nre.annotations.NiagaraProperty(name="lastStopTime", type="BAbsTime", defaultValue="BAbsTime.NULL", flags=2), @javax.baja.nre.annotations.NiagaraProperty(name="outsideTempAtBeginning", type="BStatusNumeric", defaultValue="new BStatusNumeric()", flags=2), @javax.baja.nre.annotations.NiagaraProperty(name="spaceTempAtBeginning", type="BStatusNumeric", defaultValue="new BStatusNumeric()", flags=2), @javax.baja.nre.annotations.NiagaraProperty(name="calculatedCommandTime", type="BTime", defaultValue="BTime.DEFAULT", flags=66), @javax.baja.nre.annotations.NiagaraProperty(name="programMode", type="int", defaultValue="0", flags=66)}) | |
@NiagaraActions({@javax.baja.nre.annotations.NiagaraAction(name="startTimeTrigger", flags=4), @javax.baja.nre.annotations.NiagaraAction(name="stopTimeTrigger", flags=4), @javax.baja.nre.annotations.NiagaraAction(name="calculate", flags=4)}) | |
public class BOptimizedStartStop extends BComponent | |
{ | |
public static final Property heatCoolMode = newProperty(10, new BStatusBoolean(false), BFacets.makeBoolean("coolMode", "heatMode")); | |
public static final Property parameterResetTime = newProperty(10, BAbsTime.NULL, null); | |
public static final Property startEnable = newProperty(8, new BStatusBoolean(false), null); | |
public static final Property stopEnable = newProperty(8, new BStatusBoolean(false), null); | |
public static final Property scheduleStatus = newProperty(10, new BStatusBoolean(false), null); | |
public static final Property nextEventTime = newProperty(10, BAbsTime.NULL, null); | |
public static final Property nextEventValue = newProperty(10, new BStatusBoolean(false), null); | |
public static final Property outsideTemp = newProperty(10, new BStatusNumeric(), null); | |
public static final Property spaceTemp = newProperty(10, new BStatusNumeric(), null); | |
public static final Property startTimeCommand = newProperty(74, new BStatusBoolean(false, BStatus.nullStatus), null); | |
public static final Property stopTimeCommand = newProperty(74, new BStatusBoolean(false, BStatus.nullStatus), null); | |
public static final Property message = newProperty(10, new BStatusString(""), null); | |
public static final Property upperComfortLimit = newProperty(0, 77.0F, null); | |
public static final Property lowerComfortLimit = newProperty(0, 68.0F, null); | |
public static final Property dynamicParameterAdjust = newProperty(0, true, null); | |
public static final Property oldParameterMultiplier = newProperty(0, 2, null); | |
public static final Property earliestStartTime = newProperty(0, BTime.make(0, 0, 10), null); | |
public static final Property earliestStopTime = newProperty(0, BTime.make(16, 0, 0), null); | |
public static final Property drifttimePerDegreeCoolingUserDefined = newProperty(0, 0F, null); | |
public static final Property drifttimePerDegreeHeatingUserDefined = newProperty(0, 0F, null); | |
public static final Property runtimePerDegreeCoolingUserDefined = newProperty(0, 0F, null); | |
public static final Property runtimePerDegreeHeatingUserDefined = newProperty(0, 0F, null); | |
public static final Property drifttimePerDegreeCooling = newProperty(0, 10.0F, null); | |
public static final Property drifttimePerDegreeHeating = newProperty(0, 10.0F, null); | |
public static final Property runtimePerDegreeCooling = newProperty(0, 10.0F, null); | |
public static final Property runtimePerDegreeHeating = newProperty(0, 10.0F, null); | |
public static final Property lastStartTime = newProperty(2, BAbsTime.NULL, null); | |
public static final Property lastStopTime = newProperty(2, BAbsTime.NULL, null); | |
public static final Property outsideTempAtBeginning = newProperty(2, new BStatusNumeric(), null); | |
public static final Property spaceTempAtBeginning = newProperty(2, new BStatusNumeric(), null); | |
public static final Property calculatedCommandTime = newProperty(66, BTime.DEFAULT, null); | |
public static final Property programMode = newProperty(66, 0, null); | |
public static final Action startTimeTrigger = newAction(4, null); | |
public static final Action stopTimeTrigger = newAction(4, null); | |
public static final Action calculate = newAction(4, null); | |
public static final Type TYPE = Sys.loadType(BOptimizedStartStop.class); | |
boolean controlModeAtBeginning; | |
boolean startDone; | |
boolean analysisComplete; | |
float observedMinutesPerDegree; | |
float spaceTempChange; | |
int leadTime; | |
int optimizedRuntimeMinutes; | |
int lastProgramMode; | |
BAbsTime now; | |
BAbsTime lastResetTime; | |
int lastMinute; | |
private static boolean ACTIVE = true; | |
private static boolean INACTIVE = false; | |
private static boolean START = true; | |
private static boolean STOP = false; | |
private static boolean DISABLED = false; | |
private static boolean ENABLED = true; | |
private static boolean COOLING = true; | |
private static boolean HEATING = false; | |
private static long TIME_00_01 = 60000L; | |
private static int NO_CALCULATION = 0; | |
private static int START_CALCULATION = 1; | |
private static int START_IN_PROCESS = 2; | |
private static int STOP_CALCULATION = 3; | |
private static int STOP_IN_PROCESS = 4; | |
Clock.Ticket ticket; | |
public static final Log ossLog = Log.getLog("kitControl.oss"); | |
public BOptimizedStartStop() | |
{ | |
this.analysisComplete = false; | |
this.observedMinutesPerDegree = 0F; | |
this.spaceTempChange = 0F; | |
this.leadTime = 0; | |
this.optimizedRuntimeMinutes = 0; | |
this.lastProgramMode = 0; | |
this.now = BAbsTime.NULL; | |
this.lastResetTime = BAbsTime.NULL; | |
this.lastMinute = 0; | |
this.ticket = null; | |
} | |
public BStatusBoolean getHeatCoolMode() | |
{ | |
return ((BStatusBoolean)get(heatCoolMode)); | |
} | |
public void setHeatCoolMode(BStatusBoolean v) | |
{ | |
set(heatCoolMode, v, null); | |
} | |
public BAbsTime getParameterResetTime() | |
{ | |
return ((BAbsTime)get(parameterResetTime)); | |
} | |
public void setParameterResetTime(BAbsTime v) | |
{ | |
set(parameterResetTime, v, null); | |
} | |
public BStatusBoolean getStartEnable() | |
{ | |
return ((BStatusBoolean)get(startEnable)); | |
} | |
public void setStartEnable(BStatusBoolean v) | |
{ | |
set(startEnable, v, null); | |
} | |
public BStatusBoolean getStopEnable() | |
{ | |
return ((BStatusBoolean)get(stopEnable)); | |
} | |
public void setStopEnable(BStatusBoolean v) | |
{ | |
set(stopEnable, v, null); | |
} | |
public BStatusBoolean getScheduleStatus() | |
{ | |
return ((BStatusBoolean)get(scheduleStatus)); | |
} | |
public void setScheduleStatus(BStatusBoolean v) | |
{ | |
set(scheduleStatus, v, null); | |
} | |
public BAbsTime getNextEventTime() | |
{ | |
return ((BAbsTime)get(nextEventTime)); | |
} | |
public void setNextEventTime(BAbsTime v) | |
{ | |
set(nextEventTime, v, null); | |
} | |
public BStatusBoolean getNextEventValue() | |
{ | |
return ((BStatusBoolean)get(nextEventValue)); | |
} | |
public void setNextEventValue(BStatusBoolean v) | |
{ | |
set(nextEventValue, v, null); | |
} | |
public BStatusNumeric getOutsideTemp() | |
{ | |
return ((BStatusNumeric)get(outsideTemp)); | |
} | |
public void setOutsideTemp(BStatusNumeric v) | |
{ | |
set(outsideTemp, v, null); | |
} | |
public BStatusNumeric getSpaceTemp() | |
{ | |
return ((BStatusNumeric)get(spaceTemp)); | |
} | |
public void setSpaceTemp(BStatusNumeric v) | |
{ | |
set(spaceTemp, v, null); | |
} | |
public BStatusBoolean getStartTimeCommand() | |
{ | |
return ((BStatusBoolean)get(startTimeCommand)); | |
} | |
public void setStartTimeCommand(BStatusBoolean v) | |
{ | |
set(startTimeCommand, v, null); | |
} | |
public BStatusBoolean getStopTimeCommand() | |
{ | |
return ((BStatusBoolean)get(stopTimeCommand)); | |
} | |
public void setStopTimeCommand(BStatusBoolean v) | |
{ | |
set(stopTimeCommand, v, null); | |
} | |
public BStatusString getMessage() | |
{ | |
return ((BStatusString)get(message)); | |
} | |
public void setMessage(BStatusString v) | |
{ | |
set(message, v, null); | |
} | |
public float getUpperComfortLimit() | |
{ | |
return getFloat(upperComfortLimit); | |
} | |
public void setUpperComfortLimit(float v) | |
{ | |
setFloat(upperComfortLimit, v, null); | |
} | |
public float getLowerComfortLimit() | |
{ | |
return getFloat(lowerComfortLimit); | |
} | |
public void setLowerComfortLimit(float v) | |
{ | |
setFloat(lowerComfortLimit, v, null); | |
} | |
public boolean getDynamicParameterAdjust() | |
{ | |
return getBoolean(dynamicParameterAdjust); | |
} | |
public void setDynamicParameterAdjust(boolean v) | |
{ | |
setBoolean(dynamicParameterAdjust, v, null); | |
} | |
public int getOldParameterMultiplier() | |
{ | |
return getInt(oldParameterMultiplier); | |
} | |
public void setOldParameterMultiplier(int v) | |
{ | |
setInt(oldParameterMultiplier, v, null); | |
} | |
public BTime getEarliestStartTime() | |
{ | |
return ((BTime)get(earliestStartTime)); | |
} | |
public void setEarliestStartTime(BTime v) | |
{ | |
set(earliestStartTime, v, null); | |
} | |
public BTime getEarliestStopTime() | |
{ | |
return ((BTime)get(earliestStopTime)); | |
} | |
public void setEarliestStopTime(BTime v) | |
{ | |
set(earliestStopTime, v, null); | |
} | |
public float getDrifttimePerDegreeCoolingUserDefined() | |
{ | |
return getFloat(drifttimePerDegreeCoolingUserDefined); | |
} | |
public void setDrifttimePerDegreeCoolingUserDefined(float v) | |
{ | |
setFloat(drifttimePerDegreeCoolingUserDefined, v, null); | |
} | |
public float getDrifttimePerDegreeHeatingUserDefined() | |
{ | |
return getFloat(drifttimePerDegreeHeatingUserDefined); | |
} | |
public void setDrifttimePerDegreeHeatingUserDefined(float v) | |
{ | |
setFloat(drifttimePerDegreeHeatingUserDefined, v, null); | |
} | |
public float getRuntimePerDegreeCoolingUserDefined() | |
{ | |
return getFloat(runtimePerDegreeCoolingUserDefined); | |
} | |
public void setRuntimePerDegreeCoolingUserDefined(float v) | |
{ | |
setFloat(runtimePerDegreeCoolingUserDefined, v, null); | |
} | |
public float getRuntimePerDegreeHeatingUserDefined() | |
{ | |
return getFloat(runtimePerDegreeHeatingUserDefined); | |
} | |
public void setRuntimePerDegreeHeatingUserDefined(float v) | |
{ | |
setFloat(runtimePerDegreeHeatingUserDefined, v, null); | |
} | |
public float getDrifttimePerDegreeCooling() | |
{ | |
return getFloat(drifttimePerDegreeCooling); | |
} | |
public void setDrifttimePerDegreeCooling(float v) | |
{ | |
setFloat(drifttimePerDegreeCooling, v, null); | |
} | |
public float getDrifttimePerDegreeHeating() | |
{ | |
return getFloat(drifttimePerDegreeHeating); | |
} | |
public void setDrifttimePerDegreeHeating(float v) | |
{ | |
setFloat(drifttimePerDegreeHeating, v, null); | |
} | |
public float getRuntimePerDegreeCooling() | |
{ | |
return getFloat(runtimePerDegreeCooling); | |
} | |
public void setRuntimePerDegreeCooling(float v) | |
{ | |
setFloat(runtimePerDegreeCooling, v, null); | |
} | |
public float getRuntimePerDegreeHeating() | |
{ | |
return getFloat(runtimePerDegreeHeating); | |
} | |
public void setRuntimePerDegreeHeating(float v) | |
{ | |
setFloat(runtimePerDegreeHeating, v, null); | |
} | |
public BAbsTime getLastStartTime() | |
{ | |
return ((BAbsTime)get(lastStartTime)); | |
} | |
public void setLastStartTime(BAbsTime v) | |
{ | |
set(lastStartTime, v, null); | |
} | |
public BAbsTime getLastStopTime() | |
{ | |
return ((BAbsTime)get(lastStopTime)); | |
} | |
public void setLastStopTime(BAbsTime v) | |
{ | |
set(lastStopTime, v, null); | |
} | |
public BStatusNumeric getOutsideTempAtBeginning() | |
{ | |
return ((BStatusNumeric)get(outsideTempAtBeginning)); | |
} | |
public void setOutsideTempAtBeginning(BStatusNumeric v) | |
{ | |
set(outsideTempAtBeginning, v, null); | |
} | |
public BStatusNumeric getSpaceTempAtBeginning() | |
{ | |
return ((BStatusNumeric)get(spaceTempAtBeginning)); | |
} | |
public void setSpaceTempAtBeginning(BStatusNumeric v) | |
{ | |
set(spaceTempAtBeginning, v, null); | |
} | |
public BTime getCalculatedCommandTime() | |
{ | |
return ((BTime)get(calculatedCommandTime)); | |
} | |
public void setCalculatedCommandTime(BTime v) | |
{ | |
set(calculatedCommandTime, v, null); | |
} | |
public int getProgramMode() | |
{ | |
return getInt(programMode); | |
} | |
public void setProgramMode(int v) | |
{ | |
setInt(programMode, v, null); | |
} | |
public void startTimeTrigger() | |
{ | |
invoke(startTimeTrigger, null, null); | |
} | |
public void stopTimeTrigger() | |
{ | |
invoke(stopTimeTrigger, null, null); | |
} | |
public void calculate() | |
{ | |
invoke(calculate, null, null); | |
} | |
public Type getType() | |
{ | |
return TYPE; | |
} | |
public void started() | |
throws Exception | |
{ | |
initClockTicket(); | |
super.started(); | |
if (!(Sys.atSteadyState())) | |
return; | |
} | |
public void stopped() | |
throws Exception | |
{ | |
if (this.ticket != null) | |
this.ticket.cancel(); | |
super.stopped(); | |
} | |
public void clockChanged(BRelTime value) | |
{ | |
initClockTicket(); | |
} | |
private void initClockTicket() | |
{ | |
if (this.ticket != null) | |
this.ticket.cancel(); | |
BAbsTime tom = Clock.nextTopOfMinute().add(BRelTime.makeSeconds(15)); | |
this.ticket = Clock.schedulePeriodically(this, tom, BRelTime.makeMinutes(1), calculate, null); | |
} | |
public void changed(Property property, Context context) | |
{ | |
super.changed(property, context); | |
if ((!(Sys.atSteadyState())) || (!(isRunning()))) | |
return; | |
boolean parameterReset = false; | |
if (property.equals(drifttimePerDegreeCoolingUserDefined)) | |
{ | |
setDrifttimePerDegreeCooling(getDrifttimePerDegreeCoolingUserDefined()); | |
parameterReset = true; | |
} | |
else if (property.equals(drifttimePerDegreeHeatingUserDefined)) | |
{ | |
setDrifttimePerDegreeHeating(getDrifttimePerDegreeHeatingUserDefined()); | |
parameterReset = true; | |
} | |
else if (property.equals(runtimePerDegreeCoolingUserDefined)) | |
{ | |
setRuntimePerDegreeCooling(getRuntimePerDegreeCoolingUserDefined()); | |
parameterReset = true; | |
} | |
else if (property.equals(runtimePerDegreeHeatingUserDefined)) | |
{ | |
setRuntimePerDegreeHeating(getRuntimePerDegreeHeatingUserDefined()); | |
parameterReset = true; | |
} | |
if (parameterReset) | |
setParameterResetTime(Clock.time()); | |
} | |
public BFacets getSlotFacets(Slot slot) | |
{ | |
return super.getSlotFacets(slot); } | |
public void doStopTimeTrigger() { | |
} | |
public void doStartTimeTrigger() { | |
} | |
private String formatNumeric(double value, String pattern) { | |
DecimalFormat format = new DecimalFormat(pattern); | |
return format.format(value); | |
} | |
public void doCalculate() | |
{ | |
this.now = Clock.time(); | |
if ((getStartEnable().getValue() == DISABLED) || (this.now.getTimeOfDayMillis() < TIME_00_01)) | |
this.startDone = false; | |
performStartStopAnalysis(); | |
performStartStopCalculation(); | |
performStartStopControl(); | |
updateControlOutput(); | |
this.lastProgramMode = getProgramMode(); | |
} | |
private void performStartStopCalculation() | |
{ | |
if (getNextEventTime().getDayOfYear() != this.now.getDayOfYear()) | |
{ | |
setProgramMode(NO_CALCULATION); | |
return; | |
} | |
if (getNextEventValue().getValue() == ACTIVE) | |
performStartCalculation(); | |
else | |
performStopCalculation(); | |
} | |
private void performStartCalculation() | |
{ | |
if ((getStartEnable().getValue() != ENABLED) || (getScheduleStatus().getValue() == ACTIVE)) | |
{ | |
setProgramMode(NO_CALCULATION); | |
return; | |
} | |
if ((!(this.startDone)) && (getSpaceTemp().getStatus().isValid())) | |
{ | |
if (getProgramMode() != START_IN_PROCESS) | |
{ | |
setProgramMode(START_CALCULATION); | |
if (getSpaceTemp().getValue() > getUpperComfortLimit()) | |
this.leadTime = (1 + (int)((getSpaceTemp().getValue() - getUpperComfortLimit()) * getRuntimePerDegreeCooling())); | |
else if (getSpaceTemp().getValue() < getLowerComfortLimit()) | |
this.leadTime = (1 + (int)((getLowerComfortLimit() - getSpaceTemp().getValue()) * getRuntimePerDegreeHeating())); | |
else | |
this.leadTime = 0; | |
} | |
} | |
else | |
this.leadTime = 0; | |
ossLog.trace(getParent().getName() + "." + getName() + "::oss start lead time = " + this.leadTime); | |
} | |
private void performStopCalculation() | |
{ | |
if ((getStopEnable().getValue() != ENABLED) || (getScheduleStatus().getValue() == INACTIVE)) | |
{ | |
setProgramMode(NO_CALCULATION); | |
return; | |
} | |
if ((!(getSpaceTemp().getStatus().isValid())) || | |
(getProgramMode() == STOP_IN_PROCESS)) | |
{ | |
this.leadTime = 0; | |
return; | |
} | |
setProgramMode(STOP_CALCULATION); | |
if (getSpaceTemp().getValue() > getLowerComfortLimit()) | |
{ | |
this.controlModeAtBeginning = getHeatCoolMode().getValue(); | |
if (getHeatCoolMode().getValue() == HEATING) | |
{ | |
this.leadTime = (int)((getSpaceTemp().getValue() - getLowerComfortLimit()) * getDrifttimePerDegreeHeating()); | |
} | |
else | |
this.leadTime = (int)((getUpperComfortLimit() - getSpaceTemp().getValue()) * getDrifttimePerDegreeCooling()); | |
} | |
ossLog.trace(getName() + "::oss stop lead time = " + this.leadTime); | |
} | |
private void performStartStopControl() | |
{ | |
if ((getProgramMode() != START_CALCULATION) && (getProgramMode() != STOP_CALCULATION)) | |
{ | |
setCalculatedCommandTime(BTime.make(getNextEventTime())); | |
return; | |
} | |
long calcCmdTime = getNextEventTime().getTimeOfDayMillis() - this.leadTime * TIME_00_01; | |
if (calcCmdTime < getEarliestStartTime().getTimeOfDayMillis()) | |
calcCmdTime = getEarliestStartTime().getTimeOfDayMillis(); | |
setCalculatedCommandTime(BTime.make(BRelTime.make(calcCmdTime))); | |
if ((getProgramMode() == STOP_CALCULATION) && (getCalculatedCommandTime().isBefore(getEarliestStopTime()))) | |
setCalculatedCommandTime(getEarliestStopTime()); | |
BTime currentTime = BTime.make(Clock.time()); | |
if ((currentTime.isAfter(getCalculatedCommandTime())) || (currentTime.isAfter(BTime.make(getNextEventTime())))) | |
{ | |
if (getProgramMode() == START_CALCULATION) | |
{ | |
this.startDone = true; | |
setProgramMode(START_IN_PROCESS); | |
setLastStartTime(Clock.time()); | |
getSpaceTempAtBeginning().setValue(getSpaceTemp().getValue()); | |
getOutsideTempAtBeginning().setValue(getOutsideTemp().getValue()); | |
startTimeTrigger(); | |
getMessage().setValue("Optimized start for " + getNextEventTime() + " schedule time. Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
} | |
else | |
{ | |
setProgramMode(STOP_IN_PROCESS); | |
setLastStopTime(Clock.time()); | |
getSpaceTempAtBeginning().setValue(getSpaceTemp().getValue()); | |
getOutsideTempAtBeginning().setValue(getOutsideTemp().getValue()); | |
this.controlModeAtBeginning = getHeatCoolMode().getValue(); | |
stopTimeTrigger(); | |
getMessage().setValue("Optimized stop for " + getNextEventTime() + " schedule time. Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
} | |
} | |
} | |
private void updateControlOutput() | |
{ | |
if (getProgramMode() == START_IN_PROCESS) | |
{ | |
getStartTimeCommand().setValue(START); | |
getStartTimeCommand().setStatusNull(false); | |
getStopTimeCommand().setValue(STOP); | |
getStopTimeCommand().setStatusNull(true); | |
} | |
else if (getProgramMode() == STOP_IN_PROCESS) | |
{ | |
getStopTimeCommand().setValue(STOP); | |
getStopTimeCommand().setStatusNull(false); | |
getStartTimeCommand().setValue(STOP); | |
getStartTimeCommand().setStatusNull(true); | |
} | |
else | |
{ | |
getStopTimeCommand().setValue(STOP); | |
getStopTimeCommand().setStatusNull(true); | |
getStartTimeCommand().setValue(STOP); | |
getStartTimeCommand().setStatusNull(true); | |
this.analysisComplete = false; | |
} | |
} | |
private void performStartStopAnalysis() | |
{ | |
if ((!(getDynamicParameterAdjust())) || (this.analysisComplete) || (!(getSpaceTemp().getStatus().isValid()))) | |
return; | |
if (this.lastProgramMode == START_IN_PROCESS) | |
handleStartAnalysis(); | |
else if (this.lastProgramMode == STOP_IN_PROCESS) | |
handleStopAnalysis(); | |
} | |
private void handleStartAnalysis() | |
{ | |
if (isCoolingAnalysis()) | |
{ | |
if ((getSpaceTemp().getValue() < getSpaceTempAtBeginning().getValue()) && (( | |
(getSpaceTemp().getValue() <= getUpperComfortLimit()) || (getScheduleStatus().getValue() == ACTIVE)))) | |
{ | |
this.spaceTempChange = (float)(getSpaceTempAtBeginning().getValue() - getSpaceTemp().getValue()); | |
this.optimizedRuntimeMinutes = getLastStartTime().delta(this.now).getMinutes(); | |
this.observedMinutesPerDegree = (this.optimizedRuntimeMinutes / this.spaceTempChange); | |
setRuntimePerDegreeCooling((getRuntimePerDegreeCooling() * getOldParameterMultiplier() + this.observedMinutesPerDegree) / (getOldParameterMultiplier() + 1)); | |
this.analysisComplete = true; | |
getMessage().setValue("Optimized start analysis done at " + this.now + ". Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
} | |
} | |
else if ((getSpaceTemp().getValue() > getSpaceTempAtBeginning().getValue()) && (( | |
(getSpaceTemp().getValue() >= getLowerComfortLimit()) || (getScheduleStatus().getValue() == ACTIVE)))) | |
{ | |
this.spaceTempChange = (float)(getSpaceTemp().getValue() - getSpaceTempAtBeginning().getValue()); | |
this.optimizedRuntimeMinutes = getLastStartTime().delta(this.now).getMinutes(); | |
this.observedMinutesPerDegree = (this.optimizedRuntimeMinutes / this.spaceTempChange); | |
setRuntimePerDegreeHeating((getRuntimePerDegreeHeating() * getOldParameterMultiplier() + this.observedMinutesPerDegree) / (getOldParameterMultiplier() + 1)); | |
this.analysisComplete = true; | |
getMessage().setValue("Optimized start analysis done at " + this.now + ". Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
} | |
} | |
private boolean isCoolingAnalysis() | |
{ | |
return (getSpaceTempAtBeginning().getValue() > getUpperComfortLimit()); | |
} | |
private void handleStopAnalysis() | |
{ | |
if (this.controlModeAtBeginning == HEATING) | |
{ | |
if ((getSpaceTemp().getValue() < getSpaceTempAtBeginning().getValue()) && (( | |
(getSpaceTemp().getValue() <= getLowerComfortLimit()) || (getScheduleStatus().getValue() == INACTIVE)))) | |
{ | |
this.spaceTempChange = ((float)getSpaceTempAtBeginning().getValue() - (float)getSpaceTemp().getValue()); | |
this.optimizedRuntimeMinutes = getLastStopTime().delta(this.now).getMinutes(); | |
this.observedMinutesPerDegree = (this.optimizedRuntimeMinutes / this.spaceTempChange); | |
setDrifttimePerDegreeHeating((getDrifttimePerDegreeHeating() * getOldParameterMultiplier() + this.observedMinutesPerDegree) / (getOldParameterMultiplier() + 1)); | |
getMessage().setValue("Optimized stop analysis done at " + this.now + ". Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
this.analysisComplete = true; | |
} | |
} | |
else if ((getSpaceTemp().getValue() > getSpaceTempAtBeginning().getValue()) && (( | |
(getSpaceTemp().getValue() >= getUpperComfortLimit()) || (getScheduleStatus().getValue() == INACTIVE)))) | |
{ | |
this.spaceTempChange = ((float)getSpaceTemp().getValue() - (float)getSpaceTempAtBeginning().getValue()); | |
this.optimizedRuntimeMinutes = getLastStopTime().delta(this.now).getMinutes(); | |
this.observedMinutesPerDegree = (this.optimizedRuntimeMinutes / this.spaceTempChange); | |
setDrifttimePerDegreeCooling((getDrifttimePerDegreeCooling() * getOldParameterMultiplier() + this.observedMinutesPerDegree) / (getOldParameterMultiplier() + 1)); | |
getMessage().setValue("Optimized stop analysis done at " + this.now + ". Space temp is " + formatNumeric(getSpaceTemp().getValue(), "#0.0") + "."); | |
this.analysisComplete = true; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Developer Note: For the `historyLog` feature to work, please add a new | |
// BStatusString slot named "historyLog" to this Program Object in Workbench. | |
// A static inner class to hold detailed historical performance data. | |
static class PerformanceRecord { | |
long timestamp; | |
double rate; // degrees per minute | |
String mode; // "HEAT" or "COOL" | |
double zoneTempStart; | |
double outdoorTempStart; | |
PerformanceRecord(long timestamp, double rate, String mode, double zoneTempStart, double outdoorTempStart) { | |
this.timestamp = timestamp; | |
this.rate = rate; | |
this.mode = mode; | |
this.zoneTempStart = zoneTempStart; | |
this.outdoorTempStart = outdoorTempStart; | |
} | |
} | |
// Member variables | |
private Clock.Ticket ticket; | |
private long startTimestamp = 0; | |
private boolean isOptimalStartRunning = false; | |
private long lastStartTriggerTimestamp = 0; | |
private static final double DEFAULT_RATE_DEG_PER_MIN = 0.1; // Default rate: 6 degrees per hour | |
// NEW: State variables for the command off-delay timer | |
private boolean isCommandActive = false; | |
private boolean isCountdownActive = false; | |
private long countdownStartTime = 0; | |
// Two separate lists to store performance history for each mode. | |
private java.util.List<PerformanceRecord> heatHistory = new java.util.ArrayList<>(); | |
private java.util.List<PerformanceRecord> coolHistory = new java.util.ArrayList<>(); | |
/** | |
* Called once when the program starts. Initializes defaults. | |
*/ | |
public void onStart() throws Exception { | |
// Set default values for configurable parameters if they are not already set | |
if (getMaxMinutesAllowed().isNull()) setMaxMinutesAllowed(new BStatusNumeric(180.0)); | |
if (getTempTolerance().isNull()) setTempTolerance(new BStatusNumeric(0.5)); | |
if (getHistoryDaysToRetain().isNull()) setHistoryDaysToRetain(new BStatusNumeric(10.0)); | |
if (getEmaWeightingFactor().isNull()) setEmaWeightingFactor(new BStatusNumeric(2.0)); | |
// CORRECTED LOGIC: Check if the value is the default 0.0, not if the slot is null. | |
// This ensures the default is set correctly on first startup. | |
if (getCommandOffDelaySeconds().getValue() == 0.0) { | |
double delayInSeconds = getMaxMinutesAllowed().getValue() * 60.0; | |
setCommandOffDelaySeconds(new BStatusNumeric(delayInSeconds)); | |
} | |
// Initialize output/status slots | |
setIsRunning(new BStatusBoolean(false)); | |
setMinutesToSetpoint(new BStatusNumeric(getMaxMinutesAllowed().getValue())); | |
setDegreesPerMinuteHeat(new BStatusNumeric(DEFAULT_RATE_DEG_PER_MIN)); | |
setDegreesPerMinuteCool(new BStatusNumeric(DEFAULT_RATE_DEG_PER_MIN)); | |
setEquipmentStartCommand(new BStatusBoolean(false)); | |
getEquipmentStartCommand().setStatus(BStatus.NULL); | |
getCountdownToNullStatus().setValue("Inactive"); | |
setFormattedStatusLog("[onStart] Optimal Start block initialized."); | |
// Initialize history log and update model to load any persistent values | |
updateHistoryLog(); | |
updateModel(); | |
setZoneAtTempTolerance(new BStatusBoolean(false)); | |
updateTimer(); | |
} | |
/** | |
* Main execution loop, called periodically by the timer. | |
*/ | |
public void onExecute() throws Exception { | |
updateTimer(); | |
updateZoneAtTempTolerance(); | |
if (getStartTimerNow().getValue()) { | |
lastStartTriggerTimestamp = System.currentTimeMillis(); | |
startOptimalStartSequence(); | |
setStartTimerNow(new BStatusBoolean(false)); | |
} | |
if (getClearHistoryNow().getValue()) { | |
clearHistory(); | |
setClearHistoryNow(new BStatusBoolean(false)); | |
} | |
if (isOptimalStartRunning) { | |
monitorActiveRun(); | |
} else { | |
updateIdleEstimate(); | |
} | |
// Always update the final equipment start command based on the latest values. | |
updateEquipmentStartCommand(); | |
} | |
/** | |
* Called once when the program is stopped. | |
*/ | |
public void onStop() throws Exception { | |
if (ticket != null) { | |
ticket.cancel(); | |
} | |
} | |
//================================================================ | |
// --- Core Logic Methods --- | |
//================================================================ | |
/** | |
* Starts a new warmup/cooldown sequence. | |
*/ | |
private void startOptimalStartSequence() { | |
if (getZoneAtTempTolerance().getValue()) { | |
setFormattedStatusLog("[Start] Skipping: Zone temp is already within tolerance."); | |
setMinutesToSetpoint(new BStatusNumeric(0.0)); | |
return; | |
} | |
if (!getZoneTemp().getStatus().isOk() || !getTargetZoneTempSetpoint().getStatus().isOk()) { | |
setFormattedStatusLog("[ERROR] Cannot start: Zone Temp or Target Setpoint not available."); | |
return; | |
} | |
startTimestamp = System.currentTimeMillis(); | |
isOptimalStartRunning = true; | |
setIsRunning(new BStatusBoolean(true)); | |
setZoneTempAtStart(new BStatusNumeric(getZoneTemp().getValue())); | |
if (getOutdoorAirTemp().getStatus().isOk()) { | |
setOutdoorTempAtStart(new BStatusNumeric(getOutdoorAirTemp().getValue())); | |
} else { | |
setOutdoorTempAtStart(new BStatusNumeric(Double.NaN)); | |
} | |
setFormattedStatusLog("[Start] Optimal Start sequence initiated."); | |
} | |
/** | |
* Monitors an active warmup/cooldown run. | |
*/ | |
private void monitorActiveRun() { | |
long now = System.currentTimeMillis(); | |
double elapsedMinutes = (now - startTimestamp) / 60000.0; | |
if (getZoneAtTempTolerance().getValue()) { | |
setFormattedStatusLog("[Monitor] Target met in " + round1(elapsedMinutes) + " minutes."); | |
stopAndRecordPerformance(elapsedMinutes); | |
return; | |
} | |
if (!getZoneTemp().getStatus().isOk() || !getTargetZoneTempSetpoint().getStatus().isOk()) { | |
setFormattedStatusLog("[Monitor] Run stopped: Lost Zone Temp or Target Setpoint."); | |
stopAndRecordPerformance(elapsedMinutes); | |
return; | |
} | |
setWarmupTimeMinutes(new BStatusNumeric(elapsedMinutes)); | |
if (elapsedMinutes >= getMaxMinutesAllowed().getValue()) { | |
setFormattedStatusLog("[Monitor] Max runtime reached after " + round1(elapsedMinutes) + " minutes."); | |
stopAndRecordPerformance(elapsedMinutes); | |
} | |
} | |
/** | |
* Stops the run and records its performance into the correct history list. | |
*/ | |
private void stopAndRecordPerformance(double actualMinutes) { | |
isOptimalStartRunning = false; | |
setIsRunning(new BStatusBoolean(false)); | |
double zoneStart = getZoneTempAtStart().getValue(); | |
double zoneNow = getZoneTemp().getValue(); | |
double delta = Math.abs(zoneNow - zoneStart); | |
if (actualMinutes <= 0.1) { | |
setFormattedStatusLog("[Record] Run was too short. Performance not recorded."); | |
return; | |
} | |
double rate = delta / actualMinutes; | |
String mode = (zoneStart < getTargetZoneTempSetpoint().getValue()) ? "HEAT" : "COOL"; | |
double outdoorStart = getOutdoorTempAtStart().getStatus().isOk() ? getOutdoorTempAtStart().getValue() : Double.NaN; | |
PerformanceRecord newRecord = new PerformanceRecord(System.currentTimeMillis(), rate, mode, zoneStart, outdoorStart); | |
if ("HEAT".equals(mode)) { | |
heatHistory.add(newRecord); | |
} else { | |
coolHistory.add(newRecord); | |
} | |
setFormattedStatusLog("[" + mode + "] run recorded. Rate: " + round1(rate) + " deg/min."); | |
updateModel(); | |
} | |
/** | |
* Updates the learned performance model using an EMA of historical data for each mode. | |
*/ | |
private void updateModel() { | |
pruneHistory(); | |
double heatRateEma = computeEmaForMode("HEAT"); | |
double coolRateEma = computeEmaForMode("COOL"); | |
if (heatRateEma > 0.0) { | |
setDegreesPerMinuteHeat(new BStatusNumeric(heatRateEma)); | |
} else { | |
setDegreesPerMinuteHeat(new BStatusNumeric(DEFAULT_RATE_DEG_PER_MIN)); | |
} | |
if (coolRateEma > 0.0) { | |
setDegreesPerMinuteCool(new BStatusNumeric(coolRateEma)); | |
} else { | |
setDegreesPerMinuteCool(new BStatusNumeric(DEFAULT_RATE_DEG_PER_MIN)); | |
} | |
updateCurrentHistoryRecordCount(); | |
updateHistoryLog(); | |
setFormattedStatusLog("[Model Updated] Heat Rate: " + round1(getDegreesPerMinuteHeat().getValue()) + ", Cool Rate: " + round1(getDegreesPerMinuteCool().getValue())); | |
} | |
/** | |
* Calculates and outputs the estimated time to reach setpoint when idle. | |
*/ | |
private void updateIdleEstimate() { | |
if (getZoneAtTempTolerance().getValue()) { | |
setMinutesToSetpoint(new BStatusNumeric(0.0)); | |
return; | |
} | |
if (!getZoneTemp().getStatus().isOk() || !getTargetZoneTempSetpoint().getStatus().isOk()) { | |
return; | |
} | |
double zone = getZoneTemp().getValue(); | |
double target = getTargetZoneTempSetpoint().getValue(); | |
double delta = Math.abs(target - zone); | |
double maxMinutes = getMaxMinutesAllowed().getValue(); | |
double estimatedMinutes = maxMinutes; | |
if (zone < target) { | |
double heatRate = getDegreesPerMinuteHeat().getValue(); | |
if (heatRate > 0.01) { | |
estimatedMinutes = delta / heatRate; | |
} | |
} else { | |
double coolRate = getDegreesPerMinuteCool().getValue(); | |
if (coolRate > 0.01) { | |
estimatedMinutes = delta / coolRate; | |
} | |
} | |
setMinutesToSetpoint(new BStatusNumeric(Math.min(estimatedMinutes, maxMinutes))); | |
} | |
//================================================================ | |
// --- Helper Methods --- | |
//================================================================ | |
/** | |
* This method now contains the final start/stop logic, including the off-delay countdown. | |
* It's called on every cycle from onExecute(). | |
*/ | |
private void updateEquipmentStartCommand() { | |
// If countdown is active, manage it and skip other logic. | |
if (isCountdownActive) { | |
int delay = getCommandOffDelayValue(); | |
long elapsed = (System.currentTimeMillis() - countdownStartTime) / 1000; | |
int remaining = (int) (delay - elapsed); | |
if (remaining < 1) { | |
getEquipmentStartCommand().setStatus(BStatus.NULL); | |
getEquipmentStartCommand().setValue(false); | |
isCommandActive = false; | |
isCountdownActive = false; | |
getCountdownToNullStatus().setValue("Delay expired -> Output = NULL"); | |
} else { | |
getCountdownToNullStatus().setValue("Countdown active -> " + remaining + "s remaining"); | |
} | |
return; // Do not evaluate inputs while counting down | |
} | |
// Determine if the core start condition is met | |
boolean startConditionMet = false; | |
// Check if all necessary inputs are valid | |
if (getScheduleNextValue().getStatus().isOk() && | |
getScheduleNextEventTime().getStatus().isOk() && | |
getMinutesToSetpoint().getStatus().isOk()) { | |
long currentTime = System.currentTimeMillis(); | |
long nextEventTime = (long) getScheduleNextEventTime().getValue(); | |
double timeToNextMinutes = (nextEventTime - currentTime) / 60000.0; | |
if (timeToNextMinutes < 0) { | |
timeToNextMinutes = 0; | |
} | |
boolean nextScheduleIsOccupied = getScheduleNextValue().getValue(); | |
double optimalStartMinutes = getMinutesToSetpoint().getValue(); | |
if (nextScheduleIsOccupied && (optimalStartMinutes >= timeToNextMinutes)) { | |
startConditionMet = true; | |
} | |
} | |
// Logic to set output or start countdown | |
if (startConditionMet) { | |
getEquipmentStartCommand().setValue(true); | |
getEquipmentStartCommand().setStatus(BStatus.ok); | |
isCommandActive = true; | |
isCountdownActive = false; // Ensure countdown is off | |
getCountdownToNullStatus().setValue("Active: Conditions Met"); | |
} else if (isCommandActive && getZoneAtTempTolerance().getValue()) { | |
// REVISED LOGIC: Only start the countdown if the command was active | |
// AND the zone has reached the temperature tolerance. | |
isCountdownActive = true; | |
countdownStartTime = System.currentTimeMillis(); | |
getCountdownToNullStatus().setValue("Entering countdown: Zone at temp."); | |
} else { | |
// Conditions are not met and the command wasn't active, so stay NULL | |
getEquipmentStartCommand().setValue(false); | |
getEquipmentStartCommand().setStatus(BStatus.NULL); | |
isCommandActive = false; | |
getCountdownToNullStatus().setValue("Inactive"); | |
} | |
} | |
/** | |
* Safely gets the off-delay value for the equipment command. | |
*/ | |
int getCommandOffDelayValue() { | |
int delay = 5400; // Default value | |
if (getCommandOffDelaySeconds().getStatus().isOk()) { | |
// Clamp value for safety | |
delay = (int) Math.max(1, getCommandOffDelaySeconds().getValue()); | |
} | |
return delay; | |
} | |
/** | |
* Sets the status log with a prepended timestamp of the last trigger. | |
*/ | |
private void setFormattedStatusLog(String message) { | |
String lastTriggerTimeStr = "never"; | |
if (lastStartTriggerTimestamp > 0) { | |
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); | |
lastTriggerTimeStr = sdf.format(new java.util.Date(lastStartTriggerTimestamp)); | |
} | |
String finalMessage = "[Last Trigger: " + lastTriggerTimeStr + "] " + message; | |
setStatusLog(new BStatusString(finalMessage)); | |
} | |
/** | |
* Continuously checks if the zone is within tolerance and updates the status slot. | |
*/ | |
private void updateZoneAtTempTolerance() { | |
if (!getZoneTemp().getStatus().isOk() || !getTargetZoneTempSetpoint().getStatus().isOk() || getTempTolerance().isNull()) { | |
setZoneAtTempTolerance(new BStatusBoolean(false)); | |
return; | |
} | |
double zone = getZoneTemp().getValue(); | |
double target = getTargetZoneTempSetpoint().getValue(); | |
double tolerance = getTempTolerance().getValue(); | |
boolean isWithinTolerance = Math.abs(zone - target) <= tolerance; | |
setZoneAtTempTolerance(new BStatusBoolean(isWithinTolerance)); | |
} | |
/** | |
* Removes old records from the performanceHistory list. | |
*/ | |
private void pruneHistory() { | |
if (getHistoryDaysToRetain().isNull()) return; | |
int maxRecords = (int) getHistoryDaysToRetain().getValue(); | |
while (heatHistory.size() > maxRecords) { | |
heatHistory.remove(0); | |
} | |
while (coolHistory.size() > maxRecords) { | |
coolHistory.remove(0); | |
} | |
} | |
/** | |
* Clears all learned history and resets the logs. | |
*/ | |
private void clearHistory() { | |
heatHistory.clear(); | |
coolHistory.clear(); | |
updateModel(); | |
setFormattedStatusLog("[History] All performance records have been cleared."); | |
} | |
/** | |
* Computes the Exponential Moving Average (EMA) for a specific mode. | |
*/ | |
private double computeEmaForMode(String mode) { | |
java.util.List<PerformanceRecord> relevantHistory = "HEAT".equals(mode) ? heatHistory : coolHistory; | |
if (relevantHistory.isEmpty()) return 0.0; | |
double[] series = new double[relevantHistory.size()]; | |
for (int i = 0; i < relevantHistory.size(); i++) { | |
series[i] = relevantHistory.get(i).rate; | |
} | |
if (series.length == 1) return series[0]; | |
return computeEMA(series); | |
} | |
/** | |
* Generic EMA calculation. | |
*/ | |
private double computeEMA(double[] series) { | |
if (series.length == 0) return 0.0; | |
double weightingFactor = 2.0; | |
if (getEmaWeightingFactor().getStatus().isOk()) { | |
weightingFactor = Math.max(1.0, Math.min(getEmaWeightingFactor().getValue(), 10.0)); | |
} | |
double k = weightingFactor / (series.length + 1.0); | |
double ema = series[0]; | |
for (int i = 1; i < series.length; i++) { | |
ema = series[i] * k + ema * (1 - k); | |
} | |
return ema; | |
} | |
/** | |
* Updates the detailed multi-line history log string and prints to console. | |
*/ | |
private void updateHistoryLog() { | |
if (getPrintToConsoleLog().getValue()) { | |
System.out.println("--- [MODEL UPDATE] Dumping Full Performance History ---"); | |
System.out.println("--- HEAT History (" + heatHistory.size() + " records) ---"); | |
if (heatHistory.isEmpty()) { | |
System.out.println("No heat records available."); | |
} else { | |
StringBuilder heatRatesArray = new StringBuilder("Heat Rates (deg/min): ["); | |
for (int i = 0; i < heatHistory.size(); i++) { | |
heatRatesArray.append(round1(heatHistory.get(i).rate)); | |
if (i < heatHistory.size() - 1) { | |
heatRatesArray.append(", "); | |
} | |
} | |
heatRatesArray.append("]"); | |
System.out.println(heatRatesArray.toString()); | |
System.out.println("---"); | |
for (int i = 0; i < heatHistory.size(); i++) { | |
PerformanceRecord r = heatHistory.get(i); | |
String logEntry = String.format("%d) Rate: %.1f | ZoneStart: %.1f | OAT: %s", | |
i + 1, r.rate, r.zoneTempStart, | |
Double.isNaN(r.outdoorTempStart) ? "N/A" : String.valueOf(round1(r.outdoorTempStart))); | |
System.out.println(logEntry); | |
} | |
} | |
System.out.println("\n--- COOL History (" + coolHistory.size() + " records) ---"); | |
if (coolHistory.isEmpty()) { | |
System.out.println("No cool records available."); | |
} else { | |
StringBuilder coolRatesArray = new StringBuilder("Cool Rates (deg/min): ["); | |
for (int i = 0; i < coolHistory.size(); i++) { | |
coolRatesArray.append(round1(coolHistory.get(i).rate)); | |
if (i < coolHistory.size() - 1) { | |
coolRatesArray.append(", "); | |
} | |
} | |
coolRatesArray.append("]"); | |
System.out.println(coolRatesArray.toString()); | |
System.out.println("---"); | |
for (int i = 0; i < coolHistory.size(); i++) { | |
PerformanceRecord r = coolHistory.get(i); | |
String logEntry = String.format("%d) Rate: %.1f | ZoneStart: %.1f | OAT: %s", | |
i + 1, r.rate, r.zoneTempStart, | |
Double.isNaN(r.outdoorTempStart) ? "N/A" : String.valueOf(round1(r.outdoorTempStart))); | |
System.out.println(logEntry); | |
} | |
} | |
System.out.println("\n--- End of History Dump ---"); | |
} | |
try { | |
BStatusString historyLogSlot = (BStatusString)get("historyLog"); | |
if (historyLogSlot == null) return; | |
StringBuilder sb = new StringBuilder(); | |
sb.append("--- HEAT History ---\n"); | |
if (heatHistory.isEmpty()) sb.append("No records.\n"); | |
for (PerformanceRecord r : heatHistory) { | |
sb.append(String.format("Rate: %.1f, ZS: %.1f, OAT: %s\n", r.rate, r.zoneTempStart, Double.isNaN(r.outdoorTempStart) ? "N/A" : String.valueOf(round1(r.outdoorTempStart)))); | |
} | |
sb.append("\n--- COOL History ---\n"); | |
if (coolHistory.isEmpty()) sb.append("No records.\n"); | |
for (PerformanceRecord r : coolHistory) { | |
sb.append(String.format("Rate: %.1f, ZS: %.1f, OAT: %s\n", r.rate, r.zoneTempStart, Double.isNaN(r.outdoorTempStart) ? "N/A" : String.valueOf(round1(r.outdoorTempStart)))); | |
} | |
historyLogSlot.setValue(sb.toString()); | |
} catch (Exception e) { | |
System.out.println("[ERROR] Failed to update historyLog slot: " + e.getMessage()); | |
} | |
} | |
/** | |
* Updates the output slot that shows the current number of historical records. | |
*/ | |
private void updateCurrentHistoryRecordCount() { | |
setCurrentHistoryRecordCount(new BStatusNumeric(heatHistory.size() + coolHistory.size())); | |
} | |
/** | |
* Reschedules the onExecute method to run again. | |
*/ | |
private void updateTimer() { | |
if (ticket != null) { | |
ticket.cancel(); | |
} | |
ticket = Clock.schedule(getComponent(), BRelTime.makeSeconds(15), BProgram.execute, null); | |
} | |
/** | |
* Utility function to round a double to one decimal place. | |
*/ | |
private double round1(double v) { | |
if (Double.isNaN(v)) return 0.0; | |
return Math.round(v * 10.0) / 10.0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
import datetime | |
class PerformanceRecord: | |
"""A classic class to hold the performance data for a single HVAC run.""" | |
def __init__(self, timestamp, rate, mode, zone_temp_start, outdoor_temp_start): | |
self.timestamp = timestamp | |
self.rate = rate | |
self.mode = mode | |
self.zone_temp_start = zone_temp_start | |
self.outdoor_temp_start = outdoor_temp_start | |
def __repr__(self): | |
"""Provides a clean string representation for printing the object.""" | |
return (f"PerformanceRecord(date={datetime.datetime.fromtimestamp(self.timestamp / 1000).strftime('%Y-%m-%d %H:%M')}, " | |
f"rate={self.rate:.2f}, mode='{self.mode}')") | |
class OptimalStartModel: | |
""" | |
A Python class that simulates the complete Niagara Optimal Start Program Object. | |
It uses classic getter/setter methods for its attributes to mirror the Java style. | |
""" | |
def __init__(self): | |
# --- Internal State Variables (like Java member variables) --- | |
self._is_optimal_start_running = False | |
self._start_timestamp = 0 | |
self._last_start_trigger_timestamp = 0 | |
self._is_command_active = False | |
self._is_countdown_active = False | |
self._countdown_start_time = 0 | |
self.DEFAULT_RATE_DEG_PER_MIN = 0.1 | |
# --- Data Histories --- | |
self._heat_history = [] | |
self._cool_history = [] | |
# --- Attributes mimicking Niagara Slots --- | |
# Inputs | |
self._zone_temp = 75.0 | |
self._target_zone_temp_setpoint = 72.0 | |
self._schedule_next_value = False | |
self._schedule_next_event_time = 0 | |
self._start_timer_now = False | |
self._clear_history_now = False | |
# Configurable Parameters | |
self._max_minutes_allowed = 180.0 | |
self._temp_tolerance = 1.0 | |
self._history_days_to_retain = 10 | |
self._ema_weighting_factor = 2.0 | |
self._command_off_delay_seconds = 5400.0 | |
# Outputs | |
self._is_running = False | |
self._minutes_to_setpoint = 0.0 | |
self._degrees_per_minute_heat = self.DEFAULT_RATE_DEG_PER_MIN | |
self._degrees_per_minute_cool = self.DEFAULT_RATE_DEG_PER_MIN | |
self._equipment_start_command = None # Using None to represent NULL status | |
self._zone_at_temp_tolerance = False | |
self._status_log = "" | |
self._countdown_to_null_status = "Inactive" | |
# --- "onStart" Equivalent --- | |
def start(self): | |
"""Initializes the model with default values, mimicking onStart().""" | |
print("[System] Block starting...") | |
# Set default for commandOffDelaySeconds based on maxMinutesAllowed | |
if self.get_command_off_delay_seconds() == 0.0: | |
delay_in_seconds = self.get_max_minutes_allowed() * 60.0 | |
self.set_command_off_delay_seconds(delay_in_seconds) | |
self.set_minutes_to_setpoint(self.get_max_minutes_allowed()) | |
self._update_model() | |
self._set_formatted_status_log("[onStart] Optimal Start block initialized.") | |
print(f"[Status] {self.get_status_log()}") | |
print("-" * 50) | |
# --- "onExecute" Equivalent --- | |
def execute(self): | |
""" | |
Runs a single execution cycle of the program logic, mimicking onExecute(). | |
""" | |
self._update_zone_at_temp_tolerance() | |
if self.get_start_timer_now(): | |
self._last_start_trigger_timestamp = int(time.time() * 1000) | |
self._start_optimal_start_sequence() | |
self.set_start_timer_now(False) | |
if self.get_clear_history_now(): | |
self._clear_history() | |
self.set_clear_history_now(False) | |
if self._is_optimal_start_running: | |
self._monitor_active_run() | |
else: | |
self._update_idle_estimate() | |
self._update_equipment_start_command() | |
# --- Helper Methods (Translated from Java) --- | |
def _update_zone_at_temp_tolerance(self): | |
zone = self.get_zone_temp() | |
target = self.get_target_zone_temp_setpoint() | |
tolerance = self.get_temp_tolerance() | |
is_within_tolerance = abs(zone - target) <= tolerance | |
self.set_zone_at_temp_tolerance(is_within_tolerance) | |
def _start_optimal_start_sequence(self): | |
if self.get_zone_at_temp_tolerance(): | |
self._set_formatted_status_log("[Start] Skipping: Zone temp is already within tolerance.") | |
self.set_minutes_to_setpoint(0.0) | |
return | |
self._start_timestamp = int(time.time() * 1000) | |
self._is_optimal_start_running = True | |
self.set_is_running(True) | |
self._set_formatted_status_log("[Start] Optimal Start sequence initiated.") | |
def _monitor_active_run(self): | |
now = int(time.time() * 1000) | |
elapsed_minutes = (now - self._start_timestamp) / 60000.0 | |
if self.get_zone_at_temp_tolerance(): | |
self._set_formatted_status_log(f"[Monitor] Target met in {elapsed_minutes:.1f} minutes.") | |
self._stop_and_record_performance(elapsed_minutes) | |
return | |
if elapsed_minutes >= self.get_max_minutes_allowed(): | |
self._set_formatted_status_log(f"[Monitor] Max runtime reached after {elapsed_minutes:.1f} minutes.") | |
self._stop_and_record_performance(elapsed_minutes) | |
def _stop_and_record_performance(self, actual_minutes): | |
self._is_optimal_start_running = False | |
self.set_is_running(False) | |
if actual_minutes <= 0.1: | |
self._set_formatted_status_log("[Record] Run was too short. Performance not recorded.") | |
return | |
# Simplified for Python simulation; in Java, this reads start/end values | |
delta = 5.0 # Assume a 5 degree change for simulation | |
rate = delta / actual_minutes | |
mode = "COOL" if self.get_zone_temp() > self.get_target_zone_temp_setpoint() else "HEAT" | |
new_record = PerformanceRecord(int(time.time() * 1000), rate, mode, 0, 0) | |
if mode == "HEAT": | |
self._heat_history.append(new_record) | |
else: | |
self._cool_history.append(new_record) | |
self._set_formatted_status_log(f"[{mode}] run recorded. Rate: {rate:.2f} deg/min.") | |
self._update_model() | |
def _update_model(self): | |
self._prune_history() | |
heat_rate_ema = self._compute_ema_for_mode("HEAT") | |
cool_rate_ema = self._compute_ema_for_mode("COOL") | |
self.set_degrees_per_minute_heat(heat_rate_ema if heat_rate_ema > 0 else self.DEFAULT_RATE_DEG_PER_MIN) | |
self.set_degrees_per_minute_cool(cool_rate_ema if cool_rate_ema > 0 else self.DEFAULT_RATE_DEG_PER_MIN) | |
self._set_formatted_status_log(f"[Model Updated] Heat Rate: {self.get_degrees_per_minute_heat():.2f}, Cool Rate: {self.get_degrees_per_minute_cool():.2f}") | |
def _update_idle_estimate(self): | |
if self.get_zone_at_temp_tolerance(): | |
self.set_minutes_to_setpoint(0.0) | |
return | |
zone = self.get_zone_temp() | |
target = self.get_target_zone_temp_setpoint() | |
delta = abs(target - zone) | |
if zone < target: # Heating | |
rate = self.get_degrees_per_minute_heat() | |
else: # Cooling | |
rate = self.get_degrees_per_minute_cool() | |
estimated_minutes = delta / rate if rate > 0.01 else self.get_max_minutes_allowed() | |
self.set_minutes_to_setpoint(min(estimated_minutes, self.get_max_minutes_allowed())) | |
def _update_equipment_start_command(self): | |
if self._is_countdown_active: | |
delay = self.get_command_off_delay_seconds() | |
elapsed = (time.time() * 1000 - self._countdown_start_time) / 1000 | |
remaining = delay - elapsed | |
if remaining < 1: | |
self.set_equipment_start_command(None) # NULL | |
self._is_command_active = False | |
self._is_countdown_active = False | |
self.set_countdown_to_null_status("Delay expired -> Output = NULL") | |
else: | |
self.set_countdown_to_null_status(f"Countdown active -> {int(remaining)}s remaining") | |
return | |
current_time = time.time() * 1000 | |
next_event_time = self.get_schedule_next_event_time() | |
time_to_next_minutes = (next_event_time - current_time) / 60000.0 | |
time_to_next_minutes = max(0, time_to_next_minutes) | |
optimal_start_minutes = self.get_minutes_to_setpoint() | |
next_schedule_is_occupied = self.get_schedule_next_value() | |
start_condition_met = next_schedule_is_occupied and (optimal_start_minutes >= time_to_next_minutes) | |
if start_condition_met: | |
self.set_equipment_start_command(True) | |
self._is_command_active = True | |
self._is_countdown_active = False | |
self.set_countdown_to_null_status("Active: Conditions Met") | |
elif self._is_command_active and self.get_zone_at_temp_tolerance(): | |
self._is_countdown_active = True | |
self._countdown_start_time = time.time() * 1000 | |
self.set_countdown_to_null_status("Entering countdown: Zone at temp.") | |
else: | |
self.set_equipment_start_command(None) # NULL | |
self._is_command_active = False | |
self.set_countdown_to_null_status("Inactive") | |
def _prune_history(self): | |
max_records = self.get_history_days_to_retain() | |
while len(self._heat_history) > max_records: | |
self._heat_history.pop(0) | |
while len(self._cool_history) > max_records: | |
self._cool_history.pop(0) | |
def _clear_history(self): | |
self._heat_history.clear() | |
self._cool_history.clear() | |
self._update_model() | |
self._set_formatted_status_log("[History] All performance records have been cleared.") | |
def _compute_ema_for_mode(self, mode): | |
history = self._heat_history if mode == "HEAT" else self._cool_history | |
if not history: | |
return 0.0 | |
series = [rec.rate for rec in history] | |
return self._compute_ema(series) | |
def _compute_ema(self, series): | |
if not series: return 0.0 | |
weighting_factor = self.get_ema_weighting_factor() | |
k = weighting_factor / (len(series) + 1.0) | |
ema = series[0] | |
for i in range(1, len(series)): | |
ema = series[i] * k + ema * (1 - k) | |
return ema | |
def _set_formatted_status_log(self, message): | |
last_trigger_str = "never" | |
if self._last_start_trigger_timestamp > 0: | |
last_trigger_str = datetime.datetime.fromtimestamp(self._last_start_trigger_timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S') | |
final_message = f"[Last Trigger: {last_trigger_str}] {message}" | |
self.set_status_log(final_message) | |
# --- Classic Getters and Setters --- | |
def get_zone_temp(self): return self._zone_temp | |
def set_zone_temp(self, value): self._zone_temp = value | |
def get_target_zone_temp_setpoint(self): return self._target_zone_temp_setpoint | |
def set_target_zone_temp_setpoint(self, value): self._target_zone_temp_setpoint = value | |
def get_schedule_next_value(self): return self._schedule_next_value | |
def set_schedule_next_value(self, value): self._schedule_next_value = value | |
def get_schedule_next_event_time(self): return self._schedule_next_event_time | |
def set_schedule_next_event_time(self, value): self._schedule_next_event_time = value | |
def get_start_timer_now(self): return self._start_timer_now | |
def set_start_timer_now(self, value): self._start_timer_now = value | |
def get_clear_history_now(self): return self._clear_history_now | |
def set_clear_history_now(self, value): self._clear_history_now = value | |
def get_max_minutes_allowed(self): return self._max_minutes_allowed | |
def set_max_minutes_allowed(self, value): self._max_minutes_allowed = value | |
def get_temp_tolerance(self): return self._temp_tolerance | |
def set_temp_tolerance(self, value): self._temp_tolerance = value | |
def get_history_days_to_retain(self): return self._history_days_to_retain | |
def set_history_days_to_retain(self, value): self._history_days_to_retain = value | |
def get_ema_weighting_factor(self): return self._ema_weighting_factor | |
def set_ema_weighting_factor(self, value): self._ema_weighting_factor = value | |
def get_command_off_delay_seconds(self): return self._command_off_delay_seconds | |
def set_command_off_delay_seconds(self, value): self._command_off_delay_seconds = value | |
def get_is_running(self): return self._is_running | |
def set_is_running(self, value): self._is_running = value | |
def get_minutes_to_setpoint(self): return self._minutes_to_setpoint | |
def set_minutes_to_setpoint(self, value): self._minutes_to_setpoint = value | |
def get_degrees_per_minute_heat(self): return self._degrees_per_minute_heat | |
def set_degrees_per_minute_heat(self, value): self._degrees_per_minute_heat = value | |
def get_degrees_per_minute_cool(self): return self._degrees_per_minute_cool | |
def set_degrees_per_minute_cool(self, value): self._degrees_per_minute_cool = value | |
def get_equipment_start_command(self): return self._equipment_start_command | |
def set_equipment_start_command(self, value): self._equipment_start_command = value | |
def get_zone_at_temp_tolerance(self): return self._zone_at_temp_tolerance | |
def set_zone_at_temp_tolerance(self, value): self._zone_at_temp_tolerance = value | |
def get_status_log(self): return self._status_log | |
def set_status_log(self, value): self._status_log = value | |
def get_countdown_to_null_status(self): return self._countdown_to_null_status | |
def set_countdown_to_null_status(self, value): self._countdown_to_null_status = value | |
# --- Main Execution Block (Simulation) --- | |
if __name__ == "__main__": | |
# 1. Create and start the model | |
model = OptimalStartModel() | |
model.start() | |
# 2. Set up the simulation scenario | |
# The next schedule event is in 60 minutes and it will be occupied. | |
model.set_schedule_next_event_time((time.time() + 3600) * 1000) | |
model.set_schedule_next_value(True) | |
# 3. Run the simulation loop | |
print("--- Starting Simulation ---") | |
for i in range(120): # Simulate for 30 minutes (120 * 15s = 1800s) | |
print(f"\n--- Cycle {i+1} ---") | |
# Simulate the environment: zone temp slowly cools down | |
current_temp = model.get_zone_temp() | |
model.set_zone_temp(current_temp - 0.1) | |
# Run the block's logic | |
model.execute() | |
# Print key outputs | |
print(f"Zone Temp: {model.get_zone_temp():.2f} F") | |
print(f"Minutes to Setpoint: {model.get_minutes_to_setpoint():.2f}") | |
print(f"Equipment Start Command: {model.get_equipment_start_command()}") | |
print(f"Countdown Status: {model.get_countdown_to_null_status()}") | |
time.sleep(1) # Pause for 1 second to make output readable | |
print("\n--- Simulation Complete ---") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment