Openhab Heating Configuration

This tutorial is a section of my wider series on creating a home automation solution for heating and watering in my house.  Refer to the index here.

In this post I cover a detailed configuration of Openhab for heating purposes.  We create the rule sets to allow us to have an arbitrary number of zones, with each zone having 4 setpoints at configurable times during the day.  The zones push those setpoints down to the devices in the zone.  We allow people to change the temperature in a zone using the wall thermostat, but override again at the next timed change.

We also control the running of the boiler based on how many valves are open – this is to avoid the boiler short cycling when there are very few valves or valves only partly open.  We’ll cover this in more detail when we get to the relevant rules.

We create a sitemap that gives us access to most of this information.

Let’s start with the items configuration.  This creates all the groups necessary for the rules to interact with zones and devices generically, rather than any of the rules knowing about specific zones.  This means we can add or remove zones, and change which devices are in which zones, without changing the rules.

We still need to define each zone and each device individually in here, but in general we only need to do that here and in the sitemap.

Create a file /etc/openhab/configurations/items/heating.items. Include the below content in the file.

Group All

Group Zones (All)
Group Devices (All)
Group DevicesValves (All)
Group DevicesThermostats (All)

/* Used to work around a defect on the sitemap editor */
Group none (All)

/* Used for persistence */
Group zoneSettingsSetpoints (All)
Group zoneSettingsTimes (All)

/* Used for persistence */
Group Switches (All)

/* Aggregates and grouping.  The aggregates are used to tell the rules that something has
 * changed, therefore a rule needs to run.  The groups are used to display these things on
 * the UI */
Group:Number:SUM ZoneSetpoints (All)
Group DeviceTemps (All)
Group:Number:SUM DeviceSetpoints (All)
Group:Number:SUM DeviceValves (All)
Group:Number:SUM DeviceBatteries (All)

/* Initialisations - things in these groups are iterated over by the initialisation rules
 * and set to default values as appropriate */
Group InitCurrentSetpoints (All)
Group InitSetpointsMornings (All)
Group InitSetpointsDays (All)
Group InitSetpointsEvenings (All)
Group InitSetpointsNights (All)
Group InitMornings (All)
Group InitDays (All)
Group InitEvenings (All)
Group InitNights (All)
Group InitSwitchesOn (All)
Group InitSwitchesOff (All)

/* Maxcul configuration flags, used for pairing new devices and checking what's going on */
Switch swPair "Pair"  (Switches, InitSwitchesOff) { maxcul="PairMode" }
Switch swListen "Listen"  (Switches, InitSwitchesOff) { maxcul="ListenMode" }
Number txCredit "Transmission Credit [%d]" { maxcul="CreditMonitor" }

/* Boiler pump configuration - used to set how long the pump runs and when, and to
 * manage a state machine as the pump cycles from running to waiting */
Switch swPump "Pump"  (InternalFlags, Switches, InitSwitchesOff)
Number numberPumpWaitSeconds "Pump Wait Seconds [%.0f s]"  (InternalFlags)
Number numberPumpRunBaseSeconds "Pump Run Base Seconds [%.0f s]"  (InternalFlags)
Number numberPumpRunFactor "Pump Run Factor [%.0f %%]"  (InternalFlags)
Number numberPumpAutoRunThreshold "Pump AutoOn Valve Opening Threshold [%.0f %%]"  (InternalFlags)
String stringPumpMode "Pump Mode [%s]"  (InternalFlags)
String stringTimerEnd "Current Timer End [%s]"  (InternalFlags)

/* ZONES
 * Each zone has a group for it's associated devices, and a group that contains the settings.  In general
 * you can create a new zone by copying an existing one, and changing the name of that zone everywhere it appears,
 * then creating the subsidiary devices under it */

/* Bed Zone */
Group zoneBed "Bed" (Zones)
Group zoneDevicesBed "Devices" (zoneBed)

