AVM Fritz DECT 301: how to best implement interaction between built in setpoint scheduling and OpenHab control using window contacts?

Hi all,

this post is intended to share my experiences in attempting to achieve the above, since I have not found any posts describing a similar approach, and gather feedback from more experienced OpenHab and AVM users regarding further debugging and improvement options.

Background: I recently got my first attempt at making our home smart set up with 14 AVM 301 DECT thermostats and four Magenta magnetic window contacts, both integrated via two meshed FritzBox 7590. Running OpenHabian on a Pi 4, with group controlled MariaDB persistence for all items on a QNAP NAS. All of that seems to work well, OpenHabian is really great for an easy start!

I had been playing with the AVM smart home features for a couple of weeks already and quite like the way grouping and scheduling works and therefore wanted to keep this intact when using OpenHab to extend functionality (as opposed to the apparently more common approach of doing all scheduling via OpenHab rules) . Here’s why:

  • When you manually alter setpoint temperature on one 301 in a group, the entire group will automatically get the new setpoint without further fiddling

  • The scheduling setup works nicely and seems to run on the thermostats themselves, i.e., without external triggering by the FritzBox. Evidence for the latter: had a time sync issue with a couple of the 301s => temperature settings were desynced, too. Advantage of keeping this: independent of centralized setup, should conserve battery (?)

  • The AVM templating feature is nice, the templates can be triggered via the API, this is already implemented in the AVM binding. No reason to rebuild this from scratch in OpenHab that I can see.

What is missing in the pure AVM setup is the option to couple window contacts to thermostats to reliably turn off radiators when a window is open, and of course a decent GUI for graphing & control.

So, I set out to integrate the AVM setup with OpenHab to enable turning radiators off when windows with window contacts are opened and back on to the desired temperature if they were turned on before or are supposed to be turned on as per current internal schedule. This is, of course, limited by the reaction time of the 301/FritzBox interaction (approx. 15 min max).

