New Automation: PID Controller

@fwolter I use OH3 but I have a problem with the addon it’s not allowing me to pick the items (it’s showing NA).

I have downloaded latest version (2021.01.19) - I have put it in /usr/share/openhab/addons

Then I have changed the owner of the file - sudo chown openhab org.openhab.automation.pidcontroller-3.1.0-SNAPSHOT.jar

Next was sudo systemctl restart openhab

Am I doing something wrongly?

The Item config parameters were set to read-only, which is obviously wrong. Seems like the UI didn’t honor this flag until now. However, I set the read-only flag to false and updated the link in the original post.

@fwolter After few tries with cleaning up the cache, I finally got it working but I see that my Eurotronic Spirit radiator is not responding to the proposed values at all. I’m not sure what I did wrongly.

When set up dimmer manually (50%):

2021-01-24 13:34:02.548 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'SypialniaGrzejnikNode13_Dimmer' received command 50.0

2021-01-24 13:34:02.567 [INFO ] [penhab.event.ItemStatePredictedEvent] - Item 'SypialniaGrzejnikNode13_Dimmer' predicted to become 50.0

2021-01-24 13:34:02.588 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'SypialniaGrzejnikNode13_Dimmer' changed from 35 to 50.0

But when I try to set it using rules I start to get Errors/Exceptions

2021-01-24 13:34:28.927 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'SypialniaGrzejnikNode13_Dimmer' received command 0.6570000000000004

2021-01-24 13:34:59.547 [WARN ] [l.handler.PIDControllerActionHandler] - Command was not posted because either the configuration was not correct or a service was missing: ItemName: SypialniaGrzejnikNode13_Dimmer, Command: null, eventPublisher: org.openhab.core.internal.events.OSGiEventPublisher@37f93b, ItemRegistry: org.openhab.core.internal.items.ItemRegistryImpl@10f252e

Additionally I see exception:

2021-01-24 13:51:26.771 [WARN ] [ab.core.internal.events.EventHandler] - Dispatching/filtering event for subscriber 'org.openhab.core.events.EventSubscriber' failed: No value present
java.util.NoSuchElementException: No value present
        at java.util.Optional.get(Optional.java:148) ~[?:?]
        at org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler.receive(PIDControllerTriggerHandler.java:208) ~[?:?]
        at org.openhab.core.internal.events.EventHandler.lambda$0(EventHandler.java:151) [bundleFile:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
        at java.lang.Thread.run(Thread.java:834) [?:?

So now the question is how debug process should look like? Where to start analysis?
My rule:

firefox_2021-01-24_13-59-32

I think I’m missing an command compared to a simple rule to set up the dimmer:
firefox_2021-01-24_14-03-02

PS. I guess the feature request would be to get INT instead of Float as an output.
For sure I can do a virtual item and then round it on every update but I guess having an option to receive an integer instead of float would be demanded by more items/channels.

Here is my Rule if it helps:

rule "Kitchen TRV PID Control Handler"
when
    Item CentralHeatingState changed to ON or
    Item KitchenTrvPidValvePosition changed 
then
    if (CentralHeatingState.state == ON)    {
        var int positiveOnlyPidTargetTrvValvePosition = new BigDecimal((KitchenTrvPidValvePosition.state as Number).floatValue).setScale(1, RoundingMode.HALF_UP).intValue()
        // cap valve positions to between 0 & 100
        if (positiveOnlyPidTargetTrvValvePosition < 0)    {
            positiveOnlyPidTargetTrvValvePosition = 0
        }
        if (positiveOnlyPidTargetTrvValvePosition > 100)    {
            positiveOnlyPidTargetTrvValvePosition = 100
        }
        logInfo("Kitchen", "PID control: target TRV valve position is " + positiveOnlyPidTargetTrvValvePosition + "%")

        var int currentTrvPidValvePosition = (KitchenTrvValvePosition.state as Number).intValue()
        logInfo("Kitchen", "PID control: current TRV valve position is " + currentTrvPidValvePosition + "%")

        if (positiveOnlyPidTargetTrvValvePosition != currentTrvPidValvePosition)     {
            if (positiveOnlyPidTargetTrvValvePosition == 0 
                || positiveOnlyPidTargetTrvValvePosition >= 100
                || Math::abs(positiveOnlyPidTargetTrvValvePosition - currentTrvPidValvePosition)  >= 1)   {
                    logInfo("Kitchen", "PID control: setting TRV valve position to " + positiveOnlyPidTargetTrvValvePosition + "%")
                    KitchenTrvValvePosition.sendCommand(positiveOnlyPidTargetTrvValvePosition)
                }
        }
    }
end 

You also really need to RESET the PID when the Setpoint changes or when you start the heating (e.g. turn on the boiler) to clear any accumulated Integral.

rule "Kitchen TRV PID Setpoint Change Handler"
when
    Item KitchenTrvPidSetpoint changed
then
    // reset PID controller
    logInfo("Kitchen", "Setpoint changed, resetting Kitchen TRV PID controller")
    KitchenTrvPidCommandItem.sendCommand("RESET")
end
rule "Kitchen Central Heating On"
when
     Item CentralHeatingState changed to ON
then
       // reset PID controller
        logInfo("Kitchen", "Resetting Kitchen TRV PID controller")
        KitchenTrvPidCommandItem.sendCommand("RESET")
 end

You’ll need these imports at the top of the Rule file for the rounding bit:

import java.lang.Math
import java.math.BigDecimal
import java.math.RoundingMode

I didn’t do a rebase. The NoSuchElementException should be fixed now.

@Morgano thanks - I implemented 2 out of 8 valves using scratch in very similar manner - bit of try catch - need to move those to actual code later.
Finally I ended up with virtual items like dimmer / setPoint temperature / Temperature higher one extracted from two thermometers / and PID controller for resetting the stats.

How did you figure out the PID values? As of now I just placed random values to make it work :wink:

@fwolter even after update I’m still getting those WARN messages. Not sure from where it’s coming from.

2021-01-24 19:14:00.995 [WARN ] [ab.core.internal.events.EventHandler] - Dispatching/filtering event for subscriber 'org.openhab.core.events.EventSubscriber' failed: No value present

java.util.NoSuchElementException: No value present

	at java.util.Optional.get(Optional.java:148) ~[?:?]

	at org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler.receive(PIDControllerTriggerHandler.java:208) ~[?:?]

	at org.openhab.core.internal.events.EventHandler.lambda$0(EventHandler.java:151) [bundleFile:?]

	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]

	at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]

	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]

	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]

	at java.lang.Thread.run(Thread.java:834) [?:?]