Group zoneSettingsBed "Settings" (zoneSettings, zoneBed)
Switch swBed "Bed Heating Master"  (zoneSettingsBed, Switches)
Number setCurrentBed "Bed Zone Current Setpoint [%.1f °C]"  (zoneSettingsBed, InitCurrentSetpoints, ZoneSetpoints)
Group zoneSettingsTimesBed (zoneSettingsBed)
Group zoneSettingsSetpointsBed (zoneSettingsBed)
Number set1Bed "Setpoint [%.1f °C]"  (InitSetpointsMornings, zoneSettingsBed, zoneSettingsSetpointsBed, zoneSettingsSetpoints)
Number timeStart1Bed "Time Start [%04d]"  (InitMornings, zoneSettingsBed, zoneSettingsTimesBed, zoneSettingsTimes)
Number set2Bed "Setpoint [%.1f °C]"  (InitSetpointsDays, zoneSettingsBed, zoneSettingsSetpointsBed, zoneSettingsSetpoints)
Number timeStart2Bed "Time Start [%04d]"  (InitDays, zoneSettingsBed, zoneSettingsTimesBed, zoneSettingsTimes)
Number set3Bed "Setpoint [%.1f °C]"  (InitSetpointsEvenings, zoneSettingsBed, zoneSettingsSetpointsBed, zoneSettingsSetpoints)
Number timeStart3Bed "Time Start [%04d]"  (InitEvenings, zoneSettingsBed, zoneSettingsTimesBed, zoneSettingsTimes)
Number set4Bed "Setpoint [%.1f °C]"  (InitSetpointsNights, zoneSettingsBed, zoneSettingsSetpointsBed, zoneSettingsSetpoints)
Number timeStart4Bed "Time Start [%04d]"  (InitNights, zoneSettingsBed, zoneSettingsTimesBed, zoneSettingsTimes)


/* Master bedroom */
Group valveMaster "Master Bedroom"  (zoneDevicesBed, Devices, DevicesValves)
Number MasterSetpoint "Master Current Setpoint [%.1f °C]"  (valveMaster, DeviceSetpoints, InitSetpoints) { maxcul="RadiatorThermostat:LEQ1133856:feature=thermostat:associate=MEQ0856945:configTemp=21.0/17.0/30.5/4.5/4.5/0.0/0.0" }
Switch MasterBattery "Master Battery"  (valveMaster, DeviceBatteries) { maxcul="RadiatorThermostat:LEQ1133856:feature=battery" }
Number MasterTemp "Master Temp [%.1f °C]"  (valveMaster, DeviceTemps) { maxcul="RadiatorThermostat:LEQ1133856:feature=temperature" }
Number MasterValve "Master Valve [%.1f %%]" (valveMaster, DeviceValves) { maxcul="RadiatorThermostat:LEQ1133856:feature=valvepos" }

/* Master Wall Thermostat */
Group wallMaster "Master Wall"  (zoneDevicesBed, Devices, DevicesThermostats)
Number WallMasterSetpoint "Wall Master Current Setpoint [%.1f °C]"  (wallMaster, DeviceSetpoints, InitSetpoints) { maxcul="WallThermostat:MEQ0856945:feature=thermostat:associate=LEQ1133856:configTemp=21.0/17.0/30.5/4.5/4.5/0.0/-0.8" }
Switch WallMasterBattery "Wall Master Battery"  (wallMaster, DeviceBatteries) { maxcul="WallThermostat:MEQ0856945:feature=battery:" }
Number WallMasterTemp "Wall Master Temp [%.1f °C]"  (wallMaster, DeviceTemps) { maxcul="WallThermostat:MEQ0856945:feature=temperature" }
Switch WallMasterMode "Wall Master Display Mode" (wallMaster, DeviceModes) { maxcul="WallThermostat:MEQ0856945:feature=displaySetting"}

 

Next, create a sitemap in /etc/openhab/configurations/sitemaps/default.sitemap. Include the below content. You can probably make it much prettier, but it’s suitable for my needs.

