My diy HVAC zoning setup

Assuming 1 means OPEN and 0 means CLOSED then:

Group:Number:SUM ventCount

and add this Group to all the Items will cause the state of VentCount to the number of Items that are OPEN. Then you don’t need the Rule at all. Oops, Bartus beat me to it.

Though I will note that it would be better to use Switches if this is the case over Numbers. you can use the same Group definition to count the number that are ON.

See Design Pattern: Working with Groups in Rules for some of the operations you can use on Groups that might be useful in these Rules.

I’m actually a little wary. There are a lot of Rules here and they are interrelated in ways that I don’t fully understand. I don’t see anything that stands out to me as a major problem, though the sleeps give me minor concern. These Rules that have the sleeps look like they don’t run all that often so you probably wont run unto (OH 1.x and OH 2.x Rules DSL only] Why have my Rules stopped running? Why Thread::sleep is a bad idea.

I think applying Design Pattern: Gate Keeper would be a good idea to provide a central place that regulates the sending of the command to the vents with the proper spacing. This would be safer if you use the Queues approach and trim out a good number of lines from the rules that send the commands.

I would also apply [Deprecated] Design Pattern: Time Of Day to create time of day zones instead of comparing against getHourOfDay so you only have to make changes in one place if you decide you need to adjust those times or base the times on other factors like sunrise or time of year or the like.

Whenever I see if conditions that have more than two or three && or || I consider if there is a way to centralize the check (see Time of Day for an example of centralizing such logic) and put that into an Item.

It isn’t clear from looking but it might be worth reviewing whether Design Pattern: How to Structure a Rule could be applied to some of these Rules to take out some of the nesting of if statements and perhaps shrink some of the code. It could be pretty useful to combine that DP with A simple scene management: finally a good use of scripts. Selecting which vents to move to which state given the over all state of your environment is essentially a scene.

I think this one in particular can benefit from How to Structure a Rule.

I would definitely use a Group for this sort of thing.

Group:Switch:OR(ON,OFF) NeedsHeat

if(NeedsHeat.state == ON)

NeedsHeat will be ON if any one of it’s members are ON and OFF if all of it’s members are OFF.

Over all, I wouldn’t be dealing with individual vents inside the Rules at all. Instead I would tag the vents with Groups. I’d then use the Rules to calculate the current desired state. Each desired state would have it’s own Group. Once I’ve determined the state it is a simple matter of sending the commands to the right Group. If you are clever with your naming (e.g. name your Groups following a pattern that includes the state), you can even centralize most of the logic.

Here is an example of what I’m describing which is how I manage my lighting.

Items

Group:Switch:OR(ON,OFF) gLights_ALL "All Lights"
        <light>

