Integration of Bayernlüfter from Bayernluft via HTTP Binding

[Update: 30.11.2023: Implement some improvements mentioned by rlkoshak:

  • remove http thing
  • use a cron trigger to receive data from device
  • optimized set option method]

Hi community,

I wanted to share my solution for integrating the German “Bayernlüfter” into openHAB using text-based configuration. Perhapts it’s usefull for someone. Not all features are implemented. In addition to this configuration, the MainUI must be set up to receive the set value.

I’m open to suggestions for improvements or bugfixes. Your feedback is greatly appreciated!

Items:

Group Ventilation_Arbeitszimmer "Lüftung Arbeitszimmer" <fan> (Arbeitszimmer) ["HVAC"]
Number Ventilation_Arbeitszimmer_Speed_In  "Lüftung Arbeitszimmer Stufe Rein" <fan> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Speed_Out "Lüftung Arbeitszimmer Stufe Raus" <fan> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Speed_Set "Lüftung Arbeitszimmer Stufe Setzen" <fan> (Ventilation_Arbeitszimmer) ["Point"] //{stateDescription=""[readOnly=false, min="0", max="10"] }
Number Ventilation_Arbeitszimmer_Speed_AntiFreeze "Lüftung Arbeitszimmer Stufe Vereisungsschutz" <fan> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Temp_In "Lüftung Arbeitszimmer Temperatur Rein [%.1f °C]" <temperature> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Temp_Out "Lüftung Arbeitszimmer Temperatur Raus [%.1f °C]" <temperature> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Temp_Fresh "Lüftung Arbeitszimmer Temperatur Frischluft [%.1f °C]" <temperature> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_rel_Humidity_In "Lüftung Arbeitszimmer relative Luftfeuchtigkeit Rein [%.1f %%]" <humidity> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_rel_Humidity_Out "Lüftung Arbeitszimmer relative Luftfeuchtigkeit Raus [%.1f %%]" <humidity> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_abs_Humidity_In "Lüftung Arbeitszimmer absolute Luftfeuchtigkeit Rein [%.1f g/m3]" <humidity> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_abs_Humidity_Out "Lüftung Arbeitszimmer absolute Luftfeuchtigkeit Raus [%.1f g/m3]" <humidity> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Efficiency "Lüftung Arbeitszimmer Effizienz [%.1f %%]" <fan> (Ventilation_Arbeitszimmer) ["Point"]
Number Ventilation_Arbeitszimmer_Humidity_Transport "Lüftung Arbeitszimmer Feuchtigkeitstransport [%d g/24h]" <fan> (Ventilation_Arbeitszimmer) ["Point"]
Switch Ventilation_Arbeitszimmer_Power "Lüftung Arbeitszimmer Power" <fan> (Ventilation_Arbeitszimmer) ["Point"]