sitemap heating label="Home Heating"  {
  Frame {
    /* Show a battery warning if any battery switches are turned on */
    Text item=DeviceBatteries label="At least one battery is low" visibility=[DeviceBatteries>0] labelcolor=["Red"]

    /* Provide access to some of the groups of information - all temps, all valves, all batteries, all setpoints */
  	Group item=none label="Current Status" {
  	  Group item=ZoneSetpoints label="Zone Setpoints"
  	  Group item=DeviceSetpoints label="Device Setpoints"
  	  Group item=DeviceTemps label="Temps"
  	  Group item=DeviceValves label="Valve Positions"
  	  Group item=DeviceBatteries label="Batteries [%.1f]"
  	}
  	
  	/* We have to manually configure each zone, because we need to use Setpoints and have a specific layout.  You'll
  	 * need to edit this portion each time you add a zone */
  	Group item=none label="Zones" {
      Group item=zoneBed label="Bed" icon="none" {
      	Frame label="Overview" {
          Switch item=swBed
          Group item=zoneDevicesBed visibility=[swBed==ON]
          Text item=setCurrentBed visibility=[swBed==ON]
      	}
      	Frame label="Morning" visibility=[swBed==ON] {
          Setpoint item=timeStart1Bed minValue=0 maxValue=2345 step=15
          Setpoint item=set1Bed minValue=15 maxValue=25 step=0.5
      	}
      	Frame label="Day" visibility=[swBed==ON] {
          Setpoint item=timeStart2Bed minValue=0 maxValue=2345 step=15
          Setpoint item=set2Bed minValue=15 maxValue=25 step=0.5
      	}
      	Frame label="Evening" visibility=[swBed==ON] {
          Setpoint item=timeStart3Bed minValue=0 maxValue=2345 step=15
          Setpoint item=set3Bed minValue=15 maxValue=25 step=0.5
      	}
      	Frame label="Night" visibility=[swBed==ON] {
          Setpoint item=timeStart4Bed minValue=0 maxValue=2345 step=15
          Setpoint item=set4Bed minValue=15 maxValue=25 step=0.5
      	}
      }      
    }
    
    /* Again a generic group that just gives us access to the information for each device's current state */
    Group item=Devices label="Devices" icon="none"

    /* Access to the detailed settings, particularly all the configurables around the boiler pump, and pairing
     * of maxcul devices */
    Group item=none label="Settings" icon="settings" {
      Switch item=swPair
      Switch item=swListen
      Text item=txCredit
      Setpoint item=MasterValve step=10
      Text item=DeviceValves label="Total Valve Openings [%.1f]"
      Switch item=swPump
      Setpoint item=numberPumpWaitSeconds step=10 minValue=0 maxValue=1000
      Setpoint item=numberPumpRunBaseSeconds step=10 minValue=0 maxValue=1000
      Setpoint item=numberPumpRunFactor step=10 maxValue=100 minValue=0
      Setpoint item=numberPumpAutoRunThreshold step=10 minValue=0 maxValue=1000
      Text item=stringPumpMode
      Text item=stringTimerEnd
    }
    Group item=none label="Graphs" icon="chart" {
      Image url="http://localhost:8080/rrdchart.png?items=MasterTemp,MasterValve,MasterSetpoint&period=3D&random=1&h=500&w=1000" label="Master Bedroom History"
      Image url="http://localhost:8080/rrdchart.png?items=swPump&period=D&random=1&h=500&w=1000" label="Pump Runtime"
    }
  }
}

Next we want to configure persistence – we want the settings that we create to be stored. We’ll use the rrd4j engine, as this is a simple file based service. You could choose instead to use a database if you have one available.

  sudo aptitude install openhab-addon-persistence-rrd4j

Edit /etc/openhab/configurations/openhab.cfg to include the following block to tell openhab we want to use rrd4j (include this towards the bottom of the general configurations):

# The name of the default persistence service to use
persistence:default=rrd4j

Create /etc/openhab/configurations/persistence/rrd4j.persist, which tells openhab which of our values we want to persist when we reboot, or alternatively which we want it to keep track of so that we can graph them at a later time if we wish. Content as follows:

// persistence strategies have a name and a definition and are referred to in the "Items" section
Strategies {
	everyHour 	: "0 0 * * * ?"
	everyDay 	: "0 0 0 * * ?"
	everyTenMins: "0 */10 * * * ?"
	everyMinute  : "0 * * * * ?"

	// if no strategy is specified for an item entry below, the default list will be used
	default = everyChange
}

