How to customize a Rule

Coming soon there will be a radical change to the HestiaPi configs. Instead of the big and long .rules file, all of the Rules have been moved so they can be see and edited from PaperUI directly (and this will reduce the boot time to around ten minutes).

This tutorial is a first draft showing a way you can do this without breaking the stock set of Rules that comes with HestiaPi ONE. For this tutorial we will be adding hysteresis to the Heating rule

Create a Backup

In the Maintenance section of the BasicUI there is a button to trigger a backup of the OH configurations. Trigger a backup now.

Get the REST Documentation add-on

In PaperUI open “Add-ons” and the “USER INTERFACES” tab and install the “REST Documentation”.

This will allow us to more easily duplicate a Rule. (NOTE: Future versions of OH will replace PaperUI which I hope will make this process easier without resorting to the REST API.

Once it’s installed, open the dashboard and you will see a new REST Documentation entry. It can take a few minutes to install a new add-on on the HestiaPi so be patient.

NOTE: as an alternative you can edit the JSONDB files directly but only do so when OH is offline. Given the 10 minute boot time that may not be desireable.

Create a copy of the Rule

NOTE: I’ll write a script to do these steps for you sometime soon.

In the REST API Docs go down to the row that says “rules” and open it up. We will be using the GET /rules/{ruleUID} end point and the POST /rules end point.

First we need the UID of the Rule we want to change. Go to the Rules tab in PaperUI and scroll down to the Rule you want to change. In this case we are looking for the Rule “Heating/Cooling Check”. Click the alarm clock icon next to the Rule to disable it.

Under the Rule description there is a long random string of letters and numbers. This is the UID for the Rule. Copy that to the clipboard.

Return to the REST API Docs and open the “GET /rules/{ruleUID}” row and query for the JSON that defines this Rule. Paste in the Rule UID and click “Try it out!”. Make sure you don’t have an extra space at the front or back of the UID.

The JSON in the “Response Body” is the Rule definition.

Copy this and paste it into a text editor.

In this case the JSON is

{
  "status": {
    "status": "UNINITIALIZED",
    "statusDetail": "DISABLED"
  },
  "triggers": [
    {
      "id": "1",
      "label": "HeatingCheck received command ON",
      "description": "This triggers the rule if an item receives a command.",
      "configuration": {
        "itemName": "HeatingCheck",
        "command": "ON"
      },
      "type": "core.ItemCommandTrigger"
    },
    {
      "id": "4",
      "label": "CoolingCheck received command",
      "description": "This triggers the rule if an item receives a command.",
      "configuration": {
        "itemName": "CoolingCheck",
        "command": "ON"
      },
      "type": "core.ItemCommandTrigger"
    }
  ],
  "conditions": [
    {
      "inputs": {},
      "id": "2",
      "label": "TempSetpoint and MyTempProxy are valid",
      "description": "Allows the definition of a condition through a script.",
      "configuration": {
        "type": "application/javascript",
        "script": "var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');\nload(OPENHAB_CONF + '/automation/lib/hestia/utils.js');\n\nvar logName=\"heatcool\";\n\nvar ok = (items[\"TempSetpoint\"] != NULL || items[\"TempSetpoint\"] != UNDEF ||\n          items[\"MyTempProxy\"] != NULL || items[\"MyTempProxy\"] != UNDEF);\n\nif(!ok){\n  logError(logName, \"Cannot determine heating/cooling: TempSetpoint = \" + items[\"TempSetpoint\"] + \" MyTempProxy = \" + items[\"MyTempProxy\"]);\n}\n\n(ok);"
      },
      "type": "script.ScriptCondition"
    }
  ],
  "actions": [
    {
      "inputs": {},
      "id": "3",
      "label": "Determines whether to turn on/off the heater/ac based on temp and setpoint.",
      "description": "Sends command to Heating/CoolingCtrl to change device's state.",
      "configuration": {
        "type": "application/javascript",
        "script": "var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');\nload(OPENHAB_CONF + '/automation/lib/hestia/utils.js');\n\nvar mode = event.itemName.replace(\"Check\", \"\");\nvar delta = items[\"TempSetpoint\"].floatValue() - items[\"MyTempProxy\"].floatValue();\n\n// TODO: Replace 0 with hysteresis, if heating check for \n// positive dela, if cooling check for negative delta\nvar turnOn = (mode == \"Heating\") ? (delta > 0) : (delta < 0);\n\nvar cmd = (items[mode+\"Mode\"] == \"Boost\" || turnOn) ? ON : OFF;\nevents.sendCommand(mode+\"Ctrl\", cmd);\n\nsleep(10); // keep the rule from running again too soon"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "uid": "7f38faaa-ed29-49a9-a5ff-180de02bcb84",
  "name": "Heating/Cooling Check",
  "tags": [],
  "visibility": "VISIBLE",
  "description": "Determines when to turn on or off the heating or cooling"
}

Delete the “status” section.

Delete the “uid” at the bottom.

Change the “name” to “Copy of Heating/Cooling Check”.

{
  "triggers": [
    {
      "id": "1",
      "label": "HeatingCheck received command ON",
      "description": "This triggers the rule if an item receives a command.",
      "configuration": {
        "itemName": "HeatingCheck",
        "command": "ON"
      },
      "type": "core.ItemCommandTrigger"
    },
    {
      "id": "4",
      "label": "CoolingCheck received command",
      "description": "This triggers the rule if an item receives a command.",
      "configuration": {
        "itemName": "CoolingCheck",
        "command": "ON"
      },
      "type": "core.ItemCommandTrigger"
    }
  ],
  "conditions": [
    {
      "inputs": {},
      "id": "2",
      "label": "TempSetpoint and MyTempProxy are valid",
      "description": "Allows the definition of a condition through a script.",
      "configuration": {
        "type": "application/javascript",
        "script": "var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');\nload(OPENHAB_CONF + '/automation/lib/hestia/utils.js');\n\nvar logName=\"heatcool\";\n\nvar ok = (items[\"TempSetpoint\"] != NULL || items[\"TempSetpoint\"] != UNDEF ||\n          items[\"MyTempProxy\"] != NULL || items[\"MyTempProxy\"] != UNDEF);\n\nif(!ok){\n  logError(logName, \"Cannot determine heating/cooling: TempSetpoint = \" + items[\"TempSetpoint\"] + \" MyTempProxy = \" + items[\"MyTempProxy\"]);\n}\n\n(ok);"
      },
      "type": "script.ScriptCondition"
    }
  ],
  "actions": [
    {
      "inputs": {},
      "id": "3",
      "label": "Determines whether to turn on/off the heater/ac based on temp and setpoint.",
      "description": "Sends command to Heating/CoolingCtrl to change device's state.",
      "configuration": {
        "type": "application/javascript",
        "script": "var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');\nload(OPENHAB_CONF + '/automation/lib/hestia/utils.js');\n\nvar mode = event.itemName.replace(\"Check\", \"\");\nvar delta = items[\"TempSetpoint\"].floatValue() - items[\"MyTempProxy\"].floatValue();\n\n// TODO: Replace 0 with hysteresis, if heating check for \n// positive dela, if cooling check for negative delta\nvar turnOn = (mode == \"Heating\") ? (delta > 0) : (delta < 0);\n\nvar cmd = (items[mode+\"Mode\"] == \"Boost\" || turnOn) ? ON : OFF;\nevents.sendCommand(mode+\"Ctrl\", cmd);\n\nsleep(10); // keep the rule from running again too soon"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "name": "Copy of Heating/Cooling Check",
  "tags": [],
  "visibility": "VISIBLE",
  "description": "Determines when to turn on or off the heating or cooling"
}

Copy the edited JSON and return to the REST API docs and paste into the “body” field and press “Try it out!”

Return to PaperUI and you should now find “Copy of Heating/Cooling Check”. It will have it’s own new unique UID and the original Rule will still show as uninitialized/disabled.

Editing the copy to make your changes

Click on the Rule and it will open a screen showing the three parts of a Rule.

  • When… : This is where you define the events that cause this Rule to run. In this case if HeatingCheck receives command ON or if CoolingCheck receives command ON. Add/remove triggers that will cause the Rule to run here.

  • but only if… : For those familiar with the .rules files in openHAB, this section is new. This is where you can define conditions that must be true before the Rule will run. In this case we have a short little bit of code to make sure that the setpoint and current temperature readings are valid. You can do more interesting things in this clause too. For example, if you attempt to change the temperature unit to something other than F or C, this clause for the Rule that processes that change will revert the Item to what ever it was set to before that.

  • then… : This is where the actions to take place are defined for the Rule. For simple actions like sending a command to a given Item, there is no need for code her, but most of the time a Script Action will define a script that includes some code.

For HestiaPi, the source code for Script Actions and Script Conditionals is Nashorn JavaScript.

In this example, we want to add hysteresis to the decision to turn on or off the heater or air conditioner. I don’t want the heater to come on until the temperature falls below three degrees F below the setpoint and the heater to turn off when the setpoint is reached. That leaves a buffer between when the heater turns on and off which will prevent it from rapidly cycling on and off when the temperature is hovering around the target temp.

Visually the difference is quite noticeable. Here is a graph without hysteresis:

The graph shows a 24 hour period. Notice all the short little bursts of heating (red bars at the bottom). When you zoom in you will see that there are cases where the heater turned on and off several times over the course of five minutes.

Here is a graph using a 3 degree f hysteresis:

As you can see, the heater comes on for longer periods but far less often. This is easier on the HVAC unit and uses less energy over all.

Click on the cog icon in the then… clause to bring up the script.

image

The code itself is in the “Script” text box. The current code is:

var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');
load(OPENHAB_CONF + '/automation/lib/hestia/utils.js');

var mode = event.itemName.replace("Check", "");
var delta = items["TempSetpoint"].floatValue() - items["MyTempProxy"].floatValue();

// TODO: Replace 0 with hysteresis, if heating check for 
// positive dela, if cooling check for negative delta
var turnOn = (mode == "Heating") ? (delta > 0) : (delta < 0);

var cmd = (items[mode+"Mode"] == "Boost" || turnOn) ? ON : OFF;
events.sendCommand(mode+"Ctrl", cmd);

sleep(10); // keep the rule from running again too soon

We will expand the line that populates turnOn so it uses a hysteresis buffer.

var OPENHAB_CONF = Java.type('java.lang.System').getenv('OPENHAB_CONF');
load(OPENHAB_CONF + '/automation/lib/hestia/utils.js');

var mode = event.itemName.replace("Check", "");
var setpoint = items["TempSetpoint"].floatValue();
var currTemp = items["MyTempProxy"].floatValue();
var delta = setpoint - currTemp;
var hyst = 3;

var turnOn = (mode == "Heating") ? (delta >= hyst) : (delta < (hyst*-1));
var turnOff = (mode == "Heating") ? (currTemp >= setpoint) : (currTemp <= setpoint);

// Always turn on with boost mode
if(items[mode+"Mode"] == "Boost") turnOn = true;

if(turnOn || turnOff) {
  // If both are true it means we are in boost mode so have turnOn
  // take precidence.
  var cmd = (turnOn) ? ON : OFF;
  commandIfDifferent(mode+"Ctrl", cmd);
}
// do nothing between setpoint and setpoint +- hyst

sleep(10); // keep the rule from running again too soon

Click “OK” and then the blue check icon at the top to save your changes.

Your customized Rule is now live and running, test it out. In this case we can change the temperature setpoint to trigger the Rule to run and make sure it does what it is supposed to without error. If it doesn’t work, add some logging . When you import utils.js (see the second line of the script above) you will get access to logDebug, logInfo, et. al. which you can use to log out to /var/log/openhab2/openhab.log. If you get stuck, ask here on the forum.

If you really run into trouble, it’s a simple matter to revert to the stock Rules. Just disable your new Rule and re-enable the original Rule.

Upgrades

In an upgrade you run the risk of losing your customized Rules. There are several approaches you can take to prevent/recover from this. I recommend the following:

  • always run a backup before an upgrade and save the backup.zip file somewhere where it won’t become impacted by the upgrade process (e.g. /home/pi)
  • using the REST API and instructions from above, get your modified Rule through the REST API and save it off to a text file; after the upgrade restore the Rule using the REST API. You will want to compare your customized Rule with the new version that came with the upgrade and merge the differences in your restored Rule.

Conclusion

I realize this over all approach seems a bit awkward and I plan on writing some tools that will make it a bit easier. But the advantages of the new JSONDB Rules and this approach include:

  • much better boot time
  • faster development cycle as it no longer takes 15-20 minutes to parse and load the monolithic .rules file
  • you do not need to ssh to the HestiaPi to customize it, almost everything is updatable through PaperUI
  • not yet mentioned, but as you make changes through PaperUI, backups are automatically created in /var/lib/openhab2/jsondb/backup
  • easier to make changes on an active HestiaPi with a lower risk of breaking it for long periods of time; all you need to do is disable your modified rule and reenable the original rule.

Soon the maintainers of HestiaPi will be cutting a new SD card image with these new JSONDB configs for early adopters to play with. For those who want to get started sooner, see Interoperability with other OpenHAB instance which shows the commands needed to upgrade from v1.1 to this pre-alpha/alpha test version.

I’m happy to answer and questions or comments. Good luck!

2 Likes

@rlkoshak Rich, curious how you created the graph in the tutorial, can you point me in the right direction? What persistence do you use that doesn’t kill the PiZero?

I don’t think the graph is generated on HestiaPi :thinking:

I store the states for Items I want to chart into InfluxDB using persistence and I used Grafana to generate the chart. See the InfluxDB+Grafana Tutorial for how to set it up. I believe Grafana works with MySQL/ MariaDB and a few other databases as well.

You can embed the charts from Grafana right on your sitemap, but I like to embed static images instead so I used the Grafana Image Renderer.

You will need to host InfluxDB and Grafana on some other machine separate from your HestiaPi. There is simply not enough RAM to support them and OH on an RPi 0.

If you wanted to so it all on the HestiaPi, you can use rrd4j as your database and the chart element. Those graphs don’t look as nice nor are they as flexible, but it would let you put it all on the same machine. NOTE: rrdj4 does not store Switch Items so you would need to use a proxy Number Item to generate those bars showing the ON/OFF status of the heater/fan.

No but some charts could be. And the data for those charts are generated from the HestiaPi.

Among the many things on my to-do list is a tutorial showing who to create charts and an emailed monthly report showing how much time was spend heating/cooling/humidifier/etc.

2 Likes

Thanks Rich. I’ve played with persisting to SQLite and using the standard OH2 charts before, fairly straight forward to do. I did run into the switch issue you mention. Was curious how you got those fancy charts! Will take a look at Grafana.

I ended up installing containerized versions of InfluxDB and Grafana on my local docker host server, which is a 2011 i5 Mac Mini. Got the persistence set up writing to the remote InfluxDB instance and it all works very well. Seems a little more complicated than it should though?

I too run them all using Docker.

I’m not sure what could be done to make it simpler. You are integrating three difference services from three separate vendors in three different ways:

  • openHAB to InfluxDB to store the data
  • InfluxDB to Grafana to configure the charts
  • Grafana back to openHAB to display them on your sitemap (if you choose)

And Grafana is a fully realized charting service so there is going to be a lot of options to sort through.

The built in charts for HABPanel look better than those for Sitemaps and they have more options available but not nearly so many as Grafana which makes it simpler to use. However, it means using HABPanel instead of BasicUI which would require one to build their UI from scratch.

If the RPi 0W had more RAM and at least two cores of CPU the SD image could come with it already installed and configured but that’s not the case unfortunately.

1 Like

It’s under “MISC” in my version (1.1) of HestiaPi.

The above assumes the use of the new JavaScript NGRE Rules (the ONE branch currently on github) which also assumes an upgraded openHAB version. If you are seeing REST API documentation under MISC, you are still running OH 2.4. I cannot guarantee that the v1.2 stuff will work on that old of an OH version. In particular there are potentially some changes in the MQTT Things that require a later version of OH.

See Interoperability with other OpenHAB instance for the steps necessary to do an in place upgrade from v1.1 to the WIP v1.2 version which includes the rewritten Rules and configurations and some new features.

If you stay on v1.1, which is reasonable, than this thread doesn’t apply to you at all. The Rules for v1.1 are written in default.rules so modifying them is a completely different procedure.

If you just want to access the REST API Docs, than indeed install if from the MISC tab instead of the UIs tab. But pretty much everything else in the OP will not apply to you.

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