Rules:

  rules.JSRule({
    name: "SetVentilationItemsWohnzimmer",
    description: "Set Items for Bayernluefter",
    triggers: [triggers.GenericCronTrigger("0 * * * * ? *")],
    execute: (event) => {
        var ip = "10.220.30.101";
        var rawText = actions.HTTP.sendHttpGetRequest("http://" + ip + "/index.html?export=1");
        items.getItem("Ventilation_Wohnzimmer_Speed_In").postUpdate(parseInt(actions.Transformation.transform('REGEX', '.*Speed_In: (\\d*).*', rawText)));
        items.getItem("Ventilation_Wohnzimmer_Speed_Out").postUpdate(parseInt(actions.Transformation.transform('REGEX', '.*Speed_Out: (\\d*).*', rawText)));
        items.getItem("Ventilation_Wohnzimmer_Speed_AntiFreeze").postUpdate(parseInt(actions.Transformation.transform('REGEX', '.*Speed_AntiFreeze: (\\d*).*', rawText)));
        items.getItem("Ventilation_Wohnzimmer_Temp_In").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*Temp_In: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_Temp_Out").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*Temp_Out: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_Temp_Fresh").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*Temp_Fresh: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_rel_Humidity_In").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*rel_Humidity_In: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_rel_Humidity_Out").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*rel_Humidity_Out: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_abs_Humidity_In").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*abs_Humidity_In: (\\d*,\\d*).*', rawText).replace(',', '.')));
        items.getItem("Ventilation_Wohnzimmer_abs_Humidity_Out").postUpdate(parseFloat(actions.Transformation.transform('REGEX', '.*abs_Humidity_Out: (\\d*,\\d*).*', rawText).replace(',', '.')));
        try {
            items.getItem("Ventilation_Wohnzimmer_Efficiency").postUpdate(parseFloat(actions.Transformation.transformRaw('REGEX', '.*Efficiency: (\\d*,\\d*).*', rawText).replace(',', '.')));
        } catch (error) {
            items.getItem("Ventilation_Wohnzimmer_Efficiency").postUpdate(0);
        }
        items.getItem("Ventilation_Wohnzimmer_Humidity_Transport").postUpdate(parseInt(actions.Transformation.transform('REGEX', '.*Humidity_Transport: (-?\\d*).*', rawText)));
        var power = actions.Transformation.transform('REGEX', '.*SystemOn: (\\d).*', rawText);
        if (power == 1) {
            items.getItem("Ventilation_Wohnzimmer_Power").postUpdate("ON");
        } else {
            items.getItem("Ventilation_Wohnzimmer_Power").postUpdate("OFF");
        }
        
    },
    tags: ["Ventilation"]
  });

  
  rules.JSRule({
    name: "SetVentilationOptionsWohnzimmer",
    description: "Set Ventilation On/Off and set speed",
    triggers: [triggers.ItemCommandTrigger('Ventilation_Wohnzimmer_Power'),triggers.ItemCommandTrigger('Ventilation_Wohnzimmer_Speed_Out')],
    execute: (event) => {
        var ip = "10.220.30.101";
        if (event.itemName == "Ventilation_Wohnzimmer_Power") {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?power=" + event.receivedCommand.toLowerCase());
        }
        if (event.itemName == "Ventilation_Wohnzimmer_Speed_Out") {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?speed=" + event.receivedCommand);
            items.getItem("Ventilation_Wohnzimmer_Speed_Out").postUpdate(event.receivedCommand);
        }
    }
 });
1 Like

This is an excellent candidate for a rule template. If you made this into a rule template you can use properties to allow the end users to define their own Item names. That makes it easier to adopt for the end users. In addition, end users can just install and instantiate the rule and don’t need to copy/paste/edit.

If you went down this path you could use sendHttpGetRequest instead of the HTTP Thing to further reduce the dependencies and amount of work the end users would need to do to use this capability. It would then be able to use a cron trigger for that first rule and eliminate the raw Item entirely.