/* 
 * Each line in this section defines for which item(s) which strategy(ies) should be applied.
 * You can list single items, use "*" for all items or "groupitem*" for all members of a group
 * item (excl. the group item itself).
 */
Items {
        zoneSettingsTimes* : strategy = everyChange, restoreOnStartup
        zoneSettingsSetpoints* : strategy = everyChange, restoreOnStartup
        Switches* : strategy = everyChange, restoreOnStartup
        ZoneSetpoints* : strategy = everyMinute, everyChange
        DeviceSetpoints* : strategy = everyMinute, everyChange
        swPump : strategy = everyMinute, everyChange
        DeviceValves* : strategy = everyMinute, everyChange
        DeviceTemps* : strategy = everyMinute, everyChange
        numberPumpRunFactor : strategy = everyChange, restoreOnStartup
        numberPumpRunBaseSeconds : strategy = everyChange, restoreOnStartup
        numberPumpWaitSeconds : strategy = everyChange, restoreOnStartup
        numberPumpAutoRunThreshold : strategy = everyChange, restoreOnStartup
}

Next we’re into the rules to tie all this together. Let’s start with initialisation of all the important values to some sort of sensible default. The initialisation rules will set these values on startup, unless they’ve already been restored from the persistence service to something useful.

It’s important to default, because elements such as setpoints can’t be edited until they have a value in them.

Create a file /etc/openhab/configurations/rules/initialisation.rules, with the following content:

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.openhab.core.library.types.DecimalType
import org.joda.time.*

/*
  These rules initialise all our values 30s after startup based on the groups they're in.  
  Where we have persistence for the values, they'll be filled in when persistence starts up,
  so this avoids any values that weren't saved by persistence being undefined.
  
  If we get timing issues and persistence starts after initialisation, then persistence will
  just overwrite whatever we initialise, so no drama
*/


rule "Initialise values"
when
  System started
then
  logInfo( 'initialisation', 'Waiting for rules files to load before initialising' )
  
  createTimer( now().plusSeconds( 5 )) [|  	
    logInfo( 'initialisation', "Initialising relevant items" )
    InitSwitchesOn.members.forEach( aSwitch |
      postUpdate( aSwitch, ON )
    )
    InitSwitchesOff.members.forEach( aSwitch |
      postUpdate( aSwitch, OFF )
    )
    InitMornings.members.forEach( aTime |
  	  if( aTime.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising morning to 0600' + aTime.name )
        postUpdate( aTime, 0600 )
  	  } 
    )
    InitDays.members.forEach( aTime |
      if( aTime.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising day to 0900' + aTime.name )
        postUpdate( aTime, 0900 )
      }
    )
    InitEvenings.members.forEach( aTime | 
      if( aTime.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising evening to 1800' + aTime.name )
        postUpdate( aTime, 1800 )
      }
    )
    InitNights.members.forEach( aTime |
      if( aTime.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising night to 2130' + aTime.name )
        postUpdate( aTime, 2130 )
      }
    )
    InitCurrentSetpoints.members.forEach( aSetpoint |
      if( aSetpoint.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising current setpoint to 15' + aSetpoint.name )
        postUpdate( aSetpoint, 15 )
      }
    )
    InitSetpointsMornings.members.forEach( aSetpoint |
      if( aSetpoint.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising morning setpoint to 20' + aSetpoint.name )
        postUpdate( aSetpoint, 20 )
      }
    )
    InitSetpointsDays.members.forEach( aSetpoint |
      if( aSetpoint.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising day setpoint to 19' + aSetpoint.name )
        postUpdate( aSetpoint, 19 )
      }
    )
    InitSetpointsEvenings.members.forEach( aSetpoint |
      if( aSetpoint.state == Uninitialized ) {
        logDebug( 'initialisation', 'Initialising evening setpoint to 21' + aSetpoint.name )
        postUpdate( aSetpoint, 21 )
      }
    )
    InitSetpointsNights.members.forEach( aSetpoint |
      if( aSetpoint.state == Uninitialized ) { 
        logDebug( 'initialisation', 'Initialising night setpoint to 18' + aSetpoint.name )
        postUpdate( aSetpoint, 18 )
      }
    )
  
    if( numberPumpRunFactor.state == Uninitialized ){
      postUpdate( numberPumpRunFactor, 30 )
    }

    if( numberPumpWaitSeconds.state == Uninitialized ){
      postUpdate( numberPumpWaitSeconds, 300 )
    }

    if( numberPumpRunBaseSeconds.state == Uninitialized ){
      postUpdate( numberPumpRunBaseSeconds, 60 )
    }

    if( numberPumpAutoRunThreshold.state == Uninitialized ){
      postUpdate( numberPumpAutoRunThreshold, 200 )
    }

    logInfo( 'initialisation', "Finished initialising relevant items")
  ]