Group:Switch:OR(ON, OFF) gLights_ON
Group:Switch:OR(ON, OFF) gLights_OFF
Group:Switch:OR(ON, OFF) gLights_ON_MORNING    (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_MORNING   (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_DAY        (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_DAY       (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_AFTERNOON  (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_AFTERNOON (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_EVENING    (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_EVENING   (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_NIGHT      (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_NIGHT     (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_BED        (gLights_ON)
Group:Switch:OR(ON, OFF) gLights_OFF_BED       (gLights_OFF)
Group:Switch:OR(ON, OFF) gLights_ON_WEATHER
Group:Switch:OR(ON, OFF) gLights_WEATHER_OVERRIDE

Switch aFrontLamp "Front Room Lamp"
  (gLights_ALL, gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED, gLights_ON_WEATHER) [ "Lighting" ]
  { channel="zwave:device:dongle:node2:switch_binary" }
Switch aFrontLamp_Override "Override Cloudy FR" (gLights_WEATHER_OVERRIDE)

Switch aFamilyLamp "Family Room Lamp"
  (gLights_ALL, gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED, gLights_ON_WEATHER) [ "Lighting" ]
  { channel="zwave:device:dongle:node4:switch_binary" }
Switch aFamilyLamp_Override "Override Cloudy F" (gLights_WEATHER_OVERRIDE)

Switch aPorchLight "Front Porch"
  (gLights_ALL, gLights_OFF_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED) [ "Lighting" ]
  { channel="zwave:device:dongle:node3:switch_binary" }
Switch aPorchLight_Override "Override Cloudy Porch" (gLights_WEATHER_OVERRIDE)

I’m only showing three lights.

Rules

val logName = "lights"

// Theory of operation: Turn off the light that are members of gLights_OFF_<TOD> and
// then turn on the lights that are members of gLights_ON_<TOD>. Reset the overrides.
rule "Set lights based on Time of Day"
when
  Item vTimeOfDay changed
then
  // reset overrides
  gLights_WEATHER_OVERRIDE.postUpdate(OFF)

  val offGroupName = "gLights_OFF_"+vTimeOfDay.state.toString
  val onGroupName = "gLights_ON_"+vTimeOfDay.state.toString

  logInfo(logName, "Turning off lights for " + offGroupName)
  val GroupItem offItems = gLights_OFF.members.filter[ g | g.name == offGroupName ].head as GroupItem
  offItems.members.filter[ l | l.state != OFF ].forEach[ SwitchItem l | l.sendCommand(OFF) ]

  logInfo(logName, "Turning on lights for " + onGroupName)
  val GroupItem onItems = gLights_ON.members.filter[ g| g.name == onGroupName ].head as GroupItem
  onItems.members.filter[ l | l.state != ON].forEach[ SwitchItem l | l.sendCommand(ON) ]

end

// Thoery of operation: If it is day time, turn on/off the weather lights when cloudy conditions
// change. Trigger the rule when it first becomes day so we can apply cloudy to lights then as well.
rule "Turn on lights when it is cloudy"
when
  Item vIsCloudy changed or
  Item vTimeOfDay changed to "DAY"
then
  // We only care about daytime and vIsCloudy isn't NULL
  if(vTimeOfDay.state != "DAY" || vIsCloudy.state == NULL) return;

  // give the side effects of time of day time to complete
  if(triggeringItem.name == "vTimeOfDay") Thread::sleep(500)

  logInfo(logName, "It is " + vTimeOfDay.state.toString + " and cloudy changed: " + vIsCloudy.state.toString +", adjusting lighting")

  // Apply the cloudy state to all the lights in the weather group
  gLights_ON_WEATHER.members.forEach[ SwitchItem l |

    val overrideName = l.name+"_Override"
    val override = gLights_WEATHER_OVERRIDE.members.findFirst[ o | o.name == overrideName ]

    if(override.state != ON && l.state != vIsCloudy.state) l.sendCommand(vIsCloudy.state as OnOffType)

    if(override.state == ON) logInfo(logName, l.name + " is overridden")
  ]
end


// Theory of operation: any change in the relevant lights that occur more than five seconds after
// the change to DAY or after a change caused by cloudy is an override
rule "Watch for overrides"
when
  Member of gLights_ON_DAY changed
then
  // wait a minute before reacting after vTimeOfDay changes, ignore all other times of day
  if(vTimeOfDay.state != "DAY" || vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusMinutes(1).millis)) return;

  // Assume any change to a light that occurs more than n seconds after time of day or cloudy is a manual override
  val n = 5
  val causedByClouds = vIsCloudy.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)
  val causedByTime = vTimeOfDay.lastUpdate("mapdb").isAfter(now.minusSeconds(n).millis)

  if(!causedByClouds && !causedByTime) {
    logInfo(logName, "Manual light trigger detected, overriding cloudy control for " + triggeringItem.name)
    postUpdate(triggeringItem.name+"_Override", "ON")
  }
end

Not shown is the Time of Day, which you can find in the link above. The isCloudy Rule is also not shown but it just looks at the “current conditions” from the weather and maps that to a ON/OFF. Eventually I’ll have a light sensor control this but haven’t gotten around to it yet.

The important Rule to look at is the first one.

  • The state I calculate is the time of day.
  • Each time of day state has two Groups, one that has the lights that should be OFF at the start of the state and one that has the lights that should be ON at the start of the state
  • The rest of the Rules handle automatically turning on and off the lights when it is cloudy (again, the lights that are actually controlled this way are tagged with a Group), and handling when a light is manually changed during the day which overrides the turning on and off the light based on the weather.

Unfortunately I want to spend my time over the next few days working on the Next Gen Rules Engines documentation so I won’t be able to do too much here. And as I said, except for the duplicated code and the fact that there is so much code, I don’t see anything that gives me a lot of concern.

Some of the suggestions above can be adopted pretty easily but others will require a pretty significant restructuring of the code. So it’s up to you to decide whether it is worth it.

I will second Bartus, this is really nice work. Dealing with zoned heating like this is REALLY complicated.

It is impressive work. I’m glad you posted.

One thing I’d like to see is a simple BOM with approximate prices per vent. In my area natural gas is not that expensive and I have ~30 vents so there is definitely a return on investment concern.

2 Likes