Where it gets awkward is the fact that you have three rules. But that could be managed by testing the event Object for what triggered the rule and doing the right action based on the rule trigger (i.e. if cron triggered pull down the data, if triggered by the power Item send ON or OFF, and if triggered by the speed Item. I use this technique successfully in several of my rule templates. Unfortunately only one rule

If you need help with how to do that I can answer any questions but it’s not super hard. See How to write a rule template for details.

Assuming you didn’t go down the rule template path, if you keep the Thing anyway, I recommend creating Channels for each value and move the REGEX transformation to the Thing. The HTTP binding allows for chaining transforms so you could do a REGEX and chain that to an inline JS transform to replace the commas with dots, though I think that could be done in the REGEX itself. That would enable the end users to pick and choose which values they actually care about and all they’d have to do to use it is link to the proper Channel. That will make the HTTP Thing behave more like a representation of the device, which is what Things are for.

In fact, your power and speed rule too could be implemented in the Channel config.

So you could implement this purely as a Thing with no rules required, or as a rule with no Things required. Either approach I think is better.

For the power rule you could simplify it if you made the trigger more specific. Add one for when it receives command ON and another when it receives command OFF. Then the action could be as simple as:

actions.HTTP.sendHttpGetRequest('http://10.220.30.100/?power='+event.receivedCommand.toLowerCase());

Just a one liner.

One thing that helps users manage a copy/paste/edit of rules like this is to create variables/consts at the very top of the code for everything that the user may need to customize (e.g. URL to the device). I like to put Items there too.

Wow, many thanks for the extensive feedback! So its my first own implementation of a new device and I learned a lot. I will try to adapt some of your points in the next days.

Hi @rlkoshak,
I have just implemented some of your suggestions (see above).

Two questions have arisen:

  • The cron trigger event does not return any data. From the documentation: “Time triggers do not provide any event instance, therefore no property is populated.” So I cannot determine if the instance is from the cron trigger. But I have merged successfully the 3 functions into 2. Do you have any further idea?
  • In the MainUI I have manually added a “Default List Item Widget” and a “Default Standalone Widget” to set the ventilation_speed item via GUI. Can I automate this using the items file?

Thank you and greetings,
Mario

Those docs are out of date I think. OH 4.0 introduced changes where there is almost always an event Object, even for Time triggers. Though this might also be a difference between UI rules and file based rules.

But in your case, you can test for event === undefined and assume if that’s true it was triggered by the cron job.

In UI rules event will exist when the rule is triggered in any way (but not when run manually or called from another rule) and you can tell how the rule was triggered even for time based triggers using event.type.

It’s Item metadata so you can define them on the Item.

I don’t do text file configs so won’t be of much help there. I’d rather fight home automation problems than syntax errors and hunting down missmatched quotes and the like.

I also see a lot of repeated code. That’s always a sign that you might need a function. Code is easier to maintain when you only need to change things in one place instead of many. For example, what if you decided to use the JS Scripting built in REGEX capabilities instead of the transform? You’d have to change almost every line in that first rule.

I also think shorter code is easier to read and understand so I use items['ItemName'] to get the Items instead of items.getItem('ItemName').

Blocks of code that all do something similar are often easier to read and understand if you line the parts up.

You don’t really need to parse the values to an int or a float so I removed that and keep everything as Strings. postUpdate converts whatever you pass to it to a String anyway so parsing to an int or float and then back to a String is redundant.

I used the ternary operator instead of an if/else for Power. The ternary operator is a shorthand way to get one value or the other based on a condition.

Finally, users of this may have their own naming scheme for the Items. So I moved the names of the Items into variables so all the configuration of the rule is at the top where it’s easy for a user to edit it without messing up the code;.

  // configuration properties
  const ip = "10.220.30.101";
  const itemNameRoot    = 'Ventilation_Wohnzimmer_';
  const speedIn         = itemNameRoot+'Speed_In';
  const speedOut        = itemNameRoot+'Speed_Out';
  const speedAntiFreeze = itemNameRoot+'Speed_AntiFreeze';
  const tempIn          = itemNameRoot+'Temp_In';
  const tempOut         = itemNameRoot+'Temp_Out';
  const tempFresh       = itemNameRoot+'Temp_Fresh';
  const relHumIn        = itemNameRoot+'rel_Humitidy_In';
  const relHumOut       = itemNameRoot+'rel_Humidity_Out';
  const absHumIn        = itenNameRoot+'abs_Humidity_In';
  const absHumOut       = itenNameRoot+'abs_Humidity_Out';
  const effeciency      = itemNameRoot+'Effeciency';
  const humTransport    = itemNameRoot+'Humidity_Transport';
  const power           = itenNameRoot+'Power';

  // Helper Functions
  function getIntValue(property, rawText) {
      return actions.Transformation.transform('REGEX', '.*'+property+': (-?\\d*).*', rawText);
  }

  function getFloatValue(property, rawText) {
      return actions.Transformation.transform('REGEX', '.*'+property+': (\\d*,\\d*).*', rawText).replace(',', '.');
  }

  rules.JSRule({
    name: "VentilationItemsWohnzimmer",
    description: "Bayernluefter",
    triggers: [triggers.GenericCronTrigger("0 * * * * ? *"),
               triggers.ItemCommandTrigger(power),
               triggers.ItemCommandTrigger(speedOut)],
    execute: (event) => {
        
        // Cron or manually triggered, fetch the latest data and update the Items
        if(event === undefined) {
            const rawText = actions.HTTP.sendHttpGetRequest("http://" + ip + "/index.html?export=1");
            items[speedIn]        .postUpdate(getIntValue( 'Speed_In',          rawText));
            items[speedOut]       .postUpdate(getIntValue( 'Speed_Out',         rawText));
            items[speedAntiFreeze].postUpdate(getIntValue( 'Speed_AntiFreeze',  rawText));
            items[tempIn]         .postUpdate(getFloatValue('Temp_In',          rawText));
            items[tempOut]        .postUpdate(getFloatValue('Temp_Out',         rawText));
            items[tempFresh]      .postUpdate(getFloatValue('Temp_Fresh',       rawText));
            items[relHumIn]       .postUpdate(getFloatValue('rel_Humidity_In',  rawText));
            items[relHumOut]      .postUpdate(getFloatValue('rel_Humidity_Out', rawText));
            items[absHumIn]       .postUpdate(getFloatValue('abs_Humidity_In',  rawText));
            items[absHumOut]      .postUpdate(getFloatValue('abs_Humidity_Out', rawText));

            var newEff = getFloatValue('Efficiency', rawText);
            if(newEff === null) newEff = '0';
            items[effeciency].postUpdate(newEff)
            
            items[humTransport].postUpdate(getIntValue('Humidity_Transport', rawText)));
            
            var newPower = getIntValue('SystemOn', rawText);
            items[power].postUpdate((power == '1') ? 'ON' : 'OFF');
        }

        // Change the Power
        else if (event.itemName == power) {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?power=" + event.receivedCommand.toLowerCase());
        }

        // Change the Speed
        else if (event.itemName == speedOut) {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?speed=" + event.receivedCommand);
            items[speedOut].postUpdate(event.receivedCommand);
        }    
    },
    tags: ["Ventilation"]
  });