Temperature setpoint after window closing should be reset to the setting before window opening if no setpoint alteration (e.g., due to a scheduled setpoint change) happens in the meantime. If a setpoint alteration happens, this should be caught, i.e., the radiator remain off while the window is open. After closing, the setpoint should be set to the altered setpoint received while the window was open (e.g., if the window is open while the schedule has a change from “Comfort” to “Eco” level, we want the setpoint set to the “Eco” value after the window is closed.

After some experimentation, I’ve come up with the following solution, which appears to work as a first cut but is somewhat convoluted, requiring three rules, one for opening, one for handling setpoint change events while window is open, and one for window close events, and two “shadow” items, one to store the last setpoint temperature and one to store a flag to remember if the last setpoint change was performed by this ruleset, since the “onChange” rule for the setpoint triggers itself when updating the setpoint.

Re. implementation, I tried to stick to the OpenHab 3 GUI (very cool tool, btw!) and used ECMA script for the (minimal) logic, pasting the full code here for an example room that has just a single radiator, not a group (sorry about the “warn” log level, was to lazy to figure out how to set the logging level for these rules…).

Opening rule:

triggers:
  - id: "1"
    configuration:
      itemName: FensterBadDG_TurFensterZustand
      state: OPEN
      previousState: CLOSED
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >-
        var logger = Java.type("org.slf4j.LoggerFactory").getLogger("Fenster Bad
        DG Öffnung");

        var aktuelleSollTemp = itemRegistry.getItem('HeizungBadDG_Solltemperatur').getState();

        if (aktuelleSollTemp > 6) { // Radiator wasnt off already
          logger.warn("Öffnungsregel läuft, speichere letzte Solltemperatur " + aktuelleSollTemp + ', setze auf Solltemp auf Frostschutz und aktiviere Flag');
          events.postUpdate('HeizungBadDGLetzteSolltemperatur', aktuelleSollTemp);  // save current setpoint temperature to be restored later
          events.sendCommand('HeizungBadDG_ModusdesHeizkorperreglers', 'OFF'); // turn off
          events.postUpdate('HeizungBadDGLetzteAenderungStatus', 'ON'); // let onChange routine know last setpoint change was done by us => don't store
        } else {
          logger.warn("Öffnungsregel läuft, Solltemp <= 6°C => unternehme nichts.");  
        }
    type: script.ScriptAction

Setpoint change while window open rule:

triggers:
  - id: "1"
    configuration:
      itemName: HeizungBadDG_Solltemperatur
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: FensterBadDG_TurFensterZustand
      state: OPEN
      operator: =
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "4"
    configuration:
      type: application/javascript
      script: >-
        var logger = Java.type("org.slf4j.LoggerFactory").getLogger("Bad DG
        Änderungsregel bei offenem Fenster");

        if (itemRegistry.getItem('HeizungBadDGLetzteAenderungStatus').getState() == 'OFF')   { // last change wasn't induced by rule => store setpoint temp for restoring after window closes
          var aktuelleSollTemp = itemRegistry.getItem('HeizungBadDG_Solltemperatur').getState();
          logger.warn("Solltempänderung bei offenem Fenster angefordert, HeizungBadDGLetzteAenderungStatus war OFF, speichere aktuelle Solltemp " + aktuelleSollTemp + ", reaktiviere Frostschutz und setze Statusflag auf ON");
          events.postUpdate('HeizungBadDGLetzteSolltemperatur', aktuelleSollTemp);
          events.sendCommand('HeizungBadDG_ModusdesHeizkorperreglers', 'OFF'); // set radiator valve state to OFF (will turn back on again with next planned changed of setpoint)
           events.postUpdate('HeizungBadDGLetzteAenderungStatus', 'ON'); // make sure we remember it was us who changed this...
        }  else {  // last change was induced by rule => reset flag to off, don't store setpoint
          events.postUpdate('HeizungBadDGLetzteAenderungStatus', 'OFF');
          logger.warn("HeizungBadDGLetzteAenderungStatus war ON, auf OFF gesetzt");
          }
    type: script.ScriptAction

Closing rule:

triggers:
  - id: "1"
    configuration:
      itemName: FensterBadDG_TurFensterZustand
      state: CLOSED
      previousState: OPEN
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        var logger = Java.type("org.slf4j.LoggerFactory").getLogger("Fenster Bad
        DG Schließung");

        var letzteTemp = itemRegistry.getItem('HeizungBadDGLetzteSolltemperatur').getState();

        logger.warn("Schließungsregel läuft, setzte SollTemp auf gespeicherte letzte Temp " + letzteTemp);

        if (letzteTemp > 6) { // target is not OFF
          events.sendCommand('HeizungBadDG_ModusdesHeizkorperreglers', 'ON'); // turn on
          events.sendCommand('HeizungBadDG_Solltemperatur', letzteTemp);  // set setpoint to last known setpoint value  
          logger.warn("Ausgeführt.");
        } else {
          logger.warn("Übersprungen, da letzte gespeicherte Temp 6°C.");
        }
    type: script.ScriptAction

Of course, testing is a bit of a pain given the high latencies induced by the DECT setup, so this has not been tested exhaustively in practice, but appears to work as a first cut; DECT 301 behaviour does appear slightly erratic when frequent updates are triggered centrally, although I have been unable to reproduce this reliably.

Observations re. combined FritzBox/DECT 301 behaviour:

  • Turning the radiator to “ON” appears to set the temp setpoint to 30°C for some reason, so a subsequent setpoint update is needed. Was unable to get consistent behaviour without explicitly sending “ON” commands, though, and creating case distinctions for “Eco” and “Comfort” modes didn’t seem to make much sense either, given that a setpoint change can also be initiated by a user with an arbitratry setpoint not corresponding to “Eco” or “Comfort” settings

  • FritzBox or DECT 301 seem to recognize if a setpoint corresponds to one of these settings and set the mode accordingly.

  • Assumption is that “OFF” will always correspond to a 6 °C setpoint (appears to be the case currently, I guess this is the “anti freeze” feature?).

What would be helpful from my perspective:

  • Feedback regarding potential failure modes of this approach and ways to improve this (especially in view of possible information re. the internal logic of the FritzBox/301 combination) would be much appreciated.

  • Suggestions regarding an elegant way/standard pattern in OpenHab to code repetitive rules like this generically (this repeats for every room with a window contact and radiator or group of radiators) so that I don’t have to maintain and sync this entire ruleset manually when a change is needed? At the end of the day, all this ruleset really takes as parameters (other than the artificial items to mimic statefulness) are the item labels for window contact status, radiator setpoint temp, and radiator mode control. In an ideal world, it would be nice to be able to auto-instantiate the ruleset for each room by passing these three parameters (the full setup would have a lot more window contacts, potentially).

  • Input regarding further experiences/insights wrt. possible instabilities in the AVM/OpenHab combination: This post seems to point in a similar direction, and this one may also be related, but ended inconclusively. The setpoint and mode does update, most of the time, in my setup, but does appear to become unstable with too many updates. With all the latencies involved in real world testing this setup and very limited time on my hands, further progress will be tough going…

  • A discussion of other approaches to integrate the AVM ecosystem with DECT 301s with OpenHab and their respective advantages/disadvantages.

Many thanks in advance!

Best,

Sven

Phew, quite a long post to read :wink:
You’ve put much effort in your logic.
For me the automatic “door/window open detection” of the DECT301 is enough and runs directly on the thermostat itself which means that you don’t have any delay in there.

Of course if your window stays open for a longer time then this will also fail. But then you still could change the template on the fritzbox so that all valves turn off.

About changing the temperature:
Are you really changing the setpoint that often? I initially thought, "oh it would be cool to automate the setpoint temperature smoothly by a rule that combines outside temperature, windows, sun…).
But to be honest, changing a temperature is not really fast so this approach turned out to be over engineered for me

Usually it is enough to

  • set up a presence detection that changes the templates (e…g from comfort temperature to eco if nobody’s at home and vice versa)
  • switch to a template that switches all radiator valves off after e.g. a certain date. (e…g when its getting warmer outside) and switch it on in autumn.

Thanks for the reply & sorry for the flood of prose. Wanted to make sure to share what I had found out while I can still remember, so people don’t have to reinvent the wheel, and maybe get some extra input regarding how to do this more elegantly.
Regarding your comments: our templates and schedules are simple: basically weekday dependent Comfort temp during daytime and Eco during night, with templates for home schooling (no tx to COVID) & home office, vacation, etc. Still have to get the presence detection configured (probably simply based on which mobiles have Wifi connections).
The reason for the window contact based setup is that the 301 built in detection does not appear to work at all in our environment even on max sensitivity. The delay seems acceptable compared to having the radiator run on max the entire night, as we had happen in the past at times…

I implemented a rule (ECMA 2021) that will change the vacation state of a switch item when nobody is at home for > 24 hours. Based on that I apply with another rule an AVM 301 template in regular based.
For the historical state (lastupdate) I use the persistent service mapdb, which can store only the last state of a complex object (ZoneDateTime). But this is sufficient for this solution.

configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0/20 * * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' +
        ctx.ruleUID);

        var PersistenceExtensions = Java.type("org.openhab.core.persistence.extensions.PersistenceExtensions");


        //var ZonedDateTime = Java.type("java.time.ZonedDateTime");

        //var LocalDateTime = Java.type("java.time.LocalDateTime");

        var MyDuration = Java.type('java.time.Duration');


        //var jetzt = ZonedDateTime.now().toLocalDateTime();

        //logger.info("jetzt: " + jetzt);


        var myitem = items.getItem('gPresence');

        //logger.info("Item: " + myitem);


        var gPresence_lastUpdate = myitem.history.lastUpdate('mapdb');

        //logger.info("LastUpdateItem: " + gPresence_lastUpdate);


        var actualTime = time.toZDT();

        logger.info("AktuelleZeit: " + actualTime);


        //var yesterday = time.toZDT(gPresence_lastUpdate).minusDays(1);

        //logger.info("yesterday: " + yesterday);


        // the state of gPresence_lastUpdate is no more than 24 hours ago

        // var hours24ago = time.toZDT(gPresence_lastUpdate).isClose(time.toZDT(), time.Duration.parse('PT24h')); 

        // logger.info("gPresence_lastUpdated 24h ago: " + hours24ago);


        // Differenz in Stunden zwischen letztes Update und jetzt

        diffInHours = time.Duration.between(gPresence_lastUpdate, actualTime);

        logger.info("differenz in Stunden {} ", diffInHours.toHours());


        function sleep(msec) 

        {
          var e = new Date().getTime() + (msec);
          while (new Date().getTime() <= e) {}
        }


        // Only true if items lastUpdate ZoneDateTime is >= 24h ago (1 day) 

        function CheckOffset() {
            if ((diffInHours.toHours() >= 24) && (items.getItem("gPresence").state == 'OFF' )) {
              logger.info("Alle Personen > 1-Tag nicht im Haus: {}", myitem.state);
              items.getItem("VariableActuator_Urlaub_Status").sendCommand("ON");
              sleep(1000);
              logger.info("Urlaub Status: {}", items.getItem("VariableActuator_Urlaub_Status").state.toString());
              logger.info("Praesenz Status: {}", items.getItem("gPresence").state);
              //return true;
            }
            else {
              logger.info("Alle Personen < 1-Tag nicht im Haus: {}", myitem.state);
              items.getItem("VariableActuator_Urlaub_Status").sendCommand("OFF");
              sleep(1000);
              logger.info("Urlaub Status: {}", items.getItem("VariableActuator_Urlaub_Status").state.toString());
              logger.info("Praesenz Status: {}", items.getItem("gPresence").state);
              //return false;
            }
        }

        CheckOffset();
    type: script.ScriptAction

Apply 301 template - the ID can be gathered via support http://192.168.178.1/#support

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: VariableActuator_Urlaub_Status
      state: ON
    type: core.ItemStateUpdateTrigger
  - id: "3"
    configuration:
      cronExpression: 0 0 0/1 * * ? *
    type: timer.GenericCronTrigger
conditions:
  - id: "4"
    configuration:
      itemName: VariableActuator_Urlaub_Status
      operator: =
      state: ON
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      command: tmp92183E-3E94D9DE5
      itemName: AVM_ApplyTemplate
    type: core.ItemCommandAction