Yet another Heating Setup

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.

hi,
all relevant items, things and rules are in first post already…

is there something missing?

19 days ago you post new rules and screenshots without items, sitemap configuration.
If look both screenshots (first and last) can see you change code and in first post you usage ITEM “TransferItem1” on Webview and no information more about “TransferItem1”, you can please update information ?
Thank you.
B.R…

I posted simplified ruleset but nothing else was changed. First post is still relevant as well as simplified ruleset.

TransferItem1 is something you have to define in your timelinepicker.rules which are completely separated and I did not went thorough of explaining this bit as I do expect whomever wants to use it, know how (and there is extensive post linked about how to work with it anyway)

in rule relevant to timelinepicker you need to do just this, but as this is quite obvious I did not explicitly said that

val HashMap<String,ArrayList<String>> timePicker = newHashMap(
    "TransferItem1" -> newArrayList('Heating_AutoState')
)

small update from real life usage after almost 3months.
Everything working like a charm.

only thing I needed to do is to relocate and adjust some of the temperature sensors, as they either were reporting lower temps (because they were near to the wall for example) or they were reporting higher values (because they were exposed to sun)
I was expecting this need from begining, but that’s longer process which have to be observed place by place… so that’s fine :wink:

Other than that, house is being nicely heated when needed and there is literally 0 need of interacting with it whatsoever.

I’m pretty happy with that so far.

A complete tutorial on how to do this in OH3 would be awesome (from installation of required plugins, to rules, to whatever).

this is working in OH3 as well, there was (maybe is) cheat sheet what you need to change in order to OH3 ruleengine works properly.

Main thing was to adapt TimeLinePicker for OH3, my rules are quite compatible I hope :wink:

this is my current OH3 rule file for it

val logName = "HEATING"

rule "Heating"
when
    Item HeatingHouseTemp changed or
    Member of HeatingTrigger changed or
    Item hPresence changed or
    Item vTimeOfDay received update
then
    // UNDEF & NULL checks
    if(HeatingHouseTemp.state == UNDEF || HeatingHouseTemp.state == NULL) return;               // do not continue when there is no temperature
    if(Heating_Mode.state == UNDEF || Heating_Mode.state == NULL) Heating_Mode.postUpdate(OFF)  // automode is default

    // Calculate what needs to be done.
    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
    var timepicker = ''
    var actualstate = ''


    // manual override
    if(Heating_Mode.state == ON){
        baseTemp = Heating_Temp_Manual.state as Number
        if(vTimeOfDay.state == "NIGHT") { 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": {
                timepicker = Heating_AutoState.state.toString
                actualstate = timepicker
                
                if(timepicker.toString == "Away" && hPresence.state == ON) actualstate = "Normal"  // somebody is home, but automatic program is on Away, so force it to heating

                postUpdate(holderHeatingState, actualstate)

                if      (actualstate.toString == "Comfort") { upper = 0.5; lower = 0;    baseTemp = Heating_Temp_Normal.state as Number }   // comfort
                else if (actualstate.toString == "Normal")  { upper = 0.3; lower = -0.5; baseTemp = Heating_Temp_Normal.state as Number }   // normal
                else if (actualstate.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 } // max 15 min 13.5 when away
            default:     { baseTemp = 17; upper = 0; lower = -2 }   // max 17 min 15 during summer period
        }
    }
    
    val Number targetTemp = baseTemp + upper
    val Number lowerTemp = baseTemp + lower


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

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



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