Here’s the log immediately after I select “10 minutes” in the UI:
2020-04-21 12:15:20.275 thru 12:15:20.465
Item 'DownstairsThermostat_FanTimer_Control' received command 10
DownstairsThermostat_FanTimer_Control changed from OFF to 10
Item 'DownstairsThermostatFanMode' received command 1
DownstairsThermostatFanMode predicted to become 1
Item 'DownstairsThermostat_FanTimer_10' received command ON
DownstairsThermostatFanMode changed from 0 to 1
DownstairsThermostat_FanTimer_Status changed from to Fan autoshutoff expected at 12:25.
DownstairsThermostatStatus changed from System: Idle. Fan: Off. Humidity: 46%. Battery: 95%. to System: Idle. Fan: Off. Humidity: 46%. Battery: 95%. Fan autoshutoff expected at 12:25.
DownstairsThermostatFanStateLiteral changed from Off to On
DownstairsThermostatFanState changed from 0 to 1
And here’s the items:
// MANUAL FAN TIMERS ----------------------------------------------------------------
// This item is expecting to receive a state of OFF, 10, 30, 60 or ON from a UI control.
String DownstairsThermostat_FanTimer_Control (TimedFanControls)
// These are the expiring items used to autoshutoff the fan.
String DownstairsThermostat_FanTimer_10 (ThermostatTimers) {expire="10m,command=OFF"}
String DownstairsThermostat_FanTimer_30 (ThermostatTimers) {expire="30m,command=OFF"}
String DownstairsThermostat_FanTimer_60 (ThermostatTimers) {expire="60m,command=OFF"}
String DownstairsThermostat_FanTimer_Status (DownstairsThermostat, ThermostatFanTimerStatus)
And if y’all are really willing to dig into it, here’s the relevant rules:
rule "Downstairs Thermostat Status String"
when
Member of DownstairsThermostat changed
then
var status = ""
status += "System: " + transform("MAP", "thermostatoperatingstate.map", DownstairsThermostatOperatingState.state.toString) + ". "
status += "Fan: " + DownstairsThermostatFanStateLiteral.state + ". "
status += "Humidity: " + DownstairsThermostatHumidity.state + "%. "
status += "Battery: " + DownstairsThermostatBattery.state + "%. "
status += DownstairsThermostat_FanTimer_Status.state
DownstairsThermostatStatus.postUpdate(status)
end
rule "ThermostatFanTimerStart"
when
Member of TimedFanControls changed
then
val thermostatName = triggeringItem.name.split("_").get(0)
val fan = ThermostatFans.members.findFirst[ l | l.name == thermostatName + "FanMode" ]
val time = triggeringItem.state
if (fan === null){
logInfo("Thermostats", "Attempt to start a timed fan cycled failed. No fan item found for {}", thermostatName)
return;
}
var statusItem = ThermostatFanTimerStatus.members.findFirst[ l | l.name == thermostatName + "_FanTimer_Status" ]
if (time == "OFF"){
logInfo("Thermostats", "Turning off fan for {}.", thermostatName)
fan.sendCommand(0);
// clear the timer status string
if (statusItem === null){
logInfo("Thermostats", "Status item not found: {}{}", thermostatName, "_FanTimer_Status")
} else {
statusItem.postUpdate("")
}
} else if (time == "ON"){
logInfo("Thermostats", "Turning on fan for {}.", thermostatName)
fan.sendCommand(1);
// clear the timer status string
if (statusItem === null){
logInfo("Thermostats", "Status item not found: {}{}", thermostatName, "_FanTimer_Status")
} else {
statusItem.postUpdate("")
}
} else {
val timer = ThermostatTimers.members.findFirst[ l | l.name == thermostatName + "_FanTimer_" + time]
if (timer === null){
logInfo("Thermostats", "Attempt to start a timed fan cycle failed. No {} minute timer found for {}", time, thermostatName)
return;
}
logInfo("Thermostats", "Turning on {} (timed).", fan.name)
fan.sendCommand(1);
logInfo("Thermostats", "Tripping timer {}.", timer.name)
timer.sendCommand(ON);
ThermostatTimers
.members
.filter[i | i.name.contains(thermostatName) && ! i.name.contains(time.toString)]
.forEach[ timerItem |
if (timerItem.state.toString == "ON"){
logInfo("Thermostats", "Cancelling timer on {}", timerItem.name )
timerItem.sendCommand("OFF")
} else {
logInfo("Thermostats", "Not cancelling timer on {} because it is {}", timerItem.name, timerItem.state.toString )
}
]
}
end
rule "ThermostatFanTimerTripped"
when
Member of ThermostatTimers received command
then
//logInfo("Thermostats", "ThermostatTimer {} received command {} and its state is {}", triggeringItem.name, receivedCommand.toString, triggeringItem.state.toString)
if (receivedCommand.toString != "ON"){ return; }
val minuteDelay = Integer.parseInt(triggeringItem.name.split("_").get(2))
var shutoffTime = now.plusMinutes(minuteDelay)
val thermostatName = triggeringItem.name.split("_").get(0)
var statusItem = ThermostatFanTimerStatus.members.findFirst[ l | l.name == thermostatName + "_FanTimer_Status" ]
logInfo("Thermostats", thermostatName + " fan autoshutoff expected at " + shutoffTime.toString("HH:mm"))
statusItem.postUpdate("Fan autoshutoff expected at " + shutoffTime.toString("HH:mm") + ".")
end
rule "Trying to understand tripped timers"
when
Member of ThermostatTimers received command
then
Thread::sleep(1000)
logInfo("Thermostats", "** Reporting Thermostat: {}", triggeringItem.name)
logInfo("Thermostats", "** Reported state: {}", receivedCommand)
logInfo("Thermostats", "** Actual state: {}", triggeringItem.state.toString)
ThermostatTimers
.members
.forEach[ i |
logInfo("Thermostats", "** Timer: {} State: {}", i.name, i.state.toString )
]
end
rule "ThermostatFanTimerElapsed"
when
Member of ThermostatTimers received command OFF
then
logInfo("Thermostats", "Fan timer {} elapsed.", triggeringItem.name)
val thermostatName = triggeringItem.name.split("_").get(0)
//val timerDuration = triggeringItem.name.split("_").get(2)
val fan = ThermostatFans.members.findFirst[ l | l.name == thermostatName + "FanMode" ]
if (fan === null){
logInfo("Thermostats", "Fan timer elapsed, but fan not found for {}", thermostatName)
return;
}
//logInfo("Thermostats", "{} fan timer elapsed after {} minutes.", thermostatName, timerDuration)
fan.sendCommand(0);
// Update the proxy control item so UI will look right
val control = TimedFanControls.members.findFirst[ l | l.name == thermostatName + "_FanTimer_Control" ]
if (control === null){
logInfo("Thermostats", "Fan timer elapsed, but UI control not found for {}", thermostatName)
} else {
control.postUpdate("OFF");
}
end