[SOLVED] HVAC fan runtime based on min percentage rule

I have Openhab 3 setup using default persistence and have a 3M Radio Wifi Thermostat configured using the provided binding (RadioThermostat - Bindings | openHAB).

The binding allows most things to be set through items/channels including the Fan mode (ON, AUTO) and has some information regarding runtime that it is retrieving directly from the thermostat itself however this information is limited to just the current day heat/cool runtime and yesterdays heat/cool runtime.

What I want to achieve is to have my furnace’s fan mode automatically adjusted based on some min. percentage of runtime that I want it to run for…for example 50% of the time over the course of a day or last N hours. There are 2 issues though, 1) currently the fan mode runtime info is not logged so I will have to start there, 2) I have to take into account the runtime of the furnace itself somehow when determining and logging the runtime of the fan to determine what is the current % of runtime overall. For example if the fan was always in AUTO state, then the calculation is simple as it would be the same as the furnace runtime (when calling for heat) as the fan is ON during this state. Similar for when the fan is set to always ON it would simply be the runtime of the Fan on its own. However, the part I am struggling with is how to have the Fan runtime be accurately reflected so that when in AUTO state it accounts for when the furnace is running and then I can properly determine the % of runtime of the fan independently of the furnace runtime and determine whether to force the Fan into ON mode to get to min. % runtime.

Could use some guidance with this from those with more experience as I am not sure how to proceed after reviewing the docs.
thanks

It would be rather involved to track this in the binding. For simplicity why not just setup a cron rule to run every 15 minutes and toggle the fan state from AUTO to ON and vice versa? You would end up with at least 50% fan runtime per hour (15 min on, 15 min off, 15 min on, 15 min off). During times when the HVAC is running a lot, the fan will run a little extra for the times when the thermostat is calling for heat or cool during the fan off intervals.

I would think the first hurdle is clearly defining your requirement, your strategy.

That is almost certainly going to be used - even if its only by you looking at charts to review how its going.

There is a useful “cheat” when recording on/off switches in time series databases, where ON =1. Recording once per minute allows you later to easily sum all entries for a total runtime in minutes. So I would recommend a per-minute persist strategy.

Of course none of this is possible without knowing actual ON/OFF status - it’s not clear if you are able to sense that directly. If not, it is worth making a dummy Item to represent this and calculate from boiler and mode status.

Ok so my plan of attack is to create a dummy item representing the true overall fan runtime. This item will be like a simple switch and will persist every minute. Now to set this switch to on or off state I’ll create a rule that checks if either the fan is on or furnace is calling for heat or cooling (fan is on in these states) and run this rule every minute. After that I should be able to easily determine current day runtime of fan that considers furnace runtime as well and adjust accordingly. Does that make sense?
In case someone is wondering why I want to do this…I have a HRV that doesn’t have its own independent ducting which is not effective unless furnace is also actively circulating air and when my furnace does not circulate enough air either in circulatory mode or when calling for heat I get bad moisture build up on almost all my windows. This is easily eliminated by having fan run all the time to keep air movement across windows especially when really cold out. The downside to this is the electricity used when I know it does not need to be 100% on but on enough to prevent the condensation issues. This is not an issue with high humidity…humidifier is set to 23% and excess moisture is removed by HRV but again the HRV is only truly effective when entire system is circulating air due to it not having its own ducting.

So my plan is to adjust fan run time dynamically…at first simply based on desired min. % runtime and then maybe include logic that considers outside temperature as the colder it is the more air that needs to be circulated across windows to keep condensation away.

openHAB works best as an event driven system. You don’t need to check every minute - you only need to review if your switch needs changing if and when one of your inputs change.
And probably once at system startup, too.

You might need to take care about the persistence service you use here.
rrd4j does data compression - even if you store every minute, at some point in the past rrd4j compresses that to an average over 4 minutes, then further back over a longer period etc.
So you might get a value of 0.75 for a block of 4 minutes, when the fan was on for 3 and off for 1. Summing values like these up will not give you runtime.

You can select custom compression in rrd4j, for this purpose probably keep every minute for a week and just discard older, or something like. I’ve not used this.

Or use influxdb etc.

The fan status is published via the ‘fan_status’ channel. It will be 1 any time the fan is on (either because the thermostat is calling for heat/cool or the fan_mode is set to on). This could be logged to know total fan runtime.

I’ll say how I would go about solving this problem.

  • You must solve 1) first. Get that fan state logged. Once you have that, it doesn’t matter whether it was run because of AUTO or because of HEAT. You ultimately don’t really care.

  • Where this is going to become challenging is you’ll have to be fuzzy in the logic. You cannot predict when the fan will turn on in the future because of the heater or AUTO. So you’ll need a pretty wide hysteresis between when you turn the fan on and off. So rather than targeting 50%, you might have to target 45-65% or no less than 50% or something like that. The latter would be actually the easiest to implement.

  • The fact that the fan will turn on and off independently of this rule adds additional complexity. What do you do if the fan needs to run some more but it’s already running because the heater is on or because of some other reason? What happens if you tell it to turn the fan OFF when the heater is ON (hopefully it won’t let you do that)?