@Dominik_Jeziorski
These are my current values:

  - id: "1"
    configuration:
      input: BedroomMultisensorSensorTemperature
      setpoint: BedroomTrvPidSetpoint
      kp: 65
      kd: 0
      kdTimeConstant: 1
      commandItem: BedroomTrvPidCommandItem
      ki: 0.1
      loopTime: 60000
    type: pidcontroller.trigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      output: BedroomTrvPidValvePosition
      pInspector: BedroomTrvPidPInspectorItem
      iInspector: BedroomTrvPidIInspectorItem
      dInspector: BedroomTrvPidDInspectorItem
      eInspector: BedroomTrvPidErrorInspectorItem
    type: pidcontroller.action

Have a look here for tuning tips:

You basically start with just the ‘P’ part then add the ‘I’. I believe the ‘D’ part is not normally used for heating systems as the response is slow anyway so it is not really required.

@Dominik_Jeziorski Looks like you’re running an old version. You can check the currently running version via the openHAB console:

openhab> bundle:list | grep -i pid
227 x Active x  80 x 3.1.0.202101241440      x openHAB Add-ons :: Bundles :: Automation :: PID Controller

@fwolter you were right for some reason I had 2 versions running - cleared tmp/cache and old version is gone.

Now I need some help with tunning - I used the values that @Morgano used (kp=65, ki=0.1) but it seems that I’m always undershot the desired setpoint.

I used virtual devices.

  • Blue = desired setpoint
  • Violet = Virtual temp based on which it should work (Xiaomi Aquara)
  • Light blue = Spirit Thermostat
  • Orange = additional (Xiaomi Aquara) that I can move around the house
  • Green = Dimmer

It seems that dimmer is shut down before setpoint is reached.
As getting the results is quite slow based on your experience should leave kp=65 and increase ki 0.1 => 0.3 or maybe even more?

Feature request:

Just after openhab restart I see in the the log that virtualset points are not set. It’s hard to figure out what items are not set properly:

2021-01-27 22:30:00.443 [WARN ] [.handler.PIDControllerTriggerHandler] - Setpoint item: Item type is not a number: UnDefType: NULL

Second:
After sending RESET command I’m getting WARN - did I did something wrongly or it’s a bug?

2021-01-27 22:44:58.908 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Gabinet_PID_Control' received command RESET

2021-01-27 22:44:58.921 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Gabinet_PID_Control' changed from NULL to RESET

2021-01-27 22:44:58.927 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Gabinet_PID_Control' changed from RESET to NULL

==> /var/log/openhab/openhab.log <==

2021-01-27 22:44:58.931 [WARN ] [.handler.PIDControllerTriggerHandler] - Unknown command: NULL

I’m not an expert in PID Tuning, but my understanding the ‘I’ component is there to compensate for steady state error. So if the temperature remains steady (not oscillating) but it is below the setpoint, then you could try to increase Ki a little. This should help pull the temperature up to the set point. It seems with the Ki component a little goes a long way as it accumulates over time.

Hi, thanks for this automation!!! I’m very interested on it.

I understand why when the set point changes the calculation is made, but why is done when the input item (meassure) changes too?