end

Once saved, you should see your openhab automatically run this and the values default within your openhab UI.

Next, we need some rules to handle the UI time entry within zones. When people move the setpoint up or down we want it to treat the value as if it were hours and minutes – so it should wrap around at 60 minutes to the next hour.

Create a rules file /etc/openhab/configurations/rules/time_normalisation.rules:

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.openhab.core.library.types.DecimalType

/*
  This set of rules normalise the time fields in each zone, so that minutes are always
  in 15 min blocks, and wrap around at 60, not at 99
*/

/*
  Function defined as a lambda.  Stupid function syntax if you ask me!!
  This looks at the minutes, if it's 85 then we went down, and should be 45, if it's 60 then
  we went up, and should be 00 plus add one to the hour
*/

val org.eclipse.xtext.xbase.lib.Functions$Function1 normaliseMinutes = [
    org.openhab.core.library.items.NumberItem timeStart |
  var int currentHours = (timeStart.state as DecimalType).intValue / 100
  var int currentMinutes = (timeStart.state as DecimalType).intValue % 100

  if( currentMinutes == 85 ) {
    currentMinutes = 45
  }

  if( currentMinutes == 60 ) {
    currentMinutes = 0
    currentHours = currentHours + 1
  }

  if( (timeStart.state as DecimalType).intValue != currentHours * 100 + currentMinutes ) {
    postUpdate( timeStart, currentHours * 100 + currentMinutes )
  }

]


/* 
  Time normalisers - call the time normaliser whenever a Time field gets changed
*/
rule "Modify Time - adjust minutes"
when
  Item zoneSettingsTimes changed
then
  zoneSettingsTimes.members.forEach( time | {
    normaliseMinutes.apply( time )
  })
end

Then we’ll build the rules that look at the settings on the zone(s), and decide what the current setpoint should be. Rather than trying to work out when someone has changed something (which has a bit of performance impact on the UI), we’ll just check these settings every minute at 5 seconds past the minute.

Before implementing these, you should set the timezone on your pi:

  sudo tzselect
  sudo /etc/init.d/openhab restart

Create /etc/openhab/configurations/rules/zone_setupoints_from_times.rules:

import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.openhab.core.library.types.DecimalType
import org.joda.time.*

/*
  These rules check the current time of day and the time of day settings on each zone, and work
  out what the current setpoint should be.  The rules are scheduled to run every minute - the resolution 
  of the setting times can only be up to 15 minute intervals, but you could change a setting and 
  want it to apply immediately, so we run more frequently
*/


/*
  This function calculates the correct temp for a given zone, and only changes that
  setpoint if it's different than what's already set (to avoid needlessly cascading changes, which
  would override any temporary settings made via a wall thermostat).
  If the zone is turned off, it sets the temp to 5 
*/