With these changes there is less code over all by character count, the code is easier to read and follow for future you and it’s easier for others to adopt it for their own needs.

I just typed in the above so there are likely typos.

As a rule template it would looks something like this. Again I’m just typing this in so watch out for typoes.

uid: bayernluft
label: Bayernluft
description: Polls and controls Bayernluft devices
configDescriptions:
  - name: ip
    description: The IP address or host name of the device
    type: TEXT
    label: IP address
    required: true
  - name: speedIn
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Speed In Item
    required: false
    description: Speed In Item
    defaultValue: Ventilation_Arbeitszimmer_Speed_In
  - name: speedOut
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Speed Out Item
    required: false
    description: Speed Out Item
    defaultValue: Ventilation_Arbeitszimmer_Speed_Out
  - name: speedAntifreeze
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Speed AntiFreeze Item
    required: false
    description: Speed AntiFreeze Item
    defaultValue: Ventilation_Arbeitszimmer_Speed_AntiFreeze
  - name: tempIn
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Temp In Item
    required: false
    description: Temp In Item
    defaultValue: Ventilation_Arbeitszimmer_Temp_In
  - name: tempOut
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Temp Out Item
    required: false
    description: Temp Out Item
    defaultValue: Ventilation_Arbeitszimmer_Temp_Out
  - name: tempFresh
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Temperature Fresh Item
    required: false
    description: Temperature Fresh Item
    defaultValue: Ventilation_Arbeitszimmer_Temp_Fresh
  - name: relHumIn
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Relative Humidity In Item
    required: false
    description: Relative Humidity In Item
    defaultValue: Ventilation_Arbeitszimmer_rel_Humidity_In
  - name: relHumOut
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Relative Humidity Out Item
    required: false
    description: Relative Humidity Out Item
    defaultValue: Ventilation_Arbeitszimmer_rel_Humidity_Out
  - name: absHumIn
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Absolute Humidity In Item
    required: false
    description: Absolute Humidity In Item
    defaultValue: Ventilation_Arbeitszimmer_abs_Humidity_In
  - name: absHumOut
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Absolute Humidity Out Item
    required: false
    description: Absolute Humidity Out Item
    defaultValue: Ventilation_Arbeitszimmer_abs_Humidity_Out
  - name: effeciency
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Effeciency Item
    required: false
    description: Effeciency Item
    defaultValue: Ventilation_Arbeitszimmer_Effeciency
  - name: humTransort
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Number
    label: Humidity Transport Item
    required: false
    description: Humidity Transport Item
    defaultValue: Ventilation_Arbeitszimmer_Humidity_Transport
  - name: power
    type: TEXT
    context: item
    filterCriteria:
      - name: type
        value: Switch
    label: Powern Item
    required: false
    description: Power Item
    defaultValue: Ventilation_Arbeitszimmer_Power