Untill now all the info I have readed only takes it on account when the calculation is made at the end of the cicle in order to obtain the output for the next.

In the orther hand, it would be great that all the options (kp, ki, …) where configurable by items as I have read in other comments.

Because that is the goal of a PID controller - to create a correction when the input e.g. room temperature does not match the desired value (e.g room temperature setpoint). It is a continual repeating process with a goal of creating a set of output changes (e.g radiator valve position) that cause the input to eventually match the setpoint and be held there.

The output calculation is repeated regularly to allow the system to reach the setpoint and to also to cope with any external changes e.g. if the temperature outside a house drops, the room temperature might drop due to the extra heat loss, so the PID controller may need to open the radiator valve slightly more to compensate, and to keep the room at the setpoint.

But this not causes the calculation been done at every change of the temperarute? For example, if the meassure resolution is 0.01 grades when the output is high the meassure item will be changing too fast and the calculation done with each change.

Untill now I have been doing my PID control with a flow implemented in node-red. The PID is been calculated at 10mins intervals in which the heater can be ON for a time based in the output. And OFF the rest of the cicle. If one of the parameters is changed the cicle is restarted. But the input is readed only at calculations.

@aitorforero
You could probably do something like only switch the heater on if the PID output is indicating at least, say, 2 minutes of heat are required for the cycle. You would ignore shorter activations. The error would increase and so would the output time on following cycles until the activation time becomes > 2 mins and the heater would be turned on.
Maybe what you are doing (PID control) is slightly unusual for an on/off style heater or not a good fit? Did you try a simpler ‘bang/bang’ temperature control with hysteresis?

@Morgano is right. You have number as an output of calculation. It’s “kind of” infinite number. In my case anything above 100 means that heater is still opened fully. Below 0 means that i need to push the dimmer to 0 - between I can adjust the dimmer. In your case you could wait till output is some number like 100 and the leave heating working until calculated value will drop below 100.

I read that Electric heaters are also controlled by PID but there is this param calculated how much time it should stay ON to reach desired temperature.
Stove for ceramics for example. But don’t know the implementation details.

My intention is to use a time-proportional PID control. Maybe I have missunderstud how often the cycle must be calculated.

Till now what I have been doing is reading the output and calculate de proportinal time of the new cycle when the switch must be ON. Then wait until whole cycle ends and I reacalculate it again.

What you are saying is that this calculation has to be done at evey change of temperature?

@aitorforero
No, the PID cycle time (loop time) is normally based on the characteristics of the system. If it is a ‘slow’ system, like a heating system then the adjustment time will be in the ‘minutes’ realm as control changes take a while to have any noticeable effects.
On the other hand, if you were controlling something like the flight stability of a drone (fast system), the PID cycle would be in the ‘milliseconds’ realm as control changes would take effect quicker, and are needed much more often.

There is a mention of this is the README: openhab-addons/README.md at main · openhab/openhab-addons · GitHub

“The loopTime should be max a tenth of the system response. E.g. the heating needs 10 min to heat up the room, the loop time should be max 1 min. Lower values won’t harm, but need more calculation resources”

@Dominik_Jeziorski , @Morgano

I have added a second action to the rule that is where I set ON or OFF the heater:

this.logger = this.logger || Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
this.ZonedDateTime = this.ZonedDateTime || Java.type("java.time.ZonedDateTime");
this.ChronoUnit = this.ChronoUnit || Java.type("java.time.temporal.ChronoUnit");

var HeaterSwitchItem = "CalefaccionCaldera";
var PIDOutputItem = "CalefaccionOutput";
var CycleLengthItem = "CalefaccionIntervalo";

function calculate() {
  this.onTime = interval * ir.getItem(PIDOutputItem).getState();
  this.lastTime = now;
  this.logger.trace("New interval calculated {} at {}. ON Time: {}", [interval, this.lastTime, this.onTime])
}

function turnOn() {
    utils.mandarSoloSiCambia(HeaterSwitchItem, ON);
}

function turnOff() {
    utils.mandarSoloSiCambia(HeaterSwitchItem, OFF);  
}

try {
  var now = ZonedDateTime.now();
  var interval = ir.getItem(CycleLengthItem).getState() * 60;


  if(!this.onTime) {
    calculate()
  }

  var dt = this.lastTime.until(this.now , ChronoUnit.SECONDS);
 
  
  if(dt >= interval) {
    this.logger.info("Loop!!!");
    calculate();
  }

  if(dt > this.onTime) {
    turnOff();
  }  else {
    turnOn();
  }
  
} catch(e) {
  this.logger.error(e.message);
}

It’s kind of a bad timing as the heating period is coming to an end on the northern hemisphere, but I released a PWM module to control valves more easily in conjunction with a PID controller.

Hi

I have a loop were I need to change the PID controller to work Reversed

How can this be obtained?