val org.eclipse.xtext.xbase.lib.Functions$Function1 setZoneSetpoint = [
    org.openhab.core.items.GroupItem zone |

  var int currentHours = now().getHourOfDay()
  var int currentMinutes = now().getMinuteOfHour()
  var int currentTime = currentHours * 100 + currentMinutes

  var org.openhab.core.items.GroupItem settings = zone.members.filter( m | m.name.startsWith('zoneSettings')).last
  var org.openhab.core.items.GroupItem times = settings.members.filter( m | m.name.startsWith('zoneSettingsTimes')).last

  /* we use the setpoint from the highest time that is less than the current time.  If there isn't one less than current time,
     then we use the last setpoint, which should be the night setpoint.  We assume settings are in time order, if someone
     sets time1 to be higher than time2, then this will break */
  var org.openhab.core.items.GroupItem lastTime = times.members.last
  var int setpointNumber = 0

  times.members.forEach( aTime | {
    var int time = (aTime.state as DecimalType).intValue
    if( time <= currentTime ) {
      setpointNumber = setpointNumber + 1
      lastTime = aTime
    }
  })
  
  /* night setpoint when early hours of morning */
  if( setpointNumber == 0 ){
  	setpointNumber = 4
  }

  var org.openhab.core.items.GroupItem setPoints = settings.members.filter( m | m.name.startsWith('zoneSettingsSetpoints')).last
  var int newSetpoint = ( setPoints.members.filter( m | m.name.startsWith('set' + setpointNumber)).last.state as DecimalType )
  var org.openhab.core.library.items.NumberItem currentSetpoint = settings.members.filter( m | m.name.startsWith('setCurrent')).last

  if( settings.members.filter( m | m.name.startsWith('sw')).last.state == OFF ){
  	newSetpoint = 10          // set the valve to 10 degrees, which we treat as effectively off
  }

  logDebug( 'zones', 'Calculated setpoint for zone ' + zone.name + ' is setpointNumber ' + setpointNumber + ' with value of ' + newSetpoint + ', old setpoint was ' + currentSetpoint.state )

  if( currentSetpoint.state == Uninitialized || newSetpoint != (currentSetpoint.state as DecimalType) ) {
    postUpdate( currentSetpoint, newSetpoint )
  }
]


rule "Roll current zone setpoints down from timed settings"
when
  Time cron "5 * * * * ?"
then
  logDebug( 'zones', 'Running zone settings' )
  Zones?.members.forEach( zone | {
    setZoneSetpoint.apply( zone )
  })
end

You should see your zone settings applied to the current setpoint every minute.

Next, we want to take the zone setpoint and apply it to the device setpoints. We want to do this only when the zone setpoint changes – then if someone has manually overridden the temp on one of the devices it’ll only return to the programmed value at the next timed setting change, rather than at the next minute.

Create a file /etc/openhab/configurations/rules/device_setpoints_from_zones.rules:

import org.openhab.core.library.types.*
import org.openhab.core.library.items.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.openhab.core.library.types.DecimalType
import org.joda.time.*

/*
  This set of rules takes the zone setpoints (when they change) and cascades those
  changes down to the individual rooms.  It only triggers when zone setpoints actually
  change to avoid overwriting temporary settings from a wall thermostat other than when
  the zone setting actually changed

  The core function takes in a zoneGroup, which must have a zoneSettings group and
  a zoneDevices group within it.  
*/


val org.eclipse.xtext.xbase.lib.Functions$Function1 setDeviceSetpoint = [
    org.openhab.core.items.GroupItem zone |

  /* find the zoneSetpoint and the zoneDevices in the zone */
  var org.openhab.core.items.GroupItem zoneSettings = zone.members.filter( m | m.name.startsWith('zoneSettings')).last
  var org.openhab.core.items.GroupItem zoneDevices = zone.members.filter( m | m.name.startsWith('zoneDevices')).last
  var org.openhab.core.items.GroupItem zoneCurrentSetpoint = zoneSettings.members.filter( m | m.name.startsWith('setCurrent')).last

  /* for each device, check whether the setpoint is different than the zone setpoint, 
     if it is then update it to the zone setpoint  */
  zoneDevices.members.forEach( device | {
  	var org.openhab.core.items.GroupItem aDevice = device
    var org.openhab.core.library.items.NumberItem deviceSetpoint = aDevice.members.filter( m | m.name.endsWith('Setpoint')).last
      
    logDebug( 'devices', 'Reviewing device: ' + aDevice.name + ' and considering deviceSetpoint: ' + deviceSetpoint + ' as compared to zone setpoint ' + zoneCurrentSetpoint )
  
    if( deviceSetpoint.state == Uninitialized || ( deviceSetpoint.state as DecimalType) != (zoneCurrentSetpoint.state as DecimalType) ) {
      sendCommand( deviceSetpoint, (zoneCurrentSetpoint.state as DecimalType) )
    }
  })
]



