My diy HVAC zoning setup

As requested, I will be writing up a fairly thorough post with my HVAC zoning setup. ( @vzorglub )

This was partially inspired by Keen vents. When I first saw them come up on kickstarter or wherever they started I thought it was a killer idea. At 80 USD per vent, though, I didn’t see them in my future. Additional inspiration came in the form of twins coming our way in mid 2016. I wanted to keep the nursery warm without heating the rest of the house needlessly. Previously, the thermostat would be set quite low overnight since we were sleeping. The furnace had no trouble warming the house back up in the early morning hours as we would need to get up and leave. Remembering seeing the Keen vents I wondered if maybe there was a way I could do something like that. This also coincided with me starting to gain interest in Arduinos for the first time. I’ve had 0 programming experience, but, I’m a curious person and love to learn new things. Between about September 2016 and February 2017 I spent a lot of time learning and experimenting with Arduinos and servos and plenty of time was spent scouring the web for a way to adapt a servo to a typical house register/vent.

My major breakthroughs in this came probably in December and January where I was finally able to actuate a servo based on simple inputs and it would sweep from what would be an open position to a closed position. As I would need a way to control these remotely I was also starting to learn about openHAB and the esp8266 about this time. As time moved on further web searches revealed to me a product called the “Vent Miser.” It seemed to be a failed product of the mid-2000s that incorporated a simple digital timer, a motor, and a vent. Finally I had a vent that I could adapt. Initially I managed to mate a micro servo to vent control but that eventually was replaced with the original motor when I learned to use a dual H bridge.

The next challenge was wirelessly controlling the vents. I was becoming somewhat able to do this via my own Arduino coding when I stumbled on a project called “ESPEasy” https://www.letscontrolit.com/wiki/index.php/ESPEasy This project is an easy-to-use firmware that can be loaded onto esp8266 devices and allows for a vast set of devices to be plugged into it as well as a web interface for controlling it all. Hallelujah! I had also found a rudimentary control scheme for openhab by @ben_jones12 : Virtual thermostat
It taught me a lot; enough to get my initial setup up and running around February 2017. I created several esp8266+ds18b20 battery powered sensors and placed them in various rooms and used those initially to control the vents in the master bedroom and nursery. I was able to command our Nest on and off and open and close a couple vents to regulate temperatures.

Since those first two servo-controlled vents I’ve expanded control to just about every vent in the house. I’m also no longer using arduinos that need plugged in at each vent. I ran wires back to just a couple central controllers that handle 6 vents each and still run ESPEasy. Commands are sent/received over MQTT. Each room has it’s own virtual thermostat. Some have lightswitches replaced by Nextion displays that allow control over those thermostats as well as the lights. There are a few wall mounted tablets in the house that also allow for manipulation of the room temps.

Now instead of fooling with the Nest, I have an 8-relay board mounted in the furnace that takes over control in the evening to heat/cool the bedrooms as needed. I can lock out certain furnace fan speeds and decide based on how many vents are open which fan speeds to use. I have temperature probes located in a few areas of the furnace to be sure temperatures don’t get to low or high and I can select high and low gas valve positions.

All of this has been working reliably for coming up on 2 years now. I continue to improve the code and add “features” to the rules that govern everything. A lot of the rule cleanup credit goes to @rlkoshak and a couple others. Some of the rules are old and haven’t received the same cleanup so the rules are a mix of new/old techniques that I’ve learned.

Not all rules will be posted in their entirety to save on space, but you should be able to get the idea.

That will come next. That’s all I have time to write up for this moment.

6 Likes

Here is a sampling of the temperature and setpoint items. There is a temperature item for the room as well as a proxy. One is a UoM item and one is a regular decimal type. I find it easier to deal with the decimaltype when doing math. An additional benefit: it allowed me to make a daytime change to the setpoint. As an example, lets say I want to keep the nursery at 69 degrees at night, but, during the day when the furnace is more active I want to set it higher. Well, because I have a proxy for the setpoint, I can add a couple degrees and then set it back easily without a savestate/restorestate setup. I keep all the temperature and setpoint items in a single file for easy editing.

Temperature Items:

Group   gRoomSp
Group   gRoomTemps
Group   ggRoomTempsPx
Group   gRoomTempsPx
Group   ggRoomSpPx
Group   gRoomSpPx

Number:Temperature	masterTemperature 		"Master Bedroom [%.1f %unit%]"			<temperature>	(gMainTemps,gTemps,gRoomTemps)		[ "CurrentTemperature" ]	{ mqtt="<[oh2:/MasterSensor/Master_Temp/MBD_TEMP:state:default]", expire="20m,-1" }
Number				masterTemperaturePx																(gRoomTempsPx)
Number:Temperature	nurseryTemperature		"Nursery [%.1f %unit%]"					<temperature>	(gMainTemps,gTemps,gRoomTemps)		[ "CurrentTemperature" ]	{ mqtt="<[oh2:/garage/nursery/nurTMP:state:default]", expire="20m,-1" }
Number				nurseryTemperaturePx	"Nursery Temperature [%.1f °F]"								(gRoomTempsPx)

The [ CurrentTemperature ] tag is for google home integration. The groups that they’re in will become something of focus as I continue to detail the setup.

Setpoint items:

Number:Temperature	masterSetpoint			"Master Bed Setpoint [%.1f %unit%]"		<temperature>	(masterThermostat,gRoomSp)			[ "TargetTemperature" ]
Number				masterSetpointPx																(gRoomSpPx)
Number:Temperature	nurserySetpoint			"Nursery Setpoint [%.1f %unit%]"		<temperature>	(nurseryThermostat,gRoomSp)			[ "TargetTemperature" ]
Number				nurserySetpointPx																(gRoomSpPx)

When one of the ESP units phones home and updates its temperature, this rule runs to update the temperature proxy item:

rule "Room Temperatures Update Proxy"
when
	Member of gRoomTemps changed
then
	val namePx = triggeringItem.name + "Px"
	var Number temp = (triggeringItem.state as QuantityType<Number>).doubleValue
	sendCommand(namePx, temp.toString)
	if ( triggeringItem.name.contains("attic") )
		atticTempN.sendCommand(atticTemperaturePx.state as DecimalType)
	if ( triggeringItem.name.contains("corner") )
		cornerTempNXT.sendCommand(cornerTemperaturePx.state as DecimalType)
end

