Yet another Heating Setup

Finished my updated Heating setup which I’m happy about, so let’s share some ideas.
My setup has got main unit which is heating 2floors with approx 16 radiators or so. I’ve hooked Wemos D1 mini with relay shield and DHT22 with Tasmota onto it so I can control it via MQTT or OH or Tasmota itself.
I have set of thermometers in each room, bathroom, hall etc. and I do have set of measured rooms which then are used as actual temperature for Heating setup.
Obviously some rooms/halls are not included as those are irelevant or not wanted due to great temperature fluctuancy, eg entrance hall, bathroom etc.

I don’t want to have rooms independently turn on/off boiler as it will result in constant switching on/off and therefore I’m not using "smart’ thermo couplers on each radiator but dumb ones configured to roughly 24C.

My usecase contains basically four things which may occur

  • winter season (eg. actively heating)
    ** time scheduled heat/night/away mode
  • summer season (eg. standby)
  • vacation
  • manual override

during vacation and summer mode there is hardconfigured threshold which is maintained while on AUTO as we usually don’t want to have lower temperature than 17 or 15 no matter which part of the year we are at.
MANUAL mode will override any normal setting or season and so it will heat up house to target temperature even during summer.
Manual mode is maintaining target temperature +.3 and -.5 (configurable in rules) and lower temperature during BED time.
Manual mode is switched to AUTO at 2am each day, as usually you wanna manually override Heating only for limited period of time.

For automatic mode I’m using timepicker (customized css to match my dark mode) for convenient scheduling during Winter season.

On sitemap I’m hiding controls which are not necessary when switching heating modes so users are not distracted by stuff which is not used in given setting.

Lowest/Average switch is changing actual temperature which is being used in rules. It’s because sometimes you wanna heat up coldest room to target temperature and sometimes average is enought when you have open window somewhere and so on.
Lowest switch is as well used when my fireplace in livingroom is used, as livingroom is monitored room and with fireplace average will be much more higher than other rooms.

And finally in rules I’m using modified

I wanted to have my ruleset as clean and simple as possible, which I believe in current state I have :wink:
Indeed I’m tweaking and enhancing system on the go, but current setup is flexible and covers all my family needs already.

few screens:

heating.items

/* HEATING measured rooms */
Group:Number:AVG    HeatingHouseTemp    "Rooms Average [%.1f °C]"   <temperature>
Group:Number:MIN    HeatingHouseMin     "Rooms Lowest [%.1f °C]"    <temperature>

/* heating */
Switch  Heating                 "Heating [%s]"                      <fire>      (gStoreChange)  { channel="mqtt:topic:Heating:heating" }

String  Heating_Plan            "Schedule [%s]"                     <none>      (gStoreChange)
Switch  Heating_Mode            "Mode []"                           <none>      (gStoreChange)
Switch  Heating_Style           "Heating style []"                  <none>      (gStoreChange)

// temperatures
Number  Heating_Temp_Manual     "Target Temperature [%.1f °C]"      <heating>   (gStoreChange)


Number  Heating_Temp_Day        "Target Temperature [%.1f °C]"      <heating>   (gStoreChange)
Number  Heating_Temp_Night      "Night [%.1f °C]"                   <heating>   (gStoreChange)
Number  Heating_Temp_Away       "Away [%.1f °C]"                    <heating>   (gStoreChange)

// helpers
String  Heating_AutoState       (gStoreChange)
Switch  Heating_Visibility      (gStoreChange)

.sitemap

/* HEATING */
		Text item=Heating {
			Frame label="General" {
				Text 		item=Heating		label="Heating Status [%s]"
				Selection 	item=Heating_Plan 	label="Schedule [%s]"			mappings=[AWAY="Vacation",WINTER="Heating",SUMMER="Summer (stand by)"]
				Switch 		item=Heating_Mode	label="Operation Mode []"		mappings=[ON="Manual", OFF="Auto"]
				Switch		item=Heating_Style									mappings=[ON="Lowest", OFF="Average"]

				Text item=HeatingHouseTemp
				Text item=HeatingHouseMin
			}
			
			Frame label="Manual Setting" visibility=[Heating_Visibility=="ON"]{
				Setpoint	item=Heating_Temp_Manual 		minValue=15 maxValue=25 step=0.5
			}

			Frame label="Automatic Setting" visibility=[Heating_Visibility=="OFF"] {
				Setpoint	item=Heating_Temp_Day		minValue=15 maxValue=25 step=0.5
				Text label="" icon="none"
				Setpoint	item=Heating_Temp_Night		minValue=15 maxValue=25 step=0.5
				Setpoint	item=Heating_Temp_Away		minValue=15 maxValue=25 step=0.5

				Webview		url="http://ohserver:8888/static/time-line-picker/index.html?ip=ohserver:8888&dark=yes&transferItem=TransferItem1&states=Night,Away,Heating&yAxisLabel=1,2,3,4,5,67&colorset=666666,d9c468,8ed968" height=13 icon="none"
			}
		}

