The days are getting shorter and the temperatures are slowly dropping. Summer is almost over on the northern hemisphere and with that many have to once again think about keeping their feet warm.
A normal person would turn a thermostat or crank up the fireplace - not the Home Automation Enthusiast!
A fully automated heating schedule system is one of the key components of every home automation setup. In this tutorial I’m going to guide you through one possible implementation of a heating automation using openHAB.
TL;DR; - Skip down to “Final Result” to find the configuration files used by this tutorial.
FIRST DRAFT
Please keep in mind that the tutorial is not quite finished and a few details might be missing. I will be traveling next week and will have no time to continue. I decided to post it rather sooner than later. Comments are welcome!
Side Note: Recommended Temperatures
In general we can agree, that everyone has their own preference regarding the ideal temperature in a specific room. Vast recommendations and studies online suggest slightly different settings. The majority of them agree on a few ground rules:
- Do not over-heat. You are burning money and the human body doesn’t need the extra warmth. Only increase to comfortable temperatures.
- Only heat where needed. The hallway or closet don’t have to be cozy. A warm bedroom is not ideal during sleep. Plan your temperature schedules per room.
- Do not let your home cool out. Heating up cold furniture and walls costs more energy than keeping the temperature at a certain level for a few hours.
- Do not go below a certain point. Even when gone for a longer time the temperature needs to stay stay above a certain temperature. Otherwise pipes can freeze or mold can develop in moist corners.
Remember: Temperature and time settings and the benefits and risks as well as potential savings in energy costs depend on your home, your neighbors and your families behavior. It’s not easy to define a perfect schedule but some field testing will yield quick results
For the purpose of this tutorial we are going to work with the following temperatures:
- 21°C - Comfortable temperature for rooms we spend time in
- 19°C - Comfortable temperature for rooms we are in shorter periods
- 17°C - Normal temperature to hold during the day when nobody is home
- 15°C - Lowered temperature when nobody is home for a few days
- 13°C - Lowered temperature when nobody is home for a longer period
openHAB Implementation
The following parts of the tutorial describe the implementation of the Heating Boilerplate in your home. As each home is unique and everyone has different heating preferences, you’ll need to adapt, expand and remove parts of the example as you go along.
Rationale: The solution presented here is held at a complexity that allows for easy comprehension and reproduction. Many aspects about the way temperatures are configured or temporarily manipulated could be improved, which in return would limit the ease of use. The tutorial was planned as a guide for new openHAB users. Feel free to extend on the ideas and post your results in the comments below.
Prerequisite: Heating Actuators
Your heater actuators, thermostat or heating controls need to be connected to openHAB via one of the countless Bindings or any other supported method. For this example, let’s assume the following Items are able to control your target temperature:
Number LR_Heating_TargetTemp "Livingroom Heating Target [%.1f °C]" { /*...some binding config */ }
Number BE_Heating_TargetTemp "Bedroom Heating Target [%.1f °C]" { /*...some binding config */ }
Number BA_Heating_TargetTemp "Bathroom Heating Target [%.1f °C]" { /*...some binding config */ }
Many available devices bringing your heating into openHAB will already perform a temperature control internally, leaving us with a “Target Temperature” channel to set the target of the control hysteresis. However not all solutions might provide that comfort, please see below to find a Temperature Control Hysteresis algorithm implemented in openHAB. (TODO)
For further automation ideas or nice UI presentations other properties of your heaters (like current temperature or the valve setting) might be needed as items. For the pure heating schedule presented below the target temperature items are all that matters.
Introducing Heating Modes
The main idea of the Heating Boilerplate is to have one setup-wide heating mode setting. The selected mode will control all individual room target temperatures. A virtual item will serve that purpose:
String Heating_Mode "Global Heating Mode [%s]"
Everyone of the selectable modes has its special use case and the Boilerplate is prepared in a way so it can easily be expanded by new modes or operated in combination with other solutions (like a calendar or location based interruption).
Main Mode: Normal Heating Schedule
The Boilerplate includes one Normal heating schedule that - under normal circumstances - will make sure that rooms are warm when they need to be. The idea is not new and can be found in most other automation solutions. The approach works for most households and is probably what you want to use during a normal week.
Don’t worry, exceptions for either certain times or a single rooms can still be mixed in during Normal mode and examples will be shown below.
If you believe to need more than one regular schedule, the idea of a “normal” mode is easily replicated for a second or third set of times and temperatures.
Preset Temperature Items
First we need to define virtual Items for the target temperature during the Normal heating mode. These will be updated by rules to reflect the desired target temperature throughout the day. The usage of additional Items decouples the schedule from the actual heating mode currently active on the individual heaters. This step gives us more flexibility.
Create one preset temperature item per heater/room:
Number LR_Heating_PresetTempNormal "Livingroom Heating Preset (Normal Mode) [%.1f °C]"
Number BE_Heating_PresetTempNormal "Bedroom Heating Preset (Normal Mode) [%.1f °C]"
Number BA_Heating_PresetTempNormal "Bathroom Heating Preset (Normal Mode) [%.1f °C]"
Furthermore we are going to define one single switch Item that will trigger subsequent procedures and consequently effect the heating actuators or controllers:
Switch Heating_UpdateHeaters "Send Target Temperatures to Heaters"
Schedule Definition
The next step is to define the actual time-based normal heating schedule. We are going to use the easiest way openHAB allows us to so, via cron-triggered rules.
Three rules are enough to schedule a normal workday in this example:
▼ 1. Crank heating up after work (17:00)
rule "17:00"
when
Time cron "0 0 17 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(21.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(20.0)
Heating_UpdateHeaters.sendCommand(ON)
end
2. Increase temperatures in the evening (20:30)
rule "20:30"
when
Time cron "0 30 20 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(22.0)
BE_Heating_PresetTempNormal.postUpdate(19.0)
BA_Heating_PresetTempNormal.postUpdate(20.0)
Heating_UpdateHeaters.sendCommand(ON)
end
3. Lower heating temperatures at bedtime (23:30)
rule "23:30"
when
Time cron "0 30 23 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(17.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(17.0)
Heating_UpdateHeaters.sendCommand(ON)
end
The weekends are a bit special but can be easily covered by reuse and extension of the rules above. In the example below we will simply add a higher temperature during the day.
4. On Saturday and Sunday: Comfortable temperatures during the day
rule "9:00, weekend"
when
Time cron "0 0 9 ? * SAT-SUN *"
then
LR_Heating_PresetTempNormal.postUpdate(21.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(21.0)
Heating_UpdateHeaters.sendCommand(ON)
end
Yet another example I personally enjoy quite a lot:
5. Weekday: Cozy bathroom in the morning
rule "8:00, weekday, bathroom"
when
Time cron "0 0 8 ? * MON-FRI *"
then
BA_Heating_PresetTempNormal.postUpdate(23.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "9:00, weekday, bathroom"
when
Time cron "0 0 9 ? * MON-FRI *"
then
BA_Heating_PresetTempNormal.postUpdate(17.0)
Heating_UpdateHeaters.sendCommand(ON)
end
And that’s it. A complete everyday week accompanied by a heating schedule that “just works”.
You might be disappointed that the “heating schedule” tutorial for your smart home ended at such a stiff schedule. You are right and that is where the other heating modes and the exceptions come into play!
Special Heating Modes
The Normal heating mode is a good fit for a normal week. However life is not always normal. We should prepare for times in which we want the heating to stay off over the course of a weekend trip or to stay on when we are home sick.
The Boilerplate considers the following heating modes:
- NORMAL - The normal heating mode presented above
- PARTY - Hold temperatures for a longer time and in more rooms, reset to NORMAL during the night
- SICKDAY - Heat to a higher temperature throughout the day, reset to NORMAL during the night
- WEEKEND_TRIP - Hold a normal temperature during the weekend, reset to NORMAL after
- AWAY - For vacations or longer trips the temperature is lowered to a safe temperature, no reset
- OFF_SUMMER - All heating is turned off
The algorithm to switch between these modes and to send target temperature to the actual heating actuators is now pretty simple:
rule "React on heating mode switch, send target temperatures"
when
Item Heating_Mode received update or
Item Heating_UpdateHeater received command ON
then
Heating_UpdateHeater.postUpdate(OFF)
logInfo("heating_mode.rules", "Heating Mode: " + Heating_Mode.state)
switch Heating_Mode.state {
case "NORMAL": {
LR_Heating_TargetTemp.sendCommand(LR_Heating_PresetTempNormal.state as Number)
BE_Heating_TargetTemp.sendCommand(BE_Heating_PresetTempNormal.state as Number)
BA_Heating_TargetTemp.sendCommand(BA_Heating_PresetTempNormal.state as Number)
}
case "PARTY": {
LR_Heating_TargetTemp.sendCommand(21.0)
BE_Heating_TargetTemp.sendCommand(15.0)
BA_Heating_TargetTemp.sendCommand(19.0)
}
case "SICKDAY": {
LR_Heating_TargetTemp.sendCommand(23.0)
BE_Heating_TargetTemp.sendCommand(19.0)
BA_Heating_TargetTemp.sendCommand(23.0)
}
case "WEEKEND_TRIP": {
LR_Heating_TargetTemp.sendCommand(15.0)
BE_Heating_TargetTemp.sendCommand(15.0)
BA_Heating_TargetTemp.sendCommand(15.0)
}
case "AWAY": {
LR_Heating_TargetTemp.sendCommand(13.0)
BE_Heating_TargetTemp.sendCommand(13.0)
BA_Heating_TargetTemp.sendCommand(13.0)
}
case "OFF_SUMMER": {
LR_Heating_TargetTemp.sendCommand(0.0)
BE_Heating_TargetTemp.sendCommand(0.0)
BA_Heating_TargetTemp.sendCommand(0.0)
}
default : { logError("heating_mode.rules", "Heating Mode unknown: " + Heating_Mode.state) }
}
end
The last detail missing now is the automatic switch from some of these modes back to Normal mode at a certain time.
This should be a no-brainer by now.
rule "End PARTY and SICKDAY mode at 2:00 in the night"
when
Time cron "0 0 2 ? * * *"
then
if (Heating_Mode.state == "PARTY" || Heating_Mode.state == "SICKDAY") {
Heating_Mode.postUpdate("NORMAL")
}
end
rule "End WEEKEND_TRIP mode at 13:00 on Monday"
when
Time cron "0 0 13 ? * MON *"
then
if (Heating_Mode.state == "WEEKEND_TRIP") {
Heating_Mode.postUpdate("NORMAL")
}
end
Initialization and Restore of virtual Items
Now that the heating schedule and the heating modes are implemented, we should take care of the case when openHAB is started or the openHAB configuration reloaded. That should be considered with all rules to avoid unexpected states and misbehavior.
After the start up, a restart or a reload of openHAB, all last Item states are lost. Items bound to a Binding will normally be reinitialized by the Binding within seconds but Items without a Binding channel will not be initialized (to be technically correct speaking, they are initialized as NULL
).
We are going to apply two concepts to make sure the introduced virtual Items are in a meaningful state whenever possible. A restoreOnStartup
persistence strategy (e.g. utilizing mapDB) is used to reset a previous state:
Strategies {
default = everyUpdate
}
Items {
Heating_Mode : strategy = everyUpdate, restoreOnStartup
Heating_PresetNormal_Group* : strategy = everyUpdate, restoreOnStartup
}
If no old state is know by the system (e.g. when first set up) a System started
rule is used to initialize Items with safe values:
rule "Initialize uninitialized virtual Items"
when
System started
then
createTimer(now.plusSeconds(180)) [ |
if (Heating_Mode.state == NULL) Heating_Mode.postUpdate("NORMAL")
Heating_PresetNormal_Group.members.filter[item | item.state == NULL].forEach[item | item.postUpdate(19.0)]
]
end
Final Result
Here’s an example how a sitemap presenting all data and controls could look like. I might eventually post it as well. Ask me about it if you are interested.
To get the solution working in your setup, please create the following files with the provided content and apply the mentioned modifications:
Items Configuration File
heating_mode.items
Number LR_Heating_TargetTemp "Livingroom Heating Target [%.1f °C]" { /*...some binding config */ }
Number BE_Heating_TargetTemp "Bedroom Heating Target [%.1f °C]" { /*...some binding config */ }
Number BA_Heating_TargetTemp "Bathroom Heating Target [%.1f °C]" { /*...some binding config */ }
String Heating_Mode "Global Heating Mode [%s]"
Switch Heating_UpdateHeaters "Send Target Temperatures to Heaters"
Group Heating_PresetNormal_Group
Number LR_Heating_PresetTempNormal "Livingroom Heating Preset (Normal Mode) [%.1f °C]" (Heating_PresetNormal_Group)
Number BE_Heating_PresetTempNormal "Bedroom Heating Preset (Normal Mode) [%.1f °C]" (Heating_PresetNormal_Group)
Number BA_Heating_PresetTempNormal "Bathroom Heating Preset (Normal Mode) [%.1f °C]" (Heating_PresetNormal_Group)
- Search&Replace
LR_Heating_*
and so on by your own device/room names - Duplicate
LR_Heating_*
lines for more devices/rooms in your home
Rules Configuration File
heating_mode.rules
val String filename = "heating_mode.rules"
rule "Initialize uninitialized virtual Items"
when
System started
then
createTimer(now.plusSeconds(180)) [ |
logInfo(filename, "Executing 'System started' rule for Heating")
if (Heating_Mode.state == NULL) Heating_Mode.postUpdate("NORMAL")
Heating_PresetNormal_Group.members.filter[item | item.state == NULL].forEach[item | item.postUpdate(19.0)]
]
end
rule "React on heating mode switch, send target temperatures"
when
Item Heating_Mode received update or
Item Heating_UpdateHeater received command ON
then
Heating_UpdateHeater.postUpdate(OFF)
logInfo(filename, "Heating Mode: " + Heating_Mode.state)
switch Heating_Mode.state {
case "NORMAL": {
LR_Heating_TargetTemp.sendCommand(LR_Heating_PresetTempNormal.state as Number)
BE_Heating_TargetTemp.sendCommand(BE_Heating_PresetTempNormal.state as Number)
BA_Heating_TargetTemp.sendCommand(BA_Heating_PresetTempNormal.state as Number)
}
case "PARTY": {
LR_Heating_TargetTemp.sendCommand(21.0)
BE_Heating_TargetTemp.sendCommand(15.0)
BA_Heating_TargetTemp.sendCommand(19.0)
}
case "SICKDAY": {
LR_Heating_TargetTemp.sendCommand(23.0)
BE_Heating_TargetTemp.sendCommand(19.0)
BA_Heating_TargetTemp.sendCommand(23.0)
}
case "WEEKEND_TRIP": {
LR_Heating_TargetTemp.sendCommand(15.0)
BE_Heating_TargetTemp.sendCommand(15.0)
BA_Heating_TargetTemp.sendCommand(15.0)
}
case "AWAY": {
LR_Heating_TargetTemp.sendCommand(13.0)
BE_Heating_TargetTemp.sendCommand(13.0)
BA_Heating_TargetTemp.sendCommand(13.0)
}
case "OFF_SUMMER": {
LR_Heating_TargetTemp.sendCommand(0.0)
BE_Heating_TargetTemp.sendCommand(0.0)
BA_Heating_TargetTemp.sendCommand(0.0)
}
default : { logError(filename, "Heating Mode unknown: " + Heating_Mode.state) }
}
end
// ========================
// mode resets
rule "End PARTY and SICKDAY mode at 2:00 in the night"
when
Time cron "0 0 2 ? * * *"
then
if (Heating_Mode.state == "PARTY" || Heating_Mode.state == "SICKDAY") {
Heating_Mode.postUpdate("NORMAL")
}
end
rule "End WEEKEND_TRIP mode at 13:00 on Monday"
when
Time cron "0 0 13 ? * MON *"
then
if (Heating_Mode.state == "WEEKEND_TRIP") {
Heating_Mode.postUpdate("NORMAL")
}
end
// ========================
// NORMAL schedule
rule "17:00"
when
Time cron "0 0 17 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(21.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(20.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "20:30"
when
Time cron "0 30 20 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(22.0)
BE_Heating_PresetTempNormal.postUpdate(19.0)
BA_Heating_PresetTempNormal.postUpdate(20.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "23:30"
when
Time cron "0 30 23 ? * * *"
then
LR_Heating_PresetTempNormal.postUpdate(17.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(17.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "9:00, weekend"
when
Time cron "0 0 9 ? * SAT-SUN *"
then
LR_Heating_PresetTempNormal.postUpdate(21.0)
BE_Heating_PresetTempNormal.postUpdate(17.0)
BA_Heating_PresetTempNormal.postUpdate(21.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "8:00, weekday, bathroom"
when
Time cron "0 0 8 ? * MON-FRI *"
then
BA_Heating_PresetTempNormal.postUpdate(23.0)
Heating_UpdateHeaters.sendCommand(ON)
end
rule "9:00, weekday, bathroom"
when
Time cron "0 0 9 ? * MON-FRI *"
then
BA_Heating_PresetTempNormal.postUpdate(17.0)
Heating_UpdateHeaters.sendCommand(ON)
end
- Search&Replace
LR_Heating_*
and so on by your own device/room names - Duplicate
LR_Heating_*
lines for more devices/rooms in your home - Adapt schedule times and temperatures to your needs
Persistence Configuration File
mapdb.persist
Strategies {
default = everyUpdate
}
Items {
Heating_Mode : strategy = everyUpdate, restoreOnStartup
Heating_PresetNormal_Group* : strategy = everyUpdate, restoreOnStartup
}
Sitemap Configuration File
Add the following to your existing sitemap file:
myhome.sitemap
Selection item=Heating_Mode label="Heating Mode []" mappings=[NORMAL="Normal",
PARTY="Party",
SICKDAY="Sick day at home",
WEEKEND_TRIP="Gone for the weekend",
AWAY="Adventure Time!",
OFF_SUMMER="Off (Summer standby)"
]
I hope this tutorial was helpful for you. Enjoy your warm smart home and as always: Happy Hacking!