rule "Reset device setpoints when any zone setpoint changes"
when
  Item ZoneSetpoints changed
then
  logDebug( 'devices', 'Calculating device setpoints')
  Zones.members.forEach( zone | {
    setDeviceSetpoint.apply( zone )
  })
end

rule "Reset device setpoints when restart - leave time first for persistence to restore"
when
  System started
then
  logDebug ( 'devices', 'starting timer for initialising' )
  var startTimer = createTimer( now().plusSeconds(120) ) [ | 
    logDebug ( 'devices', 'executing timer for initialising' )
  	Zones.members.forEach( zone | {
      logDebug( 'devices', 'Calculating device setpoints 2 minutes after startup for zone ' + zone.name )
  	  setDeviceSetpoint.apply( zone )
  	})
  ]
end

You should now see your devices starting to inherit the setpoint from the zone. Note that there is sometimes a lag for the various devices to update, particularly if you have a lot of devices.

Finally, we want the rules that control the boiler pump. These rules decide when to turn the boiler on and off. Create a file /etc/openhab/configurations/rules/pump_timers.rules:

import org.openhab.core.library.types.*
import org.openhab.core.library.items.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import org.openhab.core.library.types.DecimalType
import org.joda.time.*
import java.util.ArrayList

var org.openhab.model.script.actions.Timer pumpTimer

/*
  This set of rules notices when a valve opening has changed, and manages the timers
  for running the boiler pump appropriately.
  
  The logic is that when there are only a few valves open, and those valves open only a small
  amount, then we don't actually want to run the boiler until hot water returns - we want our
  heaters to get a smallish amount of hot water, then wait while that heat percolates into the
  room.  
  
  When there is a reasonable amount of valve opening, then we probably want to just run the boiler
  till all the heaters demanding heat are hot.
  
  Firstly, let's deal with changes to valve openings.  Any time a valve opening changes, we need
  to evaluate our state.  
    - If the aggregate valve opening is greater than the autoRunThreshold, then set mode to ON, and exit
    - If the aggregate valve opening is zero, then we set mode to OFF, and exit
    - If the mode is WAIT, do nothing - we'll re-evaluate when the wait timer finishes.
    - If the mode is RUN, do nothing, we'll pick it up in the next run cycle.
    - If the mode is something else, then set it to RUN.  Something else could mean OFF or ON, or could
      mean undefined
  
  Next, the timer ends.  
    - If the timer ends and the mode was WAIT, then set mode to RUN
    - If the timer ends and the mode was RUN, then set mode to WAIT

  When the mode changes to ON, cancel any timer that is running, set the pump to ON
  When the mode changes to OFF, cancel any timer that is running, set the pump to OFF
  When the mode changes to WAIT, cancel any timer that is running (log a warning, as there shouldn't
  be one), and set a WAIT timer for the specified wait seconds.
  When the mode changes to RUN, cancel any timer that is running (log a warning, as there shouldn't
  be one) and set a RUN timer for the desired run seconds. 
  
  The pump run time is based on a configured minimum run time (in theory, about filling the pipes with hot
  water) plus how many valves are open / how much they are open times the pump run factor.
  
  The pump wait time is a constant from the configuration.
  */

rule "Adjust timers whenever the amount of valve opening changes"
when
  Item DeviceValves changed
then
  logDebug( 'pump', 'Valve opening changed, deciding what to do')

  if( ( DeviceValves.state as DecimalType ).intValue == 0 ) {
  	postUpdate( stringPumpMode, "OFF" )
    logDebug( 'pump', 'No valves open, turning pump OFF')
  } 
  else if( ( DeviceValves.state as DecimalType ).intValue >= ( numberPumpAutoRunThreshold.state as DecimalType ).intValue ) {
  	postUpdate( stringPumpMode, "ON" )
    logDebug( 'pump', 'Valves greater than threshold, turning pump ON')
  } 
  else if ( stringPumpMode.state == "WAIT" ) {
    logDebug( 'pump', 'Already waiting, do nothing, valve position will be taken into account on next run')
  } 
  else if ( stringPumpMode.state == "RUN" ) {
    logDebug( 'pump', 'Already running, do nothing, valve position will be taken into account on next run')
  } 
  else {
    logDebug( 'pump', 'Pump state was ON, OFF or undefined, change to RUN')
  	postUpdate( stringPumpMode, "RUN" )
  }