and .rules

/* Heating override: manual temperature */
rule 'Heating logic'
when
    Item HeatingHouseTemp changed or
    Item Heating_Temp_Manual changed or
    Item Heating_Mode changed or
    Item Heating_Plan changed
then
    var Number actualTemp = 0
    var Number targetTemp = 0
    var Number lowerTemp = 0

    // select if average or lowest temperature is monitored
    if (Heating_Style.state == ON)  actualTemp = (HeatingHouseMin.state as Number).floatValue   // lowest
    else                            actualTemp = (HeatingHouseTemp.state as Number).floatValue  // average

    // manual override
    if (Heating_Mode.state == ON) {
        // toggle sitemap items
        Heating_Visibility.sendCommand(ON)

        val wantedTemp  = (Heating_Temp_Manual.state as Number).floatValue

        if(vTimeOfDay.state == "BED"){
            targetTemp = wantedTemp - 1.5       // 22 => 20.5
            lowerTemp  = wantedTemp - 3.5       // 22 => 18.5
        }else{
            targetTemp = wantedTemp + 0.3       // 22 => 22.3
            lowerTemp  = wantedTemp - 0.5       // 22 => 21.5
        }

    // auto mode
    } else {
        // toggle sitemap items
        Heating_Visibility.sendCommand(OFF)

        if (Heating_Plan.state == "WINTER"){
        // winter    
            val mode = Heating_AutoState.state.toString
            val dayTemp     = (Heating_Temp_Day.state as Number).floatValue
            val nightTemp   = (Heating_Temp_Night.state as Number).floatValue
            val awayTemp    = (Heating_Temp_Away.state as Number).floatValue

            if (mode == "Heating"){
                targetTemp  = dayTemp + 0.3
                lowerTemp   = dayTemp - 0.5

            }else if (mode == "Night"){
                targetTemp  = nightTemp + 0.2
                lowerTemp   = nightTemp - 0.7
            
            }else{
                targetTemp  = awayTemp + 0.2
                lowerTemp   = awayTemp - 0.7
            
            }
        }else if (Heating_Plan.state == "AWAY"){
        // away
            targetTemp  = 15
            lowerTemp   = 13.5
        }else{
        // summer
            targetTemp  = 17
            lowerTemp   = 15
        }
    }

    // temp is higher, switch OFF
    if (actualTemp > targetTemp && Heating.state != OFF){
        Heating.sendCommand(OFF)

        logInfo("Heating", actualTemp.toString + " is over " + targetTemp.toString + ", Heating OFF")
    }
    // temp is lower, switch ON
    if (actualTemp <= lowerTemp && Heating.state != ON){
        Heating.sendCommand(ON)

        logInfo("Heating", actualTemp.toString + " reached lower limit of " + targetTemp.toString + ", Heating ON")
    }
end

rule "End Manual Heating mode at 2am"
when
    Time cron "0 0 2 ? * * *"
then
    Heating_Mode.sendCommand(OFF)
end

Ideas for even cleaner rules always welcomed :wink:
Thanks

5 Likes

Cool setup. How does that schedule component (the days of the week thing) translate to an item and/or times?

I’m using helper string item Heating_AutoState which is being populated by timepicker component with values Night, Away or Heating
and in rules, based on which states it is I’m assigning desired temperature for comparison with actual measured temperature

What to the values of Heating_AutoState look like? So if it’s a black area of the schedule the component set s the value to “Night” and in a green period it’s set to “Heating”?

My heating has a lot more rules, yours is nice and simple…but mine is set with one sort of automatic period that ignores if it’s the weekend etc…and I’d like to add that.

it’s String, so “Night” is being populated to the Heating_AutoState when it’s in the dark area, “Heating” when in green etc.

It’s customizable, you can use 0 1 2 if you like… Rules have to be changed accordingly then

if(Heating_AutoState.state == "Heating"){
// do stuff
}

Hmm seems I now have a weekend project to put in the timeline picker…

