Help on concept needed: migrating my heating control

Hi everyone,

I would like to migrate my heating control from its current platform to openhab. At the moment, it is implemented in Perl and works great! Well… after I stuffed some “leaks in my heating”… memory leaks :laughing:. But the previous plattform is outdated and needs to go offline.

Concept used so far

The concept used so, I actually would like to preserve… below some details on it… it’s so cute and flexible :sunglasses: at least from my point of view :laughing:

Structure

As a basis one can define a structure the rules are applied on

  • rooms (which basically are only containers) for …
  • heating pairs as pairs of temperature sensor plus heater.
  • relevant windows and doors in the room that should not be open when heating is on
    (heaters, sensors and doors/windows are refered to by their KNX group address)
# Room definitions (Default values)
$usage_conf->{ rooms } = [];
$usage_conf->{ rooms }	= [	 			# Array of all rooms

    # A first room
    Wiregate::CrossPlugin::Room->new(
        'EG-WZ', 'Wohnzimmer / Kueche',	# id, name
        [ '3/0/1', '3/1/1' ],		# doors
        [],				# windows
        [				# heating_pairs

        # A heating pair in EG-WZ
        Wiregate::CrossPlugin::HeatingPair->new(
            1,	WG_HT_FLOOR,		# id, type
            '1/2/23', '1/4/6',		# temp_sensor, heating
            -0.4			# offset to default temperature
        ),

        # A second heating pair in this room
        Wiregate::CrossPlugin::HeatingPair->new(
            2,	WG_HT_RADIATOR,
            '1/2/20', '1/4/7',
            0			# no offset used here
        )

    ]),					# End of room "EG-WZ"

# ....continues with the other rooms....

Rules

On top of this one can define heating rules with the following features / attributes:

  • temporal scope: active from and to (relative to either recurring events like a wake up time or an absolute date-time)
  • priority: using a number to resolve rule conflicts
  • affect on heating pairs (i.e. what to change)
    • by referencing any number of rooms and/or heating pairs via string matching these can be selected
    • an abolsute or relative temperature change can be applied
    # A rule (heating extension)
    Wiregate::CrossPlugin::HeatingExtension->new (
        'SwitchOff', 'Alles (fast) aus',			# id, name
        # Note that the month starts with 0 (i.e. substract one...
        WG_RT_SPECIFIC_DATETIME, timelocal(0,0,8, 19,3,2019),	# active from
        WG_RT_SPECIFIC_DATETIME, timelocal(0,0,9, 27,3,2019),	# active to
        [
            # change temp for all heating pairs in all rooms to 17 degrees absolute
            { room => '*', hp => '*', temp_abs => 17 }
        ],
        100					# Priority 100 (rather high)
        ),

    # A second rule - with relative time to a recurring event
    Wiregate::CrossPlugin::HeatingExtension->new (
        'Nachtabs', 'Nachtabsenkung',		# id, name
        WG_RT_GODNIGHT_TOMORROW, -30,			# from ref, offset mins
        WG_RT_GETUP_TOMORROW, -45,			# to ref, offset mins
        [
            # Changes applied to specific rooms and heating pairs with
            # different target temperatures
            { room => 'EG-WZ', hp => '*', temp_abs => 20 },
            { room => 'UG-KO', hp => '*', temp_abs => 19 },
            { room => 'EG-AZ', hp => '*', temp_abs => 20 },
            { room => 'OG-BA', hp => '1', temp_abs => 21 },
            { room => 'OG-BA', hp => '2', temp_abs => 21 },
            { room => 'OG-KN', hp => '*', temp_abs => 20.8 }
        ],	# array of changes 
        0	# Rather low prio
    ),

# More rules follow ...

Furthermore

  • the system can turn off the heating, when relevant windows or doors are opened. And obviously can turn them back on again when they are closed again.
  • can react on changes of items (here: KNX group addresses) instead of iterating over everything every minute

How to move to Openhab

I used (Perl) classes when implementing this, which to me looks like a good way to ease the implementation. Actually, I would have preferred Java but that was not available :roll_eyes:.

So far, I setup my openhab and had it running for some time now. I applied a number of rules that work as expected / I’m happy with. Ok, I admit that I could just write some 100 lines rules code with all this factored out (i.e. no config files, all coded out, etc.), but this does not feel right (do I sound nerdy…?). And I would love to preserve the flexibility I had so far. And who knows, maybe someone else can make use of it, too…

From what I understand, I need to implement automation modules (https://www.openhab.org/docs/developer/module-types/).

And now my (first?) questions

  • I haven’t found a ready-made piece of software for this, so I strongly assume there is no out of the box solution… (that’s why I post it here)?
  • Is the automation modules the right point to start?
  • Cutting the config into pieces and defining how to configure things seems to be a tricky part. Is it possible to use nesting, arrays and hash maps in configs (in the tutorial, it does not look like it… https://www.openhab.org/docs/developer/module-types/#module-type-provider)?
  • The new piece of software would hook its trigger-condition derived from the read configuration into the rules engine using programmatically created rules, I guess (cf. https://www.openhab.org/docs/developer/module-types/#programmatically-define-rules)?
  • But how can I resolve rules conflicts (priorities)?

Hoping for some hints & examples so I don’t stumple to far around… :wink:
Thanks!
BR,
Alex

Python might be another option for you. Jython is Python 2 compatible.

Nope… everything you need should already be built.

I definitely recommend using scripted automation for this rather than the rules DSL. Jython has more capabilities and the helper libraries are more evolved, but there are also JS libraries available. The use of Item metadata rather than config files or Hashmaps is really nice.

BTW, I’ve moved your topic.

1 Like

openHAB provides Groups. A special type of Item to which other Items can be set as members.
For the set up you’ve described, you might have
all doors and windows in Group RoomX_openings
heater and thermostat in Group RoomX_heating
at the same time,
all therrmostats can go in Group all_thermostats
etc.
Groups of Groups are allowed, a hierarchy.

These can be exploited in rules e.g.
a singe rule can adjust all thermostats at bedtime
a single rule can observe any open window in RoomX and adjust the RoomX thermostat only.
etc.

This is my heating rules:
I have in each room a temperature/humidity sensor:

//MasterBedroom
Group                MasterBedroom_Thermostat              "Master Bedroom Thermostat"                                  (MasterBedroom, Thermostats)                                                          [ "Thermostat", "Celsius" ]      { alexa="Endpoint.Thermostat"}
String               MasterBedroom_ThermostatMode          "Master Bedroom Thermostat Mode"                             (MasterBedroom, MasterBedroom_Thermostat, Thermostats, MQTTv2)                        [ "homekit:HeatingCoolingMode" ] { channel="mqtt:topic:MasterBedroomThermostat:Mode", alexa="ThermostatController.thermostatMode" }
Number:Temperature   MasterBedroom_ThermostatTarget        "Master Bedroom Target Temp [%.1f %unit%]"     <temperature> (MasterBedroom, MasterBedroom_Thermostat, Targets, MQTTv2)                            [ "TargetTemperature" ]          { channel="mqtt:topic:MasterBedroomThermostat:Target", alexa="ThermostatController.upperSetpoint" }
Number:Temperature   MasterBedroom_ThermostatAmbientTemp   "Master Bedroom Ambient Temp. [%.1f %unit%]"   <temperature> (MasterBedroom, MasterBedroom_Thermostat, Temperatures, AmbientTemps, MQTTv2)         [ "CurrentTemperature" ]         { channel="mqtt:topic:MasterBedroomThermostat:Ambient", alexa="ThermostatController.temperature" }
Number:Dimensionless MasterBedroom_ThermostatHumidity      "Master Bedroom Humidity [%.1f %%]"            <humidity>    (MasterBedroom, Humidity, BedroomsHumidity, MQTTv2)                                   [ "CurrentHumidity" ]            { channel="mqtt:topic:MasterBedroomThermostat:Humidity" }
Number:Dimensionless MasterBedroom_ThermostatBattery       "Master Bedroom Thermostat Battery [%d %%]"    <battery>     (MasterBedroom, Batteries, MQTTv2)                                                                                     { channel="mqtt:topic:MasterBedroomThermostat:Battery" }
Switch               MasterBedroom_RadiatorValve           "Master Bedroom Radiator [%s]"                 <radiator>    (MasterBedroom, Radiators,MQTTv2)                                                                                      { channel="mqtt:topic:MasterBedroomRadiatorValve:OnOff" }
String               MasterBedroom_Thermostat_Watchdog     "Master Bedroom Thermostat Watchdog [%s]"                    (MasterBedroom, Thermostats, Thermostat_Watchdogs)                                                                     { expire="31m, command=OFFLINE" }

Windows:

Contact  MasterBedroom_WindowTop    "Window Top [%s]"       <contact> (Windows, HeatingWindows, MasterBedroom_Windows, MasterBedroom, Persist, MQTTv2) { channel="mqtt:topic:MasterBedroomWindowTop:OpenClosed" } //{ mqtt="<[mybroker:House/MasterBedroom/WindowTop:state:default]"} //
Contact  MasterBedroom_WindowBottom "Window Bottom [%s]"    <contact> (Windows, HeatingWindows, MasterBedroom_Windows, MasterBedroom, Persist, MQTTv2) { channel="mqtt:topic:MasterBedroomWindowBottom:OpenClosed" } //{ mqtt="<[mybroker:House/MasterBedroom/WindowBottom:state:default]"} //

Some groups:

Group                         Heating       (House, Persist) 
Group                         Thermostats   (Heating)   
Group:Switch:OR(ON,OFF)       Radiators     (Heating)
Group                         Temperatures  (Heating)
Group                         AmbientTemps  (Persist)
Group                         Targets       (Persist)
Group                         Thermostat_Watchdogs

And rules:

// Heating rules

rule "Group Radiator Valves Changed"
when
    Item Radiators changed
then
    //logInfo("test", Radiators.state.toString)
    if (House_RadiatorValvesTest.state == OFF) {
        House_HeatingBoiler.sendCommand(triggeringItem.state.toString)
    }
end

rule "Radiator Valves Weekly Test"
when
    Time cron "0 5 3 ? * MON *"
then
    House_RadiatorValvesTest.postUpdate(ON)
    createTimer(now.plusSeconds(1), [ | 
        Radiators.sendCommand(ON)
    ])
    createTimer(now.plusSeconds(301), [ |
        Radiators.sendCommand(OFF)
    ])
    createTimer(now.plusSeconds(302), [ |
        House_RadiatorValvesTest.postUpdate(OFF)
    ])
end

rule "Generic Windows Changed"
when
    Member of HeatingWindows changed or
    Member of Radiators changed
then
    if (previousState == NULL) return;
    val String room = triggeringItem.name.split("_").get(0)
    val GroupItem window = Windows.members.filter[ i | i.name.contains(room) ].head as GroupItem
    val SwitchItem radiator = Radiators.members.filter [ i | i.name.contains(room) ].head as SwitchItem
    if (radiator.state == ON) {
        if (window.state == OPEN) {
            sendCommand(room + "_RadiatorValve", "OFF")
            postUpdate(room + "_ThermostatMode", "off")
        }
    }
    if (radiator.state == OFF) {
        if (window.state == CLOSED) {
            val offset = House_HeatingOffset.getStateAs(QuantityType).doubleValue
            val target = (Targets.members.filter[ i | i.name.contains(room) ].head.state as QuantityType<Number>).doubleValue
            val ambient = (AmbientTemps.members.filter[ i | i.name.contains(room) ].head.state as QuantityType<Number>).doubleValue
            if (ambient <= (target - (offset / 2))) {
                sendCommand(room + "_RadiatorValve", "ON")
                postUpdate(room + "_ThermostatMode", "heat")
            }
        }
    }
end

rule "Thermostat changed generic"
when
    Member of Targets changed or
    Member of AmbientTemps changed
then
    if (previousState == NULL) return;
    val String room = triggeringItem.name.split("_").get(0)
    val offset = (House_HeatingOffset.state as QuantityType<Number>).doubleValue
    val target = (Targets.members.filter[ i | i.name.contains(room) ].head.state as QuantityType<Number>).doubleValue
    val ambient = (AmbientTemps.members.filter[ i | i.name.contains(room) ].head.state as QuantityType<Number>).doubleValue
    var Number turnOnTemp = target - (offset / 2)
    var Number turnOffTemp = target + (offset / 2)

    val GroupItem window = Windows.members.filter[ i | i.name.contains(room) ].head as GroupItem
    val SwitchItem radiator = Radiators.members.filter [ i | i.name.contains(room) ].head as SwitchItem

    if (ambient <= turnOnTemp) {
        if (window.state == CLOSED) {
            if (radiator.state == OFF) {
                sendCommand(room + "_RadiatorValve", "ON")
                postUpdate(room + "_ThermostatMode", "heat")
            }
        }
    } else if (ambient >= turnOffTemp) {
        if (radiator.state == ON) {
            sendCommand(room + "_RadiatorValve", "OFF")
            postUpdate(room + "_ThermostatMode", "off")
        }
    }
    postUpdate(room + "_Thermostat_Watchdog", "ONLINE")
end

rule "Thermostat update"
when
    Member of Humidity received update
then
    if (triggeringItem.previousState == NULL) return;
    val String room = triggeringItem.name.split("_").get(0)
    if (!room.equals("House") && !room.equals("OutsideHumidity")) {
        postUpdate(room + "_Thermostat_Watchdog", "ONLINE")
    }
end

rule "Thermostats Watchdogs"
when
    Member of Thermostat_Watchdogs received command
then
    if (triggeringItem.state == NULL) return;
    if (triggeringItem.state.toString == "OFFLINE") {
        val String room = triggeringItem.name.split("_").get(0)
        sendBroadcastNotification("WARNING - Thermostat "+ room + " OFFLINE")
    }
end

If the heating target for a particular room is above current temp it will open the radiator valve after checking if any of the windows is open.
If a window is opened when the radiator valve is ON then it turns it off
A weekly routine actions all the radiator valve to prevent them “sticking” and I use a test item to prevent the boiler from firing during the test.
The radiator valves are in an OR group so when the group is ON, I turn the boiler ON except when the test mode is ON

The target temperatures are obtained using node-red-contrib-ramp-thermostat

Correct, with Rules DSL there are no out of box anything. But there are tons and tons of examples posted to the forum.

No, you want to create Rules. For Rules you have a choice of:

  • Rules DSL: Current default, very simple domain specific language, has the most examples and support on this forum, inflexible if you are a programmer used to bending a language to your will instead of bending yourself to the language’s will.
  • JSR223 Jython: Python 2.7 environment that will support anything that can be done in Rules DSL and then some.
  • JSR223 JavaScript: JavaScript Nashorn environment that will support anything that can be done in Rules DSL and then some.
  • JSR223 Groovy: Groovy environment that will support anything that can be done in Rules DSL and then some.
  • HABApp: An external Python 3 Rules server that interacts with OH through it’s REST API.
  • NodeRed: A graphical pipes type rules engine that has an openHAB plug-in.
  • Experimental Next Generation Rules Engine: Uses the same execution engine as the JSR223 languages but presents a web based UI for building the Rules. The UI is primitive at best right now.

For the JSR223 languages, there is an extensive Helper Library that includes a Community section where users submit reusable Rules and libraries for building up their Rules. This is just getting off the ground so there are not many submissions yet.

You will find it easiest I suspect to use one of the JSR223 languages with the Helper Libraries.

You are looking in the wrong place. But what you say somewhat applies to Rules DSL Rules. There is no nesting, limited support for arrays and maps, and really no good way to load config data from a file in Rules DSL. And that is by design. That sort of information should be represented as Items, not config files. See the Topics tagged designpattern postings for best practices for coding Rules DSL Rules.

I think before you get too far down the road on this you should review How to get started (there is no step-by-step tutorial) and follow those links to get a better understanding of what OH as a platform actually is, how the various parts work, and how to migrate your existing code to the new platform.

You can’t. OH is an event based system. Event occurs, get’s put on the Event Bus, and various parts of OH react to those events, including the triggering of Rules or causing Bindings to reach out to the devices to cause something to happen.

Having said all of that, I’ll repeat a thought Marcus expressed on another thread. For something like heating, you really want it to be able to perform at least basic functions autonomously. You don’t want your pipes freezing because your home network went down while you are away from home. At a minimum it should try to maintain the last supplied target temp. Given that, perhaps the solution is somewhere in the middle where you keep a simplified version of your Perl script which maintains the last commanded target temp, add an API (MQTT is popular) and have OH implement your heating control Rules. But all OH is adjusting is the target temp, not actually directly calling for heat.

But no matter how you look at it, migrating this to OH is going to require a fundamental rethinking of how this all works. You will not be just simply porting your existing script to a new language. It’s a whole new platform which will require assembling your solution in a whole new way.

First of all, thanks for the feedback so far (and not only in this thread! :grin:). It actually took some time to get back to this and make some progress, but now, I have a first version ready. :clinking_glasses:

For the implementation I decided to use JSR223 Jython. I tackled the part for evaluating / defining the target values first. So not the actual controller that then tries to regulate actors to reach that target value. I assume besides rlkoshak pull request to the jsr223 helper libs, there may be further implementations out there that can be used. Or - as @rlkoshak pointed out, one might use a more reliable platform for the actual controller part.

The ideas contributed by @5iver, @rossko57 and @vzorglub using item meta data / nested groups for configuration purposes sounds quite interesting. But for the start, I just coded the config directly in the script.

The first version

Here are the key features for the first version:

  • Allow for setting target values…
  • Multiple elements (called meta blocks, I might need to fine tune the naming, though) can be defined to affect the target value of a given open hab item (for example: “Lower temperature during night time”).
    • The meta blocks can be prioritized against each other.
    • The blocks (“meta block specific instances”) have a start and an end.
    • Meta blocks can have a schedule (think cron like but… with an end or duration).
    • A block can influence mutliple OH target items with different effect (e.g., set different values for bed rooms versus bath room).
  • A dynamic element can be used to lower the target value on events (e.g., when a window is opened).
  • No periodic evaluation but only when needed.
  • There is a rudimentary visualization of these meta blocks

For some days, I observed the values generated now and I’m satisfied. Of course there are still multiple things to take care of. But next I will actually setup a controller and have it control my heating.

The first version I created, is available via github: GitHub - Urmel/PaT3S: Priority and Time Span Support for Scheduling (PaT3S). It features a Readme which goes into much more detail along with a sample configuration.

As a teaser, here a live snapshot from the rather rudimentary visualization of these meta blocks which I have in my sitemap:

Next step - is this relevant for others?

Besides the joy of learning a new language (Python) and getting into some interesting details of Jython, I’m now at the point where I have to decide how to proceed. It’s basically either interesting for the community or it will stay on my personal HDD (which may also be fine).

But actually I want to contribute something to this outstanding community.

Therefore, it would be very valueable to me, if you could give feedback on this. Of course, also (constructive) feedback on the implementation itself would also be valuable.

Recap - Motivation

In context of this question, I want to give a brief recap on the why…

First of all, I want to control my heating system. I laid out the key requirements in the first post. To summarize: I want to be able to independently define rules for the heating and prioritize them against each other. There may be lowering the temperature at night — lowering a temperature when a window is open — warming up the bath before you get up and into it — lowering the temperature in general when you go on vacation — on-demand heating of the bath when you want to take a bath, …
I assume that I will come out with (counting different target temperatures for different rooms) ~5 times 5 detailed rules (i.e. 5 meta blocks).

Next use case: During the summer time, we close the blinds in order to not let the sun warm up our home too much. This happens in various steps (not all blinds at the same time) for closing and opening them again. There are quite a number of times, at which we turn this function on and off (e.g., in the morning no sun - “hey we don’t need to sit in the dark”…). Now when we then later turn on the setting again, one has to manually close the blinds that should be closed at that time…

And last but not least lighting: I see some interesting use cases in turning on lights automatically like when you open your front door but you want it to go off after 5 minutes again. But wait, somebody turned it on permanently, … Also presence simulation with this is possible.

And from what I understand, this also goes into exactly this direction:

Overall I see a great benefit in the possibility to define start and end and prioritize against each other. For me, things are easier to grasp, maybe even discuss with the family, and in the end makes them less error-prone to implement.

Thank you

I do this for my lights. In my current implementation I use

Rule that detects when a light was manually controlled, marks the Light as overridden

@rule("Lights Override",
      description=("Sets the Override flag when a light is manually changed "
                   "during the day"),
      tags=["lights"])
@when("Member of gLights_ON_WEATHER changed")
def override_lights(event):
    sleep(0.5) # Give pesistence a chance to catch up

    # Wait a minute before reacting after vTimeOfDay changes, ignore all other
    # times of day.
    if (items["vTimeOfDay"] != StringType("DAY") or
        (PersistenceExtensions.lastUpdate(ir.getItem("vTimeOfDay"), "mapdb")
            .isAfter(DateTime.now().minusSeconds(10)))):
        return

    if (not PersistenceExtensions.lastUpdate(ir.getItem("vIsCloudy"), "mapdb")
            .isAfter(DateTime.now().minusSeconds(5))):
        override_lights.log.info("Manual light trigger detected, overriding the"
                                 " light for auto control for {}."
                                 .format(event.itemName))
        set_metadata(event.itemName, "Flags", { "override" : "ON" },
                     overwrite=False)

Rule that does the automation (turns on/off the lights based on how cloudy it is).

@rule("Cloudy Lights",
      description="Turns ON or OFF some lights when it is cloudy during the day",
      tags=["lights"])
@when("Item vIsCloudy changed")
@when("Item vTimeOfDay changed")
def cloudy_lights(event):
    # If it's not DAY or cloudy isn't set, exit.
    if (items["vTimeOfDay"] != StringType("DAY") or
        isinstance(items["vIsCloudy"], UnDefType)):
        return

    # If no one is home, turn OFF any lights that are on.
    if items["vPresent"] == OFF:
        [event.sendCommand(light, "OFF") for light in ir.getItem("gLights_ON_WEATHER").members if light.state == ON]
        return

    # Someone is home and it's DAY time, turn ON/OFF the lights based on the cloudiness.
    if event.itemName == "vTimeOfDay":
        sleep(0.5)

    cloudy_lights.log.info("It is {} and cloudy changed: {}"
                           .format(items["vTimeOfDay"], items["vIsCloudy"]))

    for light in [light for light in ir.getItem("gLights_ON_WEATHER").members if get_key_value(light.name, "Flags", "override") != ON]:
        if items[light.name] != items["vIsCloudy"]:
            events.sendCommand(light, items["vIsCloudy"])

The relevant part is in

for light in [light for light in ir.getItem("gLights_ON_WEATHER").members if get_key_value(light.name, "Flags", "override") != ON]

This line filters out all the Items that do not have an override metadata value set to ON.

Finally, the Rule that resets the override flags when the sun goes down.

@rule("Reset Override",
      description="Change override flag when time of day changes",
      tags=["lights"])
@when("Item vTimeOfDay changed")
def reset_overrides(event):
    for light in ir.getItem("gLights_ON_WEATHER").members:
        set_metadata(light.name, "Flags", { "override" : "OFF" },
                     overwrite=False)

I haven’t yet figured out whether/the best way to extract this and make it more generic so it can be submitted to the library. I haven’t spent a lot of time thinking about it either though. :wink:

I’ve seen two approaches for presence simulation that most people go with. The first is to control the lights randomly. My preferred approach is to just store the lights states in the database and then “play back” what ever the lights were set to at this time last week. You can find a Rules DSL version of the Rule here. I’ve not yet moved it to Python.

1 Like