The two rooms called out in the above rule have a Nextion display in place of a traditional light switch. Those lines update the temperature displayed on the screen by sending it to the esp8266 that they’re attached to.

When the setpoint item changes via the sitemap, phone app, habpanel, etc, this rule updates the setpoint proxy. There’s a timer that delays changing the proxy so that if you’re hamming the up/down button it doesn’t needlessly change things until you’re done adjusting. And again, two rooms are sent a value to be displayed.

rule "Room Setpoints Update Proxy"
when
	Member of gRoomSp received command
then
	val namePx = triggeringItem.name + "Px"
	var Number setpoint = (triggeringItem.state as QuantityType<Number>).doubleValue
	createTimer(now.plusSeconds(5), [|
		sendCommand(namePx, setpoint.toString)
		if ( triggeringItem.name.contains("attic") )
			sendCommand(atticSetpointN, setpoint.toString)
		if ( triggeringItem.name.contains("corner") )
			sendCommand(cornerSetPointNXTOut, setpoint.toString)
		spPxTimer = null
	])
end

This rule increases the setpoints during the day by 2 degrees:

rule "Upper Rooms Increase/Decrease Day temps"
when
	Time cron "0 0 7 ? * * *"
then
	if ( NestTStat_HVAC_Mode.state == "HEAT" ) 
		gRoomSpPx.members.forEach[t| t.sendCommand(t.state as DecimalType + 2) ]
	if ( NestTStat_HVAC_Mode.state == "COOL" ) 
		gRoomSpPx.members.forEach[t| t.sendCommand(t.state as DecimalType - 2) ]
end

This rule resets the setpoints in the evening to resume their night time temperatures:

rule "Rooms Reset Setpoints"
when
	Time cron "0 0 18 ? * * *"
then
	gRoomSp.members.forEach[t | t.sendCommand(t.state as QuantityType<Number>)]
end

Because the setpoints are being sent a command they trigger update proxy rule above so that anything that needs informed of the change will be made aware.

I think that’s mostly it for the setpoints/temps. There’s a rule in another file that triggers from a system start to resync the setpoints if for some reason openhab is restarted in the middle of the day.

This post details the items and rules that deal with opening and closing the vents in the rooms. For a time I had issues with various things not working so I had some error checking on whether the esp8266s received the command or not so you’ll see some of that in here. It’s not much of a problem these days and I could probably remove it. When a vent is commanded opened or closed the esp will reply whether it received the command or not and I would use this to set a switch on/off as an error. I suppose I should have used a contact.

Again, there are several groups at play here. The reason being: when heating bedrooms at night I may want to open/close other rooms in the house that would be different than when I’m cooling the house. Some of these groups aren’t necessary as I’m not really using them anymore but I may pick that up again. I also have a mode that closes the bedrooms and focuses on cooling the main parts of the house if there are a lot of people over and it’s hot and I know the doors in/out of the house will be opened often. Since the bedrooms are unoccupied I prefer to focus the cooling on the large rooms. To my (sort of) surprise, this actually worked pretty well. The house was built in the 70s when there wasn’t a lot of focus on energy efficiency and duct sealing, so I have to do things that might seem silly to get my desired results.

Since I allow some rooms outside of the bedrooms to call for extra heat/cool (basement, and a room I call the fireplace room) I need to have groups of vents that will be closed (restricted) and open. So you’ll see groups like: gUVR (upper vent restrict), gPtRC (party time restrict cooling), gPtRH (party time restrict heating).

Group	gVent
Group	gVentAck
Group	gVentError
Group	gVentMode
Group	gRoomNeedCool
Group	gRoomNeedHeat
Group	gRoomUseHVAC
Group	gVentTrigger
Group	atticThermostat			"Attic Thermostat"														[ "Thermostat" ]
Group	cornerThermostat		"Corner Thermostat"														[ "Thermostat" ]
Group	fireplaceThermostat		"Fireplace Thermostat"													[ "Thermostat" ]
Group	foyerThermostat			"Foyer Thermostat"														[ "Thermostat" ]
Group	masterThermostat		"Master Thermostat"														[ "Thermostat" ]
Group	nurseryThermostat		"Nursery Thermostat"													[ "Thermostat" ]

Number	masterVent				"Master Bed Vents"				<fire>			(gVent)												{ mqtt=">[oh2:mastervents:state:*:default]" }
Number	masterVentMode			"Master Bed Vent Control"		<heating>		(gVentMode,gUVR,gFVRH,gPtRH,gPtRC)
Number	masVentAck				"Master Vent Ack [%.1f]"						(gVentAck)											{ mqtt="<[oh2:/GarageResponse/mv/ack:state:default]" }
Number	nurseryVent				"Nursery Bed Vent"				<fire>			(gVent)												{ mqtt=">[oh2:nurseryvent:state:*:default]" }
Number	nurseryVentMode			"Nursery Vent Control"			<heating>		(gVentMode,gPtRC,gPtRH,gFVRH)
Number	nurVentAck				"Nursery Vent Ack [%.1f]"						(gVentAck)											{ mqtt="<[oh2:/GarageResponse/nuv/ack:state:default]" }

Switch	masterNeedCool			"Master Cool Request"			<fan>			(gRoomNeedCool)
Switch	masterNeedHeat			"Master Heat Request"			<fire>			(gRoomNeedHeat)
Switch	masterStatUseHVAC		"Master Use HVAC"								(gRoomUseHVAC)
Switch	masterVentTrigger		"Master Bed Vents"				<fan>			(gVentTrigger)
Switch	masVentError			"Master Vent Error"                             (gVentError)
Switch	nurseryNeedCool			"Nursery Cool Request"			<fan>			(gRoomNeedCool)
Switch	nurseryNeedHeat			"Nursery Heat Request"			<fire>			(gRoomNeedHeat)
Switch	nurseryStatUseHVAC		"Nursery Use HVAC"								(gRoomUseHVAC)
Switch	nurseryVentTrigger		"Nursery Vent Trigger"			<fire>			(gVentTrigger)
Switch	nurVentError			"Nursery Vent Error"                            (gVentError)

The -StatUseHVAC are switches used to call for additional heating/cooling. There’s an actual vent item and a trigger. This allows me to reset the trigger every time a temperature or setpoint changes to be sure the trigger is right (on/off) without sending an actual command to the ESP every time (reducing motor wear).

This rule handles the sitemap/phone app/etc simple on/off of the vent:

rule "Vent Mode Control On / Off"
when
	Member of gVentMode received command 0 or
	Member of gVentMode received command 1