NOTE: some of these suggestions are mutually exclusive.

  • You could benefit from the trinary operator.

  • Avoid the use of primitives unless absolutely necessary. Use of primitives is known to greatly extend the amount of time it takes .rules files to parse on an RPi. Just case Item states to Numbers and you can safely do all the necessary operations (comparisons, addition, etc) without problem.

    val actualTemp = (if(Heating_Style.state == ON) HeatingHouseMin.state else HeatingHouseTemp.state) as Number
  • This is more of a style thing, but I find the code easier to follow and shorter if you initialize your variables to a reasonable default to start and then your conditions are checking whether the default values need to be changed. Often you can eliminate 30% or more of if/else or switch cases by doing this. Using switch statements can also help with code clarity.
    // Initialized to summer values
    var targetTemp = 17
    var lowerTemp = 15
...
    // No longer require the summer else case
  • If you assign a value to a variable when you create it instead of null, you do not and should not supply the Type. as with the use of primitives, over specifying the Type is known to extend .rules file parsing by minutes.

  • Look at Design Pattern: How to Structure a Rule

  • Name your Heating_Temp Items the same as your modes and you can use Design Pattern: Associated Items to get the right Temp Item.

import org.eclipse.smarthome.model.script.ScriptServiceUtil

rule "Heating logic"
when
    Item HeatingHouseTemp changed or
    Item Heating_Temp_Manual changed or
    Item Heating_Mode changed or
    Item Heating_Plan changed
then
    // 1. See if you need to run the Rule at all
    // You should add some checks for UNDEF and NULL for all your Items used in this Rule here
    // and return; if so. Exercise left to the student.

    // 2. Calculate what needs to be done.
    var actualTemp = (if(Heating_Style.state == ON) HeatingHouseMin.state else HeatingHouseTemp.state) as Number

    var baseTemp = 0
    var upper = 0
    var lower = 0

    if(Heating_Mode.state == ON){
        baseTemp = Heating_Temp_Manual.state as Number
        if(vTimeOfDay.state.toString == "BED") { upper = -1.5; lower = -3.5 }
        else                                   { upper = 0.3; lower = -0.5 }
    }

    // auto mode
    else {
        switch(Heating_Plan.state.toString){
            case "WINTER": {
                baseTemp = ScriptServiceUtil.getItemRegistry.getItem("Heating_Temp_"+Heating_AutoState.state.toString).state as Number
                if(Heating_AutoState.state.toString == "Heating") { upper = 0.3; lower = -0.5 }
                else                                              { upper = 0.2; lower = -0.7 }
            }
            case "AWAY": { baseTemp = 15; upper = 0; lower = -1.5 }
            default:     { baseTemp = 17; upper = 0; lower = -2 }
        }
    }

    val targetTemp = baseTemp + upper
    val lowerTemp = baseTemp + lower

    // 3. Do it
    Heating_Visibility.sendCommand(Heating_Mode.state) // consider just using Heating_Mode to control visibility on your sitemap

    // temp is higher, switch OFF
    var newState = "STAY"
    
    if(actualTemp > targetTemp) newState = "OFF"
    else if(actualTemp <= lowerTemp) newState = "ON"

    if(newState != "STAY && Heating.state.toString != newState)
        Heating.sendCommand(newState)
        logInfo("Heating", "Current temp is " + actualTemp + " and target temp is " + targetTemp + " so switching Heating to " + newState)
    }
end

thank you
I still find OH rules bit odd from other languages I know better even while am working with them for some time already so your inputs are very appreciated!

Iam not running production OH on rpi anymore so I dont have concerns about performance, but your code looks even cleaner so I will adjust it.

Thanks

It would be a good idea to consider migrating to JSR223 where you can code Rules in Python, JavaScript or Groovy. At some point, hopefully OH 3, Rules DSL will be deprecated or at the very least no longer be the default.

The Python version of this Rule would look something like (note, I’m just typing this in, it’s probably riddled with errors, also, there might be more Pythonic ways to do it):

from core.rules import rule
from core.triggers import when