triggers:
  - id: "1"
    configuration:
      itemName: "{{power}}"
    type: core.ItemCommandTrigger
  - id: "2"
    configuration:
      itemName: "{{speedOut}}"
    type: core.ItemCommandTrigger
  - id: "3"
    configuration:
      cronExpression: 0 * * * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >
          // configuration properties
          const ip = '{{ip}}';
          const speedIn         = '{{speedIn}};
          const speedOut        = '{{speedOut}}';
          const speedAntiFreeze = '{{speedAntiFreeze}}';
          const tempIn          = '{{tempIn}}';
          const tempOut         = '{{tempOut}}';
          const tempFresh       = '{{tempFresh}}';
          const relHumIn        = '{{relHumIn}}';
          const relHumOut       = '{{relHumOut}}';
          const absHumIn        = '{{absHumIn}};
          const absHumOut       = '{{absHumOut}}';
          const effeciency      = '{{effeciency}}';
          const humTransport    = '{{humTransport}}';
          const power           = '{{power}}';

        // Cron or manually triggered, fetch the latest data and update the Items
        if(event === undefined) {
            const rawText = actions.HTTP.sendHttpGetRequest("http://" + ip + "/index.html?export=1");
            items[speedIn]        .postUpdate(getIntValue( 'Speed_In',          rawText));
            items[speedOut]       .postUpdate(getIntValue( 'Speed_Out',         rawText));
            items[speedAntiFreeze].postUpdate(getIntValue( 'Speed_AntiFreeze',  rawText));
            items[tempIn]         .postUpdate(getFloatValue('Temp_In',          rawText));
            items[tempOut]        .postUpdate(getFloatValue('Temp_Out',         rawText));
            items[tempFresh]      .postUpdate(getFloatValue('Temp_Fresh',       rawText));
            items[relHumIn]       .postUpdate(getFloatValue('rel_Humidity_In',  rawText));
            items[relHumOut]      .postUpdate(getFloatValue('rel_Humidity_Out', rawText));
            items[absHumIn]       .postUpdate(getFloatValue('abs_Humidity_In',  rawText));
            items[absHumOut]      .postUpdate(getFloatValue('abs_Humidity_Out', rawText));

            var newEff = getFloatValue('Efficiency', rawText);
            if(newEff === null) newEff = '0';
            items[effeciency].postUpdate(newEff)
            
            items[humTransport].postUpdate(getIntValue('Humidity_Transport', rawText)));
            
            var newPower = getIntValue('SystemOn', rawText);
            items[power].postUpdate((power == '1') ? 'ON' : 'OFF');
        }

        // Change the Power
        else if (event.itemName == power) {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?power=" + event.receivedCommand.toLowerCase());
        }

        // Change the Speed
        else if (event.itemName == speedOut) {
            actions.HTTP.sendHttpGetRequest("http://" + ip + "/?speed=" + event.receivedCommand);
            items[speedOut].postUpdate(event.receivedCommand);
        }    
    type: script.ScriptAction 

At creation time the stuff in {{ }} gets replaced with the value of the property the end user configures.

Creating an instance from the rule looks something like:

  1. install the rule template from Settings → Automation
  2. Create a new rule
  3. Select the Bayernluft rule template
  4. Fill out the properties

Selecting each Item brings up the standard Item selection dialog.

The defaults above use your Item names but if users want their own naming scheme they have that option.

If you really wanted to get clever, you could test for the existence of the Items first run and if they don’t exist create them from the rule.

1 Like

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.