then
	sendCommand(triggeringItem.name.replace("VentMode","VentTrigger"),if (receivedCommand == 1) "ON" else "OFF")
	if ( triggeringItem.name == "atticVentMode" ) {
		atticVentN.sendCommand(receivedCommand)
		atticAutoN.sendCommand(0)
	}
	if ( triggeringItem.name == "cornerVentMode" ) {
		cornerAutoNXTOut.sendCommand(0)
		cornerVentNXTOut.sendCommand(receivedCommand)
	}
end

This rule handles opening/closing of the vent based on temperature/setpoint changes. It also makes the call for heating or cooling:

rule "Vent Control via Temperature"
when
	Member of gRoomTempsPx changed or
	Member of gRoomSpPx changed or
	Member of gVentMode changed or
	Member of gRoomUseHVAC changed
then
	val baseName = triggeringItem.name.replace("VentMode","").replace("SetpointPx","").replace("TemperaturePx","").replace("StatUseHVAC","")
	val setpointPx = gRoomSpPx.members.findFirst[ i | i.name == baseName+"SetpointPx" ]
	val ventMode = gVentMode.members.findFirst[ i | i.name == baseName+"VentMode" ]
	val ventTrigger = gVentTrigger.members.findFirst[ i | i.name == baseName+"VentTrigger" ]
	val tempPx = gRoomTempsPx.members.findFirst[ i | i.name == baseName+"TemperaturePx" ]
	val useHVAC = gRoomUseHVAC.members.findFirst[ i | i.name == baseName+"StatUseHVAC" ]
	val needCool = gRoomNeedCool.members.findFirst[ i | i.name == baseName+"NeedCool" ]
	val needHeat = gRoomNeedHeat.members.findFirst[ i | i.name == baseName+"NeedHeat" ]
        	var Number useFur = 0
	var Number useNxt = 0
	if ( vacation != ON ) {
	if ( baseName == "livingroom" || baseName == "kitchen" || baseName == "basement" || baseName == "foyer" )
		useFur = 0
		else
			useFur = 1
	if ( vacation != ON ) {
	if ( triggeringItem.name == "atticVentMode" && ventMode.state == 2) {
		atticAutoN.sendCommand(1)
		atticVentN.sendCommand(1)
	}
	if ( triggeringItem.name == "cornerVentMode" && ventMode.state == 2 ) {
		cornerAutoNXTOut.sendCommand(1)
		cornerVentNXTOut.sendCommand(1)
	}
	if ( triggeringItem.name == "atticStatUseHVAC" && ventMode.state == 2 && useHVAC.state == 1 ) {
		atticFurnaceN.sendCommand(1)
	} else if ( triggeringItem.name == "atticStatUseHVAC" ) {
		atticFurnaceN.sendCommand(0)
	}
	if ( triggeringItem.name == "cornerStatUseHVAC" && ventMode.state == 2 && useHVAC.state == 1 ) {
		cornerFurnaceNXTOut.sendCommand(1)
	} else if ( triggeringItem.name == "cornerStatUseHVAC" ) {
		cornerFurnaceNXTOut.sendCommand(0)
	}
	if ( ventMode.state == 2 && NestTStat_HVAC_Mode.state.toString == "HEAT" ) {
		logInfo("vent heating mode",triggeringItem.name)
		logInfo("vent heating temp",tempPx.state.toString)
		logInfo("vent heating SP",setpointPx.state.toString)
		if ( tempPx.state <= (setpointPx.state as DecimalType - 1) ) {
				ventTrigger.sendCommand(ON)
			if ( useFur == 1 )
				if ( useHVAC.state == ON && needHeat.state != ON )
					needHeat.sendCommand(ON)
		}
		if ( tempPx.state >= (setpointPx.state as DecimalType + 1) ) {
				ventTrigger.sendCommand(OFF)
			if ( useFur == 1 )
				if ( useHVAC.state == ON && needHeat.state != OFF )
					needHeat.sendCommand(OFF)
		}
		if ( furnaceRunState.state != "OFF" )
			if ( tempPx.state < (setpointPx.state as DecimalType + 0.3) )
					ventTrigger.sendCommand(ON)
	}
	if ( ventMode.state == 2 && NestTStat_HVAC_Mode.state.toString == "COOL" ) {
		if ( tempPx.state >= (setpointPx.state as DecimalType + 0.5) ) {
				ventTrigger.sendCommand(ON)
			if ( useFur == 1 )
				if ( useHVAC.state == ON && needCool.state != ON )
					needCool.sendCommand(ON)
		}
		if ( tempPx.state <= (setpointPx.state as DecimalType - 0.5) ) {
				ventTrigger.sendCommand(OFF)
			if ( useFur == 1 )
				if ( useHVAC.state == ON && needCool.state != OFF )
					needCool.sendCommand(OFF)
		}
		if ( furnaceRunState.state != "OFF" )
			if ( tempPx.state > (setpointPx.state as DecimalType - 0.3) )
					ventMode.sendCommand(ON)
	}
	if ( useFur == 1) {
		if ( ventMode.state !=2 || useHVAC.state == OFF ) {
			if (NestTStat_HVAC_Mode.state.toString == "HEAT" && needHeat.state != OFF )
				needHeat.sendCommand(OFF)
			if ( NestTStat_HVAC_Mode.state.toString == "COOL" && needCool.state !=OFF )
				needCool.sendCommand(OFF)
		}
	}
	}
end

These rules used to be 100s of lines longer. I’m sure they could probably be further shortened, but this is where they’re at for now and they work. Depending on the mode of the Nest thermostat the vents open and close with a 1 degree “setback” or hysteresis.

The ventmodes are 0 = off, 1 = on, 2 = auto. There are additional lines in there that will open a vent if the furnace kicks on while the vent is closed but still above/below it’s setpoint. So, lets say we’re heating and a room was set to 72 and at 73 degrees it closed its vent. The room has fallen to 72 and the vent has still closed. The furnace just turned on, but that vent is still closed. It won’t open until the room falls to 71. There a few lines in the rule that take care of that. Should the furnace turn on, the vent will reopen until it again reaches 73.

This rule uses the vent triggers to send commands to the esp8266 units and set an error if need be:

rule "Room Vent Actuation"
when
	Member of gVentTrigger received command
then
	val baseName = triggeringItem.name.replace("VentTrigger","")
	val vent = gVent.members.findFirst[ i | i.name == baseName+"Vent" ]
	val ackBase = triggeringItem.name.substring(0,3)
	val ack = gVentAck.members.findFirst[ i | i.name == ackBase+"VentAck" ]
	val ventError = gVentError.members.findFirst[ i | i.name == ackBase+"VentError" ]
	if ( receivedCommand == ON && vent.state != 1 ) {
		vent.sendCommand(1)
		createTimer(now.plusSeconds(5), [|
		if ( ack.state == 0 ) {
			vent.sendCommand(1)
			createTimer(now.plusSeconds(5), [|
			if ( ack.state == 0 ) {
				ventError.sendCommand(ON)
			}
			])
		} else {
			ack.sendCommand(0)
			ventError.sendCommand(OFF)
		}
		])
	}	
	if ( receivedCommand == OFF && vent.state != 0 ) {
		vent.sendCommand(0)
		createTimer(now.plusSeconds(5), [|
		if ( ack.state == 0 ) {
			vent.sendCommand(0)
			createTimer(now.plusSeconds(5), [|
			if ( ack.state == 0 ) {
				ventError.sendCommand(ON)
			}
			])
		} else {
			ack.sendCommand(0)
			ventError.sendCommand(OFF)
		}
		])
	}
end

The next posts will detail the furnace control rules and additional vent control rules. That’ll have to wait until tomorrow

Awesome write-up and design! Will you be posting the hardware (H-bridge/ventmiser) setup, too? I remember you posting some photos of it in my Kube temp sensor post before, but seeing the hardware design/layout along with the rules all in one post would be great!

Could this be handled via a Persistence restoreonstartup strategy?

Are you by any chance using the HA-Switchplate project for this, or did you design your own HMI for Nextion?

Good idea handling errors on the vent actuators, but how are you handling the input temperature errors? I see that you’re setting the temperature sensors to “-1” using the expire binding, but I don’t see you checking for that state in the control rules (at least not what you’ve posted so far) - won’t that cause a “runaway”? I guess worst case, a room vent will remain open/closed and won’t stay at the desired temperature, but it’d be nice to handle/indicate that.

Thanks for writing this up! It’s definitely on my todo list of projects, and I already have a ventmiser ready, just haven’t had time to focus on it yet :slight_smile:

Yes

They are persisted and restored on startup. This is (currently is) an oversight. Thanks for pointing that out.

Ha! I used his 3d print files, yup. I created my own interface, though. And there’s a Nextion plugin for ESPEasy so it’s handling the communication.
edit And I used his PCB. I plan to make changes to it to make it more suitable for my needs. Whether that’ll actually happen or not, I don’t know :stuck_out_tongue:

I’m mostly not. That exists for the battery powered esp units. It serves as a reminder I need to charge them! My plan is to eliminate all the battery powered units as they only last about 5-6 weeks on a charge and even though there’s only a few, I’m annoyed every time I have to charge them. I’d like to integrate the temperature sensing into the Nextion units that are placed in rooms. I’ve got to figure out how to place them so that they’re not affected by the heat of the unit.

It’s a problem, just not a big enough one that I’ve bothered to do anything about.

1 Like

This is where things start to get ugly. There are probably better ways to do this, I don’t know them, yet. I count the number of open vents. This helps to determine heat stage/fan speed.

rule "Count Vents Open / Closed"
when
	System started or
	Member of gVent received command
then
	createTimer(now.plusSeconds(2), [|
	var Number VCHour = now.getHourOfDay
	//if ( (VCHour >= 19) || (VCHour <= 7) ) {
		var Number mvc = 0
		var Number nvc = 0
		var Number avc = 0
		var Number cvc = 0
		var Number bvc = 0
		var Number fivc = 0
		var Number fovc = 0
		var Number lvc = 0
		var Number kvc = 0
		if ( masterVent.state == 1 ) {
			mvc = 1
		} else {
			mvc = 0
		}
		if ( nurseryVent.state == 1 ) {
			nvc = 1
		} else {
			nvc = 0
		}
...
		var Number Count = mvc + nvc + avc + cvc
		var Number totalCount = mvc + nvc + avc + cvc + bvc + fivc + fovc + kvc + lvc
		zoneUpperVentCount.postUpdate(Count)
		ventCount.postUpdate(totalCount)
	])

Here are the party time rules I mentioned previously:

rule "Party Time - Vent Restriction"
when
	Item partyTime received command ON
then
	logInfo("partytime", "on")
	ptRestrictCool = storeStates(masterVentMode, nurseryVentMode, atticVentMode, cornerVentMode)
	ptOpenCool = storeStates(kitchenVentMode, livingroomVentMode, foyerVentMode, fireplaceVentMode, basementVent)
	gPtRC.sendCommand(0)
	gPtOC.sendCommand(1)
end

rule "Party Time - Restore Vents"
when
	Item partyTime received command OFF
then
	if ( ptRestrictCool !== null ) {
		restoreStates(ptRestrictCool)
		ptRestrictCool = null
	}
	if ( ptOpenCool !== null ) {
		restoreStates(ptOpenCool)
		ptOpenCool = null
	}
end

This rule opens/closes vents based on what’s going on in the night with the room heating/cooling. There will be times when there are vents open in only 1 bedroom, so, to relieve any pressure I need to dump the conditioned air somewhere. Based one whether I’m heating or cooling I use different rooms for pressure relief.
( thread::sleeps :open_mouth: sorry rich. I can probably remove those, that had to do with sending too many mqtt commands to a single ESP in quick succession. I don’t think this is an issue anymore)
The fur**** items are furnace high heat (as requested by openhab vs nest), low heat, and cool.

rule "Upper Zone Vent Balancing"
when
	Item zoneUpperVentCount changed or
	Item furHiHtOH_Trigger received command or
	Item furLoHtOH_Trigger received command or
	Item furLoClOh received command or
	Time cron "0 1 7 ? * * *"
then
	var Number huzHour = now.getHourOfDay
	if ( vacation.state != ON ) {
	if ( (huzHour >= 19) || (huzHour <= 7) ) {
		if ( NestTStat_HVAC_Mode.state == "HEAT" ) {
			if ( furHiHtOH_Trigger.state == ON && zoneUpperVentCount.state < 3 && AuxHeatUpper.state == ON ) {
				if ( (huzHour >= 19) || (huzHour <= 2) ) {
					fireplaceVent.sendCommand(1)
				} else {
					kitchenVent.sendCommand(1)
					Thread::sleep(500)
					livingroomVent.sendCommand(1)
				}
    		}
    		if ( furHiHtOH_Trigger.state == ON && zoneUpperVentCount.state > 2 && fireplaceVent.state == 1 && AuxHeatUpper.state == ON ) {
        		fireplaceVent.sendCommand(0)
    		}
    		if ( furHiHtOH_Trigger.state == ON && zoneUpperVentCount.state > 2 && livingroomVent.state == 1 && AuxHeatUpper.state == ON ) {
        		livingroomVent.sendCommand(0)
				Thread::sleep(500)
				kitchenVent.sendCommand(0)
			}
    		if ( furLoHtOH_Trigger.state == ON && zoneUpperVentCount.state < 2 && AuxHeatUpper.state == ON ) {
        		if( (huzHour >=19 ) || (huzHour <= 2) ) {
            		fireplaceVent.sendCommand(1)
        		} else {
            		kitchenVent.sendCommand(1)
					Thread::sleep(500)
            		livingroomVent.sendCommand(1)
        		}
    		}
    		if ( furLoHtOH_Trigger.state == ON && zoneUpperVentCount.state > 1 && fireplaceVent.state == 1 && AuxHeatUpper.state == ON ) {
        		fireplaceVent.sendCommand(0)
    		}
    		if ( furLoHtOH_Trigger.state == ON && zoneUpperVentCount.state > 1 && livingroomVent.state == 1 && AuxHeatUpper.state == ON ) {
        		livingroomVent.sendCommand(0)
       		 	Thread::sleep(500)
				kitchenVent.sendCommand(0)
    		}
		}
		if ( NestTStat_HVAC_Mode.state == "COOL" ) {	
			if ( furLoClOh.state == ON && zoneUpperVentCount.state <2 ) {
				livingroomVent.sendCommand(1)
				kitchenVent.sendCommand(1)
			} else if ( furLoClOh.state == ON && zoneUpperVentCount.state > 1 && livingroomVent.state == 1 && kitchenVent.state == 1 ) {
				livingroomVent.sendCommand(0)
				kitchenVent.sendCommand(0)
			}
		}
	}
	}
end

This next rule is about 200 lines long. There’s a significant amount of repetition in it that I can probably remove, but, I haven’t wanted to tackle it yet. It works. Some of its function (I think) is redundant now. I’ll get to it. It has multiple functions and I did it that way because I wanted a single rule based on this trigger instead of multiple rules all using the same trigger item. I thought this would reduce complexity as far as openhab is concerned. I’ll cut out some redundancy so you get the idea:

The first part deals with opening the vents every time the furnace mode changes if the room temperature is within .5 degrees of when it would open based on temperature alone. This helps to maintain a tighter temperature.

rule "Furnace / Nest Run open vents & Vent Restrict / Restore Strategies"
when
	Item furnaceRunState received update
then
	if ( vacation.state != ON) {
	logInfo("furnace run state change", "vent selection function run")
	if ( AuxHeatLower.state == OFF && AuxHeatUpper.state == OFF && AuxCoolUpper.state == OFF ) {
		logInfo("furnace run state change","ahloahuoacuo")
		if (atticVentMode.state == 2 && NestTStat_HVAC_Mode.state == "HEAT" ) {
			if ( furnaceOHRun.state == ON || Nest_Run.state == ON ) {
				if ( atticTemperaturePx.state < (atticSetpointPx.state as DecimalType + 0.5) ) {
					atticVentTrigger.sendCommand(ON)
				}
			}
		}
		if (atticVentMode.state == 2 && NestTStat_HVAC_Mode.state == "COOL" ) {
			if ( furnaceOHRun.state == ON || Nest_Run.state == ON ) {
				if ( atticTemperaturePx.state > (atticSetpointPx.state as DecimalType - 0.5) ) {
				atticVentTrigger.sendCommand(ON)
				}
			}
		}

This next section deals with all the vent restriction/opening in the evening/night when bedrooms call for heat/cool:

	if ( Nest_Run.state != ON ) {
		if ( NestTStat_HVAC_Mode.state == "HEAT" ) {
			// Restrict UPPER vents for heating LOWER zone 
			if ( AuxHeatLower.state == ON ) {
				if ( AuxHeatUpper.state == OFF && zoneUpperRestrict === null ) {
					zoneUpperRestrict = storeStates(livingroomVentMode, kitchenVentMode, foyerVentMode, VT_Vent_Mode_Master, cornerVentMode, atticVentMode)
		    		gUVR.sendCommand(0)
        		} else if ( AuxHeatUpper.state == ON && zoneUpperRestrict === null ) {
        			zoneUpperRestrict = storeStates(livingroomVentMode, kitchenVentMode, foyerVentMode)
					livingroomVentMode.sendCommand(0)
					kitchenVentMode.sendCommand(0)
					foyerVentMode.sendCommand(0)
        		}
    		}
			// Restore UPPER vents after LOWER zone heat call end
			if ( AuxHeatLower.state == OFF && zoneUpperRestrict !== null ) {
				restoreStates(zoneUpperRestrict)
				zoneUpperRestrict = null
			}
			// Restrict LOWER vents for heating UPPER zone
			if ( AuxHeatUpper.state == ON && AuxHeatLower.state == OFF ) {
				zoneLowerRestrict = storeStates(livingroomVentMode, kitchenVentMode, foyerVentMode, fireplaceVentMode, basementVent)
				gLVR.sendCommand(0)
			} else if ( AuxHeatUpper.state == ON && AuxHeatLower.state == ON ) {
				zoneLowerRestrict = storeStates(livingroomVentMode, kitchenVentMode, foyerVentMode, basementVent)
				livingroomVentMode.sendCommand(0)
				kitchenVentMode.sendCommand(0)
				foyerVentMode.sendCommand(0)
				basementVent.sendCommand(0)
			}
			// Restore LOWER vents after UPPER zone heat call end
			if ( AuxHeatUpper.state == OFF && zoneLowerRestrict !== null ) {
				restoreStates(zoneLowerRestrict)
				zoneLowerRestrict = null
			}
		}
		if ( NestTStat_HVAC_Mode.state == "COOL") {
			if ( AuxCoolUpper.state == ON ) {
				zoneLowerRestrict = storeStates(livingroomVentMode, kitchenVentMode, foyerVentMode, fireplaceVentMode, basementVent)
				gLVR.sendCommand(0)
			}
			if ( AuxCoolUpper.state == OFF && zoneLowerRestrict !== null ) {
				restoreStates(zoneLowerRestrict)
				zoneLowerRestict = null
			}
		}
	}
	//Restore ALL vents if Nest calls for heat / cool
	if ( Nest_Run.state == ON ) {
		if ( zoneUpperRestrict !== null ) {
			restoreStates(zoneUpperRestrict)
			zoneUpperRestrict = null
		}
		if ( zoneLowerRestrict !== null ) {
			restoreStates(zoneLowerRestrict)
			zoneLowerRestrict = null
		}
	}
	}
end

That section probably needs a review and I need to add in the basement vents now that I have a temperature probe there. The short version is, when one of the bedrooms calls for heat/cool, all the vents on the lower levels close up. When the heat/cool call is done all the vents are restored to their previous positions.

Now I deal with the operation of the furnace. There are what I’m calling Aux heating/cooling calls/triggers. These are triggered by various sections of the house calling for heat/cool. All of them are cancelled at 7am:

rule "Turn Off All Auxiliary Heating / Cooling @ 7a"
when
	Time cron "0 0 7 ? * * *"
then
	AuxHeatUpper.sendCommand(OFF)
	AuxHeatLower.sendCommand(OFF)
	AuxCoolUpper.sendCommand(OFF)
end

Heating/cooling calls start at 7pm:

rule "Auxiliary Heating Upper Trigger"
when
	Member of gRoomNeedHeat changed or
	Time cron "0 0 19 ? * * *"
then
	var Number xHour = now.getHourOfDay
	if ((xHour >= 19) || (xHour <=6)) {
		if ( nurseryNeedHeat.state == ON || masterNeedHeat.state == ON || atticNeedHeat.state == ON || cornerNeedHeat.state == ON ) {
			Thread::sleep(100)
			AuxHeatUpper.sendCommand(ON)
		}
	}
	if ((xHour >= 19) || (xHour <=9)) {
		if (nurseryNeedHeat.state == OFF && masterNeedHeat.state == OFF && atticNeedHeat.state == OFF && cornerNeedHeat.state == OFF ) {
			Thread::sleep(100)
			AuxHeatUpper.sendCommand(OFF)
		}
	}
end

(there’s one for cooling,too. I’ll skip it)

This rule sets the various furnace mode triggers. Depending on how many things are calling for heat/cool I’m deciding what fan speed/heat stage is needed. If the Nest thermostat calls for heating/cooling I cancel all my heating/cooling modes until it’s done.

rule "Auxiliary Heating and Cooling Modes"
when
	Item AuxCoolUpper received command or
	Item AuxHeatLower received command or
	Item AuxHeatUpper received command or
	Item Nest_Run received command
then
	Thread::sleep(500)
	if ( Nest_Run.state == ON ) {
		if ( NestTStat_HVAC_Mode.state.toString == "HEAT" ) {
			if ( AuxHeatLower.state == ON && AuxHeatUpper.state == ON ) {
				furLoHtOH_Trigger.postUpdate(OFF)
				furHiHtOH_Trigger.postUpdate(ON)
				furnaceOHRun.sendCommand(ON)
			}
			if ( AuxHeatLower.state == OFF && AuxHeatUpper.state == ON ) {
				 if ( zoneUpperVentCount.state == 4 || zoneUpperVentCount.state == 3 ) {
					furnaceOHRun.sendCommand(ON)
					furLoHtOH_Trigger.postUpdate(OFF)
					furHiHtOH_Trigger.postUpdate(ON)
				} else if ( zoneUpperVentCount.state == 1 || zoneUpperVentCount.state == 2 ) {
					furnaceOHRun.sendCommand(ON)
					furHiHtOH_Trigger.postUpdate(OFF)
					furLoHtOH_Trigger.postUpdate(ON)
				}
			}
			if ( AuxHeatLower.state == ON && AuxHeatUpper.state == OFF ) {
				furHiHtOH_Trigger.postUpdate(OFF)
				furLoHtOH_Trigger.postUpdate(ON)
				furnaceOHRun.sendCommand(ON)
			}
		}
		if ( NestTStat_HVAC_Mode.state.toString == "COOL" ) {
			if ( AuxCoolUpper.state == ON ) {
				furnaceOHRun.sendCommand(ON)
				furLoClOh.postUpdate(ON)
			}
		}
	}
	if ( Nest_Run.state == OFF ) {
		if ( NestTStat_HVAC_Mode.state.toString == "HEAT" ) {
			if ( AuxHeatLower.state == ON && AuxHeatUpper.state == ON ) {
				furLoHtOH_Trigger.postUpdate(OFF)
				furHiHtOH_Trigger.sendCommand(ON)
				furnaceOHRun.sendCommand(ON)
			}
			if ( AuxHeatLower.state == OFF && AuxHeatUpper.state == ON ) {
				 if ( zoneUpperVentCount.state == 4 || zoneUpperVentCount.state == 3 ) {
					furnaceOHRun.sendCommand(ON)
					furLoHtOH_Trigger.postUpdate(OFF)
					furHiHtOH_Trigger.sendCommand(ON)
				} else if ( zoneUpperVentCount.state < 3 ) {
					furnaceOHRun.sendCommand(ON)
					furHiHtOH_Trigger.postUpdate(OFF)
					furLoHtOH_Trigger.sendCommand(ON)
				}
			}
			if ( AuxHeatLower.state == ON && AuxHeatUpper.state == OFF ) {
				furHiHtOH_Trigger.postUpdate(OFF)
				furLoHtOH_Trigger.sendCommand(ON)
				furnaceOHRun.sendCommand(ON)
			}
			if ( AuxHeatLower.state == OFF && AuxHeatUpper.state == OFF ) {
				furHiHtOH_Trigger.postUpdate(OFF)
				furLoHtOH_Trigger.sendCommand(OFF)
				furnaceOHRun.sendCommand(OFF)
			}
		}
		if ( NestTStat_HVAC_Mode.state.toString == "COOL" ) {
			if ( AuxCoolUpper.state == ON ) {
				furnaceOHRun.sendCommand(ON)
				furLoClOh.sendCommand(ON)
			}
			if ( AuxCoolUpper.state == OFF ) {
				furnaceOHRun.sendCommand(OFF)
				furLoClOh.sendCommand(OFF)
			}
		}
	}
end

This next rule handles high/low stage heat for the nest. I had an electrical way of listening to the high/low call of the nest but the baseplate seems to have stopped working correctly so now I’m just basing it off of a temperature difference. I plan to add a time element to it, too. I don’t have an actual 2 stage furnace. It can only be called for heat and the furnace controller has some strategy for using high or low. I’d rather the Nest make that determination.

rule "Low heat call stand-in"
when
	Item N_V_NestCurrTemp changed or
	Item S_V_NestTargetTemp changed
then
	if ( NestTStat_HVAC_Mode.state.toString == "HEAT" ) {
		logInfo("nest state","heating")
		var Number nestSetPoint = S_V_NestTargetTemp.state as DecimalType
		var Number nestCurrentTemp = N_V_NestCurrTemp.state as DecimalType
		var Number nestDifference = nestSetPoint - nestCurrentTemp
		logInfo("Nest Temperature Difference:",nestDifference.toString)
		if ( nestDifference >= 2 ) {
			nestLowHeatCall.sendCommand(0)
		} else {
			nestLowHeatCall.sendCommand(1)
		}
	}
end

This rule is just for setting the furnace mode based on the Nest.

rule "Nest Set Furnace States"
when
	Item nestLowHeatCall changed or
	Item NestTStat_hvac_state changed or
	Item ventCount changed
then
	Thread::sleep(500)
	if ( NestTStat_hvac_state.state == "HEATING" && nestLowHeatCall.state == 1 ) {
		Nest_Run.sendCommand(ON)
		furLoHtNT_Trigger.sendCommand(ON)
		furHiHtNT_Trigger.postUpdate(OFF)
	} else if ( NestTStat_hvac_state.state == "HEATING" && nestLowHeatCall.state == 0 ) {
		Nest_Run.sendCommand(ON)
		furHiHtNT_Trigger.sendCommand(ON)
		furLoHtNT_Trigger.postUpdate(OFF)
	} else if ( NestTStat_hvac_state.state == "COOLING" && ventCount.state > 4 ) {
		Nest_Run.sendCommand(ON)
		furHiClNt.sendCommand(ON)
	} else if ( NestTStat_hvac_state.state == "COOLING" && ventCount.state <= 4 ) {
		Nest_Run.sendCommand(ON)
		furLoClNt.sendCommand(ON)
	} else {
		if ( Nest_Run.state != OFF ) {
			Nest_Run.sendCommand(OFF)
		}
		if ( furHiHtNT_Trigger.state != OFF ) {
			furHiHtNT_Trigger.sendCommand(OFF)
		}
		if ( furLoHtNT_Trigger.state != OFF ) {
			furLoHtNT_Trigger.sendCommand(OFF)
		}
		if ( furHiClNt.state != OFF ) {
			furHiClNt.sendCommand(OFF)
		}
		if ( furLoClNt.state != OFF ) {
			furLoClNt.sendCommand(OFF)
		}
	}
end

And finally, to bring that all together. This rule determines, based on the triggers, what command to send to the esp8266 in control of the relay board at the furnace:
(the thread sleep exists to be sure all changes to the furnace mode are done before I send the command off to the esp)

rule "Set Furnace Mode"
when
	Member of gFurnaceModes changed
then
	if ( vacation.state != ON) {
	Thread::sleep(500)
	if ( furHiHtNT_Trigger.state == ON && NestTStat_HVAC_Mode.state == "HEAT" ) {
		if ( furnaceRunMode.state != 1 ) {
			furnaceRunMode.sendCommand(1)
			furnaceRunState.postUpdate("nHiHeat")
		}
	} else if ( furLoHtNT_Trigger.state == ON && NestTStat_HVAC_Mode.state == "HEAT" ) {
		if ( furnaceRunMode.state != 2 ) {
			furnaceRunMode.sendCommand(2)
			furnaceRunState.postUpdate("nLoHeat")
		}
	} else if ( furHiHtOH_Trigger.state == ON && NestTStat_HVAC_Mode.state == "HEAT" ) {
		if ( furnaceRunMode.state != 3 ) {
			furnaceRunMode.sendCommand(3)
			furnaceOHRun.postUpdate(ON)
			furnaceRunState.postUpdate("oHiHeat")
		}
	} else if ( furLoHtOH_Trigger.state == ON && NestTStat_HVAC_Mode.state == "HEAT" ) {
		if ( furnaceRunMode.state != 4 ) {
			furnaceRunMode.sendCommand(4)
			furnaceOHRun.postUpdate(ON)
			furnaceRunState.postUpdate("oLoHeat")
		}	
	} else if ( furLoFan_Trigger.state == ON ) {
		if ( furnaceRunMode.state != 5 ) {
			furnaceRunMode.sendCommand(5)
			furnaceOHRun.postUpdate(ON)
			furnaceRunState.postUpdate("oFan")
		}
	} else if ( furHiClNt.state == ON && NestTStat_HVAC_Mode.state == "COOL" ) {
		if ( furnaceRunMode.state != 6 ) {
			furnaceRunMode.sendCommand(6)
			furnaceRunState.postUpdate("nHiCool")
		}
	} else if ( furLoClNt.state == ON && NestTStat_HVAC_Mode.state == "COOL" ) {
		if ( furnaceRunMode.state != 9 ) {
			furnaceRunMode.sendCommand(9)
			furnaceOHRun.postUpdate(ON)
			furnaceRunState.postUpdate("nLoCool")
		}
	} else if ( furHiClOh.state == ON && NestTStat_HVAC_Mode.state == "COOL" ) {
		if ( furnaceRunMode.state !=7 ) {
			furnaceRunMode.sendCommand(7)
			furnaceRunState.postUpdate("oHiCool")
		}
	} else if ( furLoClOh.state == ON && NestTStat_HVAC_Mode.state == "COOL" ) {
		if ( furnaceRunMode.state !=8 ) {
			furnaceRunMode.sendCommand(8)
			furnaceRunState.postUpdate("oLoCool")
		}
	} else {
		furnaceRunMode.sendCommand(0)
		furnaceOHRun.postUpdate(OFF)
		furnaceRunState.postUpdate("OFF")
	}
	}
end

This got me thinking - I’ve got a couple Nextion displays on order and already have the rest of the parts to build a few HA-Switchplates, but I only have two rooms where I have spare switch gangs to insert them into. So, I’m thinking about replacing the OLED screen on my sensors with these Nextion displays. That way I can still use the DHT22 for remote temp/humidity, the screen for local reporting, but also add local control via the touchscreen. I wouldn’t worry about internal heat - I’ve got my sensors well-calibrated to the HVAC themorstat (via linear offset). Thanks for the inspiration :slight_smile:

I think that entire rule could be replaced with a group item, e.g.:

Group:Number:SUM gVentCount

that would return the number of open vents (since open sends a 1 and closed a 0).

I’ll let Rich chime in on the rest of the rules, as I’m sure he’ll have better comments/efficiencies than I could come up with :slight_smile:

All in all, excellent work! As you’ve said yourself, these rules work as designed and you’ve got well-automated temperature control system for your family!

I extended the back cover of his prints to accommodate a single relay hat that’s used to switch the room light on/off. Now I have “smart” control of the room light without a fibaro/aeotec/hue and it’s all done in a single gang (pictures of the interface are in the next post I’m starting to write).

thanks

No doubt!

Thanks :smiley:

1 Like

That covers most of the rules. I didn’t post a lot of the items. You should be able to figure out what’s going on from the rules and make the items accordingly. 95% (pulled out of the air) of the items are simple switches. Just on/off and then the rules make decisions based on what’s on or off. If you have questions, just ask. If you have suggestions, go for it. I started this project with ZERO programming knowledge. I’m a systems admin, so I’m not completely in the dark here. I’ve been tinkering with things since I could hold a screwdriver. But, all this is proof that someone with no real programming knowledge can do this. You can do it!

From here I will detail the arduino side of things and the hardware involved. As I mentioned previously, I’m using a firmware for the esp8266 called ESPEasy. Here are some screenshots of what it looks like:

ESPEasy allows for sending/receiving MQTT messages. I take these messages and using the built in rules engine I can switch various things like the dual H bridge or a 16 port mcp23017 channel expander.
I used an L293D dual H bridge to drive 2 separate vents. Here’s a hand drawn pic of how it is wired:


pins 2/7 on the left control 3/6 on the left. If you pulse +3.3v (say for 1000ms) to pin 2, pin 3 goes + and pin 6 goes - driving the motor. If you give pin 7 +3.3v then 3 goes - and pin 6 goes positive driving the motor in reverse. Easy, right? Now you can open and close a vent using the built-in dc motor on the vent. On the right side you can do the same thing to a second vent.

Here’s a Vent Miser sans motor:


Closeup view of where the motor mounts (sorry, not sure where the motor for this one is. It’s just a typical hobby-type DC motor).

The 3.3v on the L293D on pins 1,8,9,16 are all taken from the power supply, not from the ESP regulator. I don’t know that it could handle it, especially when the motor stalls. Maybe it could, I don’t know. I drove it from a separate regulator. I think it’s really supposed to be 5v minimum but it works on 3.3v and the motor is nice and quiet at 3.3v.

Here are some samples of the rules on the ESP:

on UtilityRoom#furnacerunmode=1 do
mcpgpio 15,0
timerSet,1,1
endon

on UtilityRoom#furnacerunmode=2 do
mcpgpio 15,0
timerSet,2,100
endon

on UtilityRoom#furnacerunmode=3 do
timerSet,3,1
delay 1000
timerSet,1,3
endon

The MQTT message is received as “UtilityRoom#furnacerunmode=#”
the mcpgpio turns on or off a channel on the mcp23017 which in turn turns on or off a relay in the furnace. These relays cut out the various fan speeds or connect the heat/cool calls.

The timers allow the furnace to turn on, ignite and stabilize before I change fan speeds or cut the High gas valve. The low valve is energized during High and Low heat calls so I only need to cut High to use Low.

This timer turns all the fan relays to on to cut power to them if the furnace is using them and powers just the low fan speed. Here’s what one of the timers looks like:

on Rules#Timer=2 do
timerSet,1,0
timerSet,3,0
timerSet,6,0
mcpgpio 13,0 //low heat cut
mcpgpio 10,0 //med fan cut
mcpgpio 14,0 //high fan cut
mcpgpio 12,0 //med low fan cut
delay 50
mcpgpio 9,0  //low fan on
endon

The next rule is a vent actuation rule. Power is pulsed to an mcp23017 which switches a leg of one of the L293Ds on to open/close a vent. It also sends an MQTT message back to openHAB to inform it that it has received the command and the vent was actuated:

on UtilityRoom#livingroomvents=1 do
Publish /%sysname%/lrv/ack,1
mcppulse 27,1,2000
mcppulse 25,1,1200
endon

Here are some miscellaneous things. Pics of the board I designed using easyeda.com The boards were like 20$ bucks shipped. It took maybe a week from order to having the product in hand. Easyeda is simple enough for someone like me to use. I have a few of these boards spare since the minimum order is 5.
You’ll see a lot of extra junk on this board. This board is designed to accommodate functions for the vents, the furnace, and work as a controller in the garage to watch a lot of things on the two garage doors/openers.

And some pics of the Nextion interface. This is v1. V2 is prettier and laid out better. You get the idea:

Temperature can be adjusted and the vent can be turned on/off, auto, and we can ask for heating/cooling at night using the furnace icon. All these icons are updated via MQTT to ESPEasy if these are switched via openHAB ( sitemap etc).

A snippet of the of sitemap:


you can see the setpoint and the daytime setpoint that was mentioned a long time ago.

Here is a simple chart of the nursery temperature. The highlighted portion is the night time when I’m regulating the furnace solely for keeping the nursery up to temp:
chart

So, I think that’s most of it.

1 Like

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 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 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

Thanks @rlkoshak for all the suggestions. A lot of this was written a while ago before i started “modernizing” and compacting the rules based off of stuff I’ve learned from you. I’ll look into some of those in the coming weeks/months as time permits. I’m in a bit of a lull as it comes to adding features so cleaning up these rules would be a good way to fill some skills gaps.

I’ll try to add some information on material costs in the coming days.

Likely most of those thread::sleeps can be removed, especially with cleanup. Some existed to give things time to settle before checking various items for state and I know better ways around that now. That said, I don’t think my system is big enough with enough rules that anything ever hangs - at least not that I’ve ever noticed :slight_smile:

1 Like

coming soon, wireless+battery powered vents using mysensors + smartsleep
2xAAA batteries and possible life of 300+ days
i’ll call this beta v2 of this.

I’ve been running 3 of these battery powered vents now for about a month. They check in every 3 minutes and open or close if need be. So far they still show over 3v and they actuate on average probably once or more per hour. The controller fits inside the removable original control box without removing anything making them plug and play.

1 Like