end

rule "Pump mode changed to OFF"
when
  Item stringPumpMode changed to "OFF"
then
  logDebug( 'pump', 'Mode changed to OFF')
  if( pumpTimer != null ){
    pumpTimer.cancel()
    pumpTimer = null
    logDebug( 'pump', 'Timer was present, cancelled it')
  }
  postUpdate( swPump, OFF )
  logDebug( 'pump', 'Pump turned OFF')
end

rule "Pump mode changed to ON"
when
  Item stringPumpMode changed to "ON"
then
  logDebug( 'pump', 'Mode changed to ON')
  if( pumpTimer != null ){
    pumpTimer.cancel()
    pumpTimer = null
    logDebug( 'pump', 'Timer was present, cancelled it')
  }
  postUpdate( swPump, ON )
  logDebug( 'pump', 'Pump turned ON')
end

rule "Pump mode changed to WAIT"
when
  Item stringPumpMode changed to "WAIT"
then
  logDebug( 'pump', 'Mode changed to WAIT')
  if( pumpTimer != null ){
    pumpTimer.cancel()
    pumpTimer = null
    logDebug( 'pump', 'Timer was present, cancelled it')
  }

  if( swPump.state == ON ){
    postUpdate( swPump, OFF )
  	logDebug( 'pump', 'Pump was on, turned it off')
  }

  var int pumpWaitSeconds = (numberPumpWaitSeconds.state as DecimalType).intValue
  postUpdate( stringTimerEnd, now().plusSeconds( pumpWaitSeconds ).toString('H:m:s') )
  pumpTimer = createTimer( now().plusSeconds( pumpWaitSeconds ) ) [ | {
    logDebug( 'pump', 'End of pump wait timer')
    postUpdate( stringPumpMode, "RUN" )
    logDebug( 'pump', 'Set mode to RUN')
  }]
end

rule "Pump mode changed to RUN"
when
  Item stringPumpMode changed to "RUN"
then
  logDebug( 'pump', 'Mode changed to RUN')
  if( pumpTimer != null ){
    pumpTimer.cancel()
    pumpTimer = null
    logDebug( 'pump', 'Timer was present, cancelled it')
  }

  var int pumpRunSeconds = ( numberPumpRunBaseSeconds.state as DecimalType ).intValue + ( ( DeviceValves.state as DecimalType ).intValue * ( numberPumpRunFactor.state as DecimalType).intValue ) / 100
  postUpdate( stringTimerEnd, now().plusSeconds( pumpRunSeconds ).toString('H:m:s') )
  pumpTimer = createTimer( now().plusSeconds( pumpRunSeconds ) ) [ | {
    logDebug( 'pump', 'End of pump run timer')
    postUpdate( stringPumpMode, "WAIT" )
    logDebug( 'pump', 'Set mode to WAIT')
  }]
end

Finally, we should add configurables to the logging subsystem to allow us to individually debug each of our rules. In the example below we’ve set the pump rules to debug, all other rules to info. Edit /etc/openhab/logback.xml to add the following blocks:

    
        
    
    
        
    
    
        
    
    
        
    
    
        
    

And that should be it for the rule sets.

You should be able to go to the settings within the UI, and manually change the valve opening for the Master Bedroom Valve, which in turn should turn the pump on and off.

Advertisements

2 thoughts on “Openhab Heating Configuration

  1. Pingback: Using Pi and OpenHab to control radiators and watering | technpol

  2. ahha PaulL, I could not fathom how to warm my home using above methods. So I came to Bangkok instead. Don’t even need the laptop to heat up Bangkok girl .

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s