@rule("Heating logic")
@when("Item HeatingHouseTemp changed")
@when("Item Heating_Temp_Manual changed")
@when("Item Heating_Mode changed")
@when("Item Heating_Plan changed")
def heating(event):
    # 1. See if we need to run the Rule at all
    for i in ["HeatingHouseTemp", "Heating_Temp_Manual", "Heating_Mode", "Heating_Plan"]:
        if isinstance(items[i], UnDefType):
            heating.log.error("{} is {}, cannot process heating rule!".format(i, items[i]))

    # 2. Calculate what needs to be done.
    actualTemp = items["HeatingHouseMin"] if items["Heating_Style"] == ON else items["HeatingHouseTemp"]

    baseTemp = 0
    upper = 0
    lower = 0

    if items["Heating_Mode"] == ON:
        baseTemp = items["Heating_Temp_Manual"]
        if items["vTimeOfDay"] == StringType("BED"):
            upper = -1.5
            lower = -3.5
        else:
            upper = 0.3
            lower = -0.5

    # auto mode
    else:
        if items["Heating_Plan"] == StringType("WINTER"):
            baseTemp = items["Heating_Temp_{}".format(items["Heating_AutoState"])]
            if items["Heating_AutoState"] == StringType("Heating"):
                upper = 0.3
                lower = -0.5
            else:
                upper = 0.2
                lower = -0.7
        elif items["Heating_Plan"] == StringType("AWAY"):
            baseTemp = 15
            upper = 0;
            lower = -1.5
        else:
            baseTemp = 17
            upper = 0
            lower = -2

    targetTemp = baseTemp + upper
    lowerTemp = baseTemp + lower

    # 3. Do it
    events.sendCommand("Heating_Visibility", str(items["Heating_Mode"]))

    newState = "STAY"
    if actualTemp > targetTemp:
        newState = "OFF"
    elif actualTemp <= lowerTemp:
        newState = "ON"

    if(newState != "STAY" and str(items["Heating"]) != newState:
        events.sendCommand(newState)
        heating.log.info("Current temp is {} and target temp is {} so switching Heating to {}"
                                  .format(actualTemp, targetTemp, newState)

The code can be somewhat simplified further by taking advantage of some features built into the helper libraries. There is a hysteresis function awaiting merging you can find here and there is a send_command_if_different function in core.util which can make the last part of the function above look something like


    # 3. Do it
    send_command_if_different("Heating_Visibility", items["Heating_Mode"])
    hyst = hysteresis(targetTemp, actualTemp, lower=lower, high=high)

    if not hyst:
        send_command_if_different("Heating", "ON" if hyst == 1 else "OFF")

It does look quite messy in this, eventho python itself is very readable language this looks not very pretty.
And if OH3 will have DSL depreciated is ok, but till then I don’t know what will be the “default” therefore there is no point to sleve up trousers before ford crossing.

doing things in even more non friendly format when I dont know if that will be the prefered and supported way seems bit unnecessary effort to me.

even documentantion for jsr is bit tricky as you have to edit paths, install exact versions etc.
It’s not yet time for me to go this way :wink:

Default will be the NGRE which will be building Rules in the browser (replacement to PaperUI) using Python, JavaScript, or Groovy in cases where you need to write your own custom Script Actions. NGRE and JSR223 Rules both run on the same execution engine so any work you do in JSR223 right now will be directly portable to the future default.

Scott is working on making it installable like a plugin or it just comes with OH. The fiddly bits with the paths and such is only required for Python. JavaScript comes embedded in OH by default, though he Helper Libraries are more mature in Python.

Great project! About 25 years ago I created a heating system, completely written in Visualbasic for Dos. It works great but the hardware side is becoming a risk: an old PC with Windows 98 and no way to just replace the hardware. So 2 years ago I started searching alternatives. As I like the old application, I decided to port the software on a Raspberry with Dosbox-x and connect the sensors cia a combination of Python scripts, MQTT and OpenHab. As every room has its own sensors, I also decided to replace the old NTC’s with Wemos & DHT22 sensors.
My first priority us to get everything working, but I’m already dreaming about all the possible extensions I could do. The automatic schedule you created looked very familiar: I have exactly the same thing in the Dos application so once I find some tie I’ll take a closer look at it.
Thanks for sharing this project!

glad you like it, thanks.

I’ve added some features into it, now it got “comfort” schedule which maintains bit higher temperature with less hiatus in the evening and as well cleaned and simplied ruleset thanks to @rlkoshak hints.

next thing will be chart from grafana and some statistics about runtime and such, but those are just some cosmetics things.
Overall setup works absolutely great and this is where OH shines. OH learning curve might be sometimes quite pain, but after that, everything seems to be easy and possible :wink:

actual code (yes i don’t care about unset Items here as i do have it solved by mqtt out of the rules for now)

val logName = "Heating"

rule "Heating logic"
when
    Item HeatingHouseTemp changed or
    Member of HeatingTrigger changed
then
    var actualTemp = (if(Heating_Style.state == ON) HeatingHouseMin.state else HeatingHouseTemp.state) as Number

    var Number baseTemp = 0
    var Number upper = 0
    var Number lower = 0

    // manual override
    if(Heating_Mode.state == ON){
        baseTemp = Heating_Temp_Manual.state as Number
        if(vTimeOfDay.state.toString == "BED") { upper = -1.5;  lower = -3.5 } // 22 => 20.5 - 18.5
        else                                   { upper = 0.3;   lower = -0.5 } // 22 => 22.3 - 21.5
    }

    // auto mode
    else {
        switch(Heating_Plan.state.toString){
            case "WINTER": {
                if      (Heating_AutoState.state.toString == "Comfort") { upper = 0.5; lower = 0;    baseTemp = Heating_Temp_Normal.state as Number }   // comfort
                else if (Heating_AutoState.state.toString == "Normal")  { upper = 0.3; lower = -0.5; baseTemp = Heating_Temp_Normal.state as Number }   // normal
                else if (Heating_AutoState.state.toString == "Away")    { upper = 0;   lower = -0.5; baseTemp = Heating_Temp_Away.state as Number }     // away
                else                                                    { upper = 0.2; lower = -0.7; baseTemp = Heating_Temp_Night.state as Number }    // night
            }
            case "AWAY": { baseTemp = 15; upper = 0; lower = -1.5 }
            default:     { baseTemp = 17; upper = 0; lower = -2 }
        }
    }

    val Number targetTemp = baseTemp + upper
    val Number lowerTemp = baseTemp + lower


    // Heating ON/OFF
    var newState = "STAY"
    
    if(actualTemp > targetTemp) newState = "OFF"
    else if(actualTemp <= lowerTemp) newState = "ON"

    if(newState != "STAY" && Heating.state.toString != newState){
        Heating.sendCommand(newState)
        logInfo(logName, "Current temp is " + actualTemp + " and target temp is " + targetTemp + " : switching Heating " + newState)
    }
end

rule "End Manual Heating mode at 2.02 am"
when
    Time cron "0 2 2 ? * * *"
then
    Heating_Mode.sendCommand(OFF)
end

That might not be sufficient. If, for example, the connection between the MQTT broker and OH is lost, I believe the binding will set the Items to UNDEF. Also, when using Rules DSL you cannot guarantee that your Items will have been restoreAtStartup or retained MQTT messages retrieved before the Rules start running. In either case, the Rule will generate errors.

no it wont

as they are triggered and evaluated by values from mqtt I can

might, but fix itself when new data arrives anyway

why I basically dont do that in rules for now is fact I dont like fact there are two states NULL and UNDEFF which factically is the same and because my mqtt works 100% reliable + retained msgs.

And for heating there is no point to have it at all, as you need to work with proper data, not some default initialisation values.

Personally I would limit to 3 temperatures: min, medium & max. It worked out for me during all that time, besides that: I have this for each room seperately. On the other side, I would keep Saturday & Sunday seperated.
Did you also think about monitoring the sensors, for example send an alert when one of the Wemos stops sending updates?

It used to. Maybe there has been a change recently.

How? Let’s say your Rule starts triggering before MQTT has completed connecting and retrieving all the retained messages and restoreOnStartup hasn’t finished. How can you guarantee that every Item used in that Rule isn’t NULL?

I don’t like it so I’ll ignore it doesn’t sound like a good reason to let your code blow up to me. It works 100% reliable for you right now only because you are lucky with the timing in how everything loads on your system. Some day you will make a change or upgrade OH and you won’t be lucky any more.

And they are not “factually” the same thing. NULL means uninitialized, the Item has never received a State since OH started. UNDEF means that the binding or a Rule is asserting that it does not and cannot know what state the Item is in. There are lots of use cases users other than yourself have where the distinction is both useful and meaningful.

Perhaps, or perhaps one wants the rule to fail gracefully instead of exploding with null errors in the logs.

But I guess I don’t know what I’m talking about. It’s not like I’ve been using OH for years and helping people with problems, like the one described above, in all that time.

nobody said that, dont act like child.
my approach here is different for reasons, why thats bothers you so much? chill

i had this scenario as well and i have it still. fourth interval has got different hiatus not the temperature.
On top of that, I dont want each room to have separate temperature because in my house its not needed, simple as that

indeed but it is not part of this rule, i have separate rules for sensors
anyway I have own sketches in there or custom tasmota which handles self issues well enough so OH is just a information processor as they are always allright

I care not because of you. You can make any decisions you want to and do what ever you want and I don’t care.

But I cannot leave misleading or incorrect assertions about how OH works unchallenged because users come across threads like these, make similar or the same choice and end up consuming lots of our time asking for help when it blows up in their face.

You have reasons but the only reason it works for you right now is luck. Other users may not be as lucky.

1 Like

HI !
I see you use “time scheduled”, so, you use in sitemap “transferItem=TransferItem1”
why only TransferItem1 ?
Maybe you can share more information ? (lasted code items, sitemap, rules and “time scheduled rules(config)”.

Thank you.
B.R.