So, assuming you have the fan state logged once a minute as a 1 when on you can write a rule as something like this. Trigger it once every five minutes or so.

Rule DSL (because dealing with persistence is easier)

val hours = 6
val mins = 60 * 60
val runtime = FanState.sumSince(now.minusHours(hours);
if(runtime === null) {
    // log error
    return;
}
if(runtime < mins*.45)  {
    // turn on fan if fan isn't already ON
}
else if(runtime < mins*.5) {
    // turn off the fan if the fan isn't on for some other reason
}

If turning off the fan when it’s running for some other reason like the heater doesn’t do anything, you can just send command OFF in the else if.

You’d need to experiment with the hysteresis. I’ve currently set it to 5%. The hysteresis may need to change with the number of hours used (fewer hours needs a wider range).

Unfortunately this fan status is what is captured and reported by thermostat and does not change status when furnace is calling for heat or cooling. In other words it simply reports based on the fan mode which isn’t very useful.

I finally found some time to come back to this considering everyone’s comments. I created a contact item to represent the blower fan status so far and created a rule that updates this based on changes to either the fan mode or if furnace is actively calling for heat or cooling. I’ll post the rule once I get back in front of computer.
I had some challenges getting it to work and getting openhab to acknowledge the rule…was using # for comments in rule file instead of // duh.
Anyway I will have to tweak this rule to run at start up too to evaluate starting state. Next up to look at persistence and rule to adjust fan in a sane manner. Fyi the fan mode can either be ON or AUTO (fan activated only when needed for heat and cooling). I cannot set to OFF and override system requirements when in heating or cooling state and risk damaging system.

Which RadioThermostat do you have? There seem to be firmware differences between models. I have both a CT-30 and a CT-50 (3M-50). I just checked and the CT-30 does not update the ‘fstate’ value to 1 while the heat is on. However my CT-50 does update fstate. It goes to 1 while the thermostat is calling for heat and even stays as 1 during the short time that the blower stays on after the call for heat is satisfied.

I actually have a 3M branded model…3M Filtrete which is 3M50. Although I purchased it probably back around 2009/10 so maybe firmware has changed. Wounder if firmware can be updated on these things? The way your CT50 functions is the way I’d expect it to be as I don’t see a useful use case where fstat doesn’t report fan status independently.

The thermostat that I have that does report the fstate properly is a 3M50. Here is a sample of what the raw json output looks like on the pertinent endpoints. The first shows when the thermostat is calling for heat. It indicates tstate:1 for heat on and fstate:1 for the fan being on.

/tstat
{“temp”:65.50,“tmode”:1,“fmode”:0,“override”:1,“hold”:1,“t_heat”:67.00,“tstate”:1,“fstate”:1,“time”:{“day”:3,“hour”:18,“minute”:59},“t_type_post”:0}

/tstat/model
{“model”:“CT50 V1.09”}

/sys
{“uuid”:“xxxxxxxxxxxx”,“api_version”:113,“fw_version”:“1.04.84”,“wlan_fw_version”:“v10.105576”}

To update the firmware you just have to connect/provision to the RadioThermostat cloud service. Once the thermostat is communicating with the cloud it will update to the latest firmware automatically. Once that is done you can reset the thermostat to disconnect it from the cloud service if desired.

yup that is strange, we have the same thermostat. I had read some other thread that the firmware is actually within the wifi or zwave modules and not the thermostat itself. Anyway, same model and same firmware version and api versions…I don’t know what to make of it.

/tstat/model
{“model”:“CT50 V1.09”}

/sys
{“uuid”:“xxxxxxxxxxxxxxxx”,“api_version”:113,“fw_version”:“1.04.84”,“wlan_fw_version”:“v10.105576”}

next time it calls for heat I am going to inspect the raw json reported by the thermostat to see it actually is updating the fstate

here is my rule to dynamically determine furnace blower state, used fan mode instead of fan state to reduce the delay a bit.

rule "Set HVAC blower fan status based on fan mode and furnace status"
when
    System started or
    Item RadioThermostat3M50_FanMode changed or
    Item RadioThermostat3M50_Status changed
then
    if (RadioThermostat3M50_FanMode.state == 2) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
   else if (RadioThermostat3M50_Status.state != 0) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
    else {
	RadioThermostat3M50_BlowerStatus.postUpdate(0)
    }
end

from my understanding of the default persistence db I should be fine with this as I only plan to try to reach a % of fan run time by adjusting within current day or a sliding 24h window maybe.

default rrd4j for default_numeric (like contact items)

It defines 5 archives:

  1. granularity of 10s for the last hour
  2. granularity of 1m for the last week
  3. granularity of 15m for the last year
  4. granularity of 1h for the last 5 years
  5. granularity of 1d for the last 10 years

So I think I just have to piece together the logic part to adjust the fan based on the available information without causing it to be constantly flipped on and off…

1 Like

I went ahead and put in a change to the binding code to report the fan status as 1 any time the thermostat is calling for heat or cool. But it won’t be available before the next version of openHAB comes out.

Just realized you are the author of this binding! Thanks for reporting bug and putting together this binding. Prior to this binding I only had something hacked together to change temperature essentially…my setup was certainly a mess and less than ideal. I’ll keep my work around for now and once upgrade comes further down the road I’ll switch then.

1 Like

more time to finalize this…I have decided to use a dimmer item to be able to change the level of circulation of the furnace fan and incorporate this in the rules. However, I have messed something up as the rule never fully initializes (openhab always shows that is being initialized)

here is my code for the rules…

val hours = 6
val mins = 60 * hours

rule "Set HVAC blower fan status based on fan mode and furnace states"
when
    System started or
    Item RadioThermostat3M50_FanMode changed or
    Item RadioThermostat3M50_Status changed
then
    if (RadioThermostat3M50_FanMode.state == 2) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
   else if (RadioThermostat3M50_Status.state != 0) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
    else {
	RadioThermostat3M50_BlowerStatus.postUpdate(0)
    }
end

rule "Dynamically adjust furnace fan based on min circulation level"
when
    System started or
    Item RadioThermostat3M50_FanCMode changed or
    Time cron "*/20 * * * *"
then
    val runtime = RadioThermostat3M50_BlowerStatus.sumSince(now.minusHours(hours));
    if (runtime === null) {
        if (RadioThermostat3M50_FanMode.state != AUTO) {
            RadioThermostat3M50_FanMode.sendCommand(AUTO)
        }
	// log error
        return;
    }
    if (runtime < mins*(RadioThermostat3M50_FanCMode.state/100)) {
        // turn on fan if fan isn't already ON
        if (RadioThermostat3M50_FanMode.state != ON) {
            RadioThermostat3M50_FanMode.sendCommand(ON)
        }
    }
    else {
	// turn off the fan if the fan isn't on for some other reason
        if (RadioThermostat3M50_FanMode.state != AUTO) {
            RadioThermostat3M50_FanMode.sendCommand(AUTO)
        }
    }
end

Alright, I got it working finally! This rule has been working all day and seems to be adjusting the fan runtime accordingly. I had to switch ON/AUTO for their respective numbers instead…I guess I don’t have the mapping thing working which I thought would automatically take place with the use of the binding but it does not seem to be the case.

The other thing I would like to do but does not work is have val mins be calculated as 60 * hours. Wanted to do it like that so that I can easily introduce another item that would allow me to easily adjust history duration or slightly reduce number of changes required if I wanted to change the number of historical hours to look at. I probably won’t bother…but still would like to know why val mins could not be calculated with reference to val hours.

With the blower adjustment being assessed only every 20 mins I think I am good with respect the possibility of any frequent flip flopping of the fan.

val hours = 6
val mins = 60 * 6

rule "Set HVAC blower fan status based on fan mode and furnace states"
when
    System started or
    Item RadioThermostat3M50_FanMode changed or
    Item RadioThermostat3M50_Status changed
then
    if (RadioThermostat3M50_FanMode.state == 2) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
   else if (RadioThermostat3M50_Status.state != 0) {
        RadioThermostat3M50_BlowerStatus.postUpdate(1)
    }
    else {
	RadioThermostat3M50_BlowerStatus.postUpdate(0)
    }
end

rule "Dynamically adjust furnace fan based on min circulation level"
when
    System started or
    Item RadioThermostat3M50_FanCMode changed or
    Time cron "0 */20 * * * ?"
then
    val runtime = RadioThermostat3M50_BlowerStatus.sumSince(now.minusHours(hours))
    if (runtime === null) {
        if (RadioThermostat3M50_FanMode.state != 0) {
            RadioThermostat3M50_FanMode.sendCommand(0)
        }
	// log error
       logErro	r("heati	ng", "Bl	ower run	time returned null")
        return;
    }
    if (runtime < mins*((RadioThermostat3M50_FanCMode.state as Number)/100)) {
        // turn on fan if fan isn't already ON
        if (RadioThermostat3M50_FanMode.state != 2) {
            RadioThermostat3M50_FanMode.sendCommand(2)
        }
    }
    else {
        // turn off the fan if the fan isn't on for some other reason
        if (RadioThermostat3M50_FanMode.state != 0) {
            RadioThermostat3M50_FanMode.sendCommand(0)
        }
    }
end

“Global” variables in DSL rules file are not truly global. You can’t see them from other rules files, not too surprising. Less obviously, you can’t see them from the context outside of rules at all.

val x = 1
val y = x + 1

will fail in the outside-of-any-rule context, x is not available for the y evaluation.

val x = 1
val y = 0

rule “test”

y = x+1

will work just fine - all globals are available within a rule body.