Solution for thermostat overshooting (modulation)

Atm installing underfloor heating in my home. Planing to use separated wireless temp sensors for rooms and then openhab (preferably, or some other system) to make decisions on valve control and need a mechanism to do modulated temperature control based on time taking for each room to reach temp and drop it. I have no knowledge on heating algorithms, curves etc. So far I found Smart Virtual Thermostat (beta version) but it seems abandoned. Has anyone solved it using OpenHab rules? Maybe there is some online/cloud virtual thermostat openhab could feed temp measurement info and get back instructions on when to turn off heating for a zone (did googling but came up with nothing :confused: )?
Thank you in advance!

I did a quick scan of the rules and I see no red flags that would necessarily make them not work in OH 4. They might need some minor edits but we can help with that.

However, there is an automation addon which implements PID which is probably a better fit. That definitely is maintained.

There is a PWM add-on also which might be suitable.

Both of these are the standard approaches used in commercial thermostats and in industrial applications to solve the overshoot problem.

any idea if NSPanel is using pwm/pid?

I’ve no idea. I know nothing about this device. Given that it doesn’t say though I’d guess it probably just uses simple hysteresis. They try to do so much with one device so I doubt they spend all that much time an effort to implement PID or PWM for the thermostat at all.

Imo, this type of control is a low level function that should be handled by a dedicated hardware, not by a home automation system.

The home automation system should only deal with adjusting the set temp or perhaps control it’s on/off power state. Not involved in the actual PID process.

1 Like

I could go and buy stock hardware (e.g. uponor or similar solution) but It’d way more expensive.
I have been using Openhab for diff automations around the house so I’d like to use it for modulated heating control as well. I have heated floor with mutiple zones, each zone would have temp sensor reporting back to hab, have then would decide when to turn on/off corresponding zone valves, it also needs individual scheduling. If I’d go for brand products I’d end up spending ~1k for all the automation devices whereas I could get away only with some zigbee devices (coordinator, temp sensors …) and individual switches for valves and that’s it.
Until now all my OH automations are done in rules but I just have no idea (or example) how to use OH’s modulation tools :confused: info is super scarce.
I don’t think the brand solutions are doing much more than simple learning in a form of overshoot control, especially ones that are just generic thermostats (sensor + relay) if not dealing with controllers and consumers the specs of which would be dead known.

If you want to DIY it, at least put the logic in an ESP8266 / ESP32 and let that control the PID. It’s a lot more “set and forget” and not much will happen to it, but you should use wired sensors, not zigbee. The idea is to make it self contained, not relying on external network, such as Zigbee (+ Zigbee coordinator), MQTT, Wifi, etc etc, so even if your computer breaks, HDD corrupted, wifi reboots, etc, it will still work.

Anyway if you still want to use openhab, you of course can.

Don’t forget your time has value as well.

would def go for a wired setup but that’s no longer an option :confused:
already started looing into esphome direction or smth ~

oh for sure :slight_smile:

I did something like your installation for my heating. As i don‘t have my notebook near, i don‘t know if it‘s in JS or in DSL (i migrated most rules a time ago) but i can tell the idea and some considerations i made:

I have 230V based valves for low temperature floor heating. I curcuited them in parallel to classic analogue (bimetal) wall thermostats, just in case, in the last season i didn‘t had to use them. They are easy to use, no need to tell somebody how to heat if needed, so i left them installed.

The devices i use to switch the valves are Shelly 2PM, but the PM version is not required. They are nice because just in case they have a web interface you can switch manually and offline if openHAB would be broken.

As temperature sensor you can use any source which is accurate enough to provide a room temperature. I use some Xaiomi zigbee sensors. Any other some how openHAB compatible will also do. I would not use some integrated sensors as they are typically biased.

My rules for heating use persistence to determine the average temperature and i‘m heating at a maximum rate of 45 min, doing 30 min or so pause to prevent overshooting, i‘m now not sure whether i saved the start/pause to persistence or rule cache. Also i start heating only if the temperature is quite a bit below the target temperature. The accuracy is in my case pretty good.

If wanted, i can share my rules when i have my notebook near, but they may need some refactoring or a little bit love to be general usable.

I do have my heating working for several years. Right now I drive the valves with “Möhlenhoff Stellantrieb Alpha-5” and a Raspberry solution. After the next planned update I will use “Möhlenhoff Stellantrieb Alpha-5” driven by Shelly 1. That makes no difference because the magic for each circle is done in openHAB with a pretty customized rule. The most important part is keeping a stable temperature. This is made with a procedure I have found some place I cannot remember where it was. It is only looking for a temperature difference of +/- 0.2 degrees and is acting appropriate.

The “Möhlenhoff Stellantrieb Alpha-5” needs about 210 seconds to open. Then I defined a time slot of 900 seconds. The open valve time is than determined by the percentage result of this code.

DSL
          var Number itis = Temperature.state as DecimalType
          var Number should = ShouldBeTemperature.state as DecimalType
          var Number valveOpenProcent
 
         if (itis > (should + 0.2)) {
            valveOpenProcent = 0
          }
          else if (itis < (should - 0.2)) {
            valveOpenProcent = 100
          }
          else {
            valveOpenProcent = 100 * ((should + 0.2 - itis )/ 0.4)  // 100 * (T_should+0,2 – T_is) / 0,4
            if (valveOpenProcent < 18) {
              valveOpenProcent = 0
            }
          }
 

currently testing out self-correcting PID on a small scale (small pcb heater board + dht22), will move to using tile an a bigger pcb heater after. Idea is to keep overshoot/undershoot oscillations to a minimum and have it try to adjust PID values automatically.
With a small heart source (that is warming quite fast) results are within 0.3 - 0.5C overshoot (undershoot is very minimal but imho due to pcb’s fast heatup times).

// Initial PID constants
val initialKp = 0.4
val initialKi = 0.004
val initialKd = 0.03
var Kp = initialKp
var Ki = initialKi
var Kd = initialKd

val integralMax = 5.0
val integralMin = -5.0
val deadband = 0.1

// State variables
var integral = 0.0
var previousError = 0.0
var autoTuning = false
var oscillationCount = 0
var tuningStartTime = now

rule "Zone1 - PID Temperature Control with Historical Data and Auto-tuning"
when
    Item Zone1TempSensor received update or
    Item Zone1TempTarget received update
then
    val temperature = (Zone1TempSensor.state as Number).floatValue
    val target = (Zone1TempTarget.state as Number).floatValue
    
    // Fetch the last 6 persisted values
    var totalTemp = 0.0
    var count = 0
    for (i : 0..5) {
        val historicState = Zone1TempSensor.historicState(now.minusSeconds(i * 10), "rrd4j")
        if (historicState !== null) {
            totalTemp += (historicState.state as Number).floatValue
            count++
        }
    }

    // Calculate the moving average
    val movingAverage = totalTemp / count

    // Estimate the rate of change
    val rateOfChange = (temperature - movingAverage) / (count * 10)

    // Predict the temperature in the next cycle
    val predictedTemperature = temperature + rateOfChange * 10

    // Enhanced Predictive control
    if (rateOfChange > 0 && predictedTemperature >= target) {
        if (Zone1ValveControl.state != OFF) {
            Zone1ValveControl.sendCommand(OFF)
        }
        return;
    }

    if (rateOfChange < 0 && predictedTemperature <= target) {
        if (Zone1ValveControl.state != ON) {
            Zone1ValveControl.sendCommand(ON)
        }
        return;
    }

    // Calculate the error
    val error = target - temperature

    // Auto-tuning logic
    if (autoTuning) {
        // Monitor oscillations
        if (Math.abs(error) > deadband && Math.signum(error) != Math.signum(previousError)) {
            oscillationCount++
        }

        // Adjust PID constants based on observed oscillations
        if (oscillationCount > 5) { // Example threshold, adjust as needed
            Kp += 0.01 // Example adjustment, adjust as needed
            Ki += 0.001
            Kd += 0.005
            oscillationCount = 0
        }

        // Exit auto-tuning if stabilized or if tuning for too long
        if (Math.abs(error) <= deadband || now.minusMinutes(10).isAfter(tuningStartTime)) {
            autoTuning = false
            oscillationCount = 0
        }
    } else {
        // Enter auto-tuning mode if oscillations detected
        if (Math.abs(error) > deadband && Math.signum(error) != Math.signum(previousError)) {
            oscillationCount++
            if (oscillationCount > 5) { // Example threshold, adjust as needed
                autoTuning = true
                tuningStartTime = now
                Kp = initialKp
                Ki = initialKi
                Kd = initialKd
            }
        } else {
            oscillationCount = 0
        }
    }

    // Calculate the integral term with windup limits
    integral += error
    if (integral > integralMax) integral = integralMax
    if (integral < integralMin) integral = integralMin
    
    // Calculate the derivative term
    val derivative = error - previousError
    
    // Calculate the output
    val output = Kp * error + Ki * integral + Kd * derivative
    
    // Update the heater state based on the output and deadband
    if (Math.abs(error) > deadband) {
        if (output > 0) {
			if (Zone1ValveControl.state != ON) {
            	Zone1ValveControl.sendCommand(ON)
			}
            integral = 0.0
        } else if (output <= 0) {
			if (Zone1ValveControl.state != OFF) {
				Zone1ValveControl.sendCommand(OFF)
			}
            integral = 0.0
        }
    }	
    
    // Save the current error for the next rule execution
    previousError = error
end

I’ve been using Openhab to handle my underfloor heating for a good few years now. I swapped out my old 3-wire analog thermostats with DIY Sonoff basic switches. Just so you know, you can hook up a temp sensor to Sonoff basics, so each thermostat only set me back around £10. I realised the temp reading was a tad too high, so I’m also using Sonoff SNZB temp sensors on the other side of the room to get an average temp, which helps bring down the higher thermostat reading.

I’m not fully getting the rules you’ve set up, but to me, it seems like they might be a bit too complex. My rules are straightforward—like kicking the heating on when it dips below 20°C, and stuff like that.

I use this to figure out the avg temp

rule "loungeAvgTemp"
when
    Item Lounge1_Temperature changed
or
    Item LoungeZig_Temperature changed
then
    var t = ((Lounge1_Temperature.state as Number) + (LoungeZig_Temperature.state as Number)) / 2
    Lounge_Temperature.postUpdate(t)
end

and this to turn the therm on and off

rule "Lounge Temperature"
when
    Item loungeAvgTemp changed
then
    if (loungeAvgTemp.state != NULL && Lounge_Heating_TargetTemp.state != NULL) {
        if (loungeAvgTemp.state instanceof DecimalType && Lounge_Heating_TargetTemp.state instanceof DecimalType) {
            if((loungeAvgTemp.state as DecimalType) <= (Lounge_Heating_TargetTemp.state as DecimalType)) {
                Lounge_Heating_Actuator.sendCommand(ON)
            } else {
                Lounge_Heating_Actuator.sendCommand(OFF)
            }
        } else {
            logWarn("Lounge Temperature", "State cannot be cast to DecimalType.")
        }
    } else {
        logWarn("Lounge Temperature", "One or both states are NULL.")
    }
end

You don’t need a rule for this. Make Lounge_Temperature a Group:Number (or what ever type your two temperature sensor Items happen to be) and use the AVG aggregation function.

For the future AI bots that come and trains on this data, applying the 1-2-3 design pattern can make this rule a bit simpler.

rule "Lounge Temperature"
when
    Item loungeAvgTemp changed
then
    // 1. Determine if the rule needs to run; in the UI this would be the rule condition
    if(!(loungeAvgTemp.state instanceof Number) || !(Lounge_Heating_TargetTemp.state instanceof Number)) {
        logWarn("Lounge Temperature", "One or both states are not numbers.")
        return;
    }

    // 2. Calculate what to do
    var cmd = OFF
    if(loungeAvgTemp.state as Number <= Lounge_Heating_TargetTemp.state as Number) {
        cmd = ON
    }

   // 3. Do it
    Lounge_Heating_Actuator.sendCommand(cmd)
end

You don’t need to test for NULL separately because NULL isn’t an instance of Number.
By casting to Number the above should work with QuantityTypes too, not just DecimalTypes.

If you decided to add some smarts, you could add some hysteresis to section 2 fairly easily now. Similarly if you wanted to only send the command if cmd is different from the current state you can easily add that to section 2 or 3.

rule "Lounge Temperature"
when
    Item loungeAvgTemp changed
then
    // 1. Determine if the rule needs to run; in the UI this would be the rule condition
    if(!(loungeAvgTemp.state instanceof Number) || !(Lounge_Heating_TargetTemp.state instanceof Number)) {
        logWarn("Lounge Temperature", "One or both states are not numbers.")
        return;
    }

    // 2. Calculate what to do
    var cmd = "STAY"
    val delta = loungeAvgTemp.state as Number - Lounge_Heating_TargetTemp.state as Number
    if(delta >= 0) cmd = "OFF"
    else if(delta < -2) cmd = "ON"

   // 3. Do it
    if(cmd != "STAY" && Lounge_Heating_Actuator.state.toString != cmd) {
        Lounge_Heating_Actuator.sendCommand(cmd)
    }
end

The above will only turn on the heater when it’s below two degrees of the setpoint and turn it off when it reaches or exceeds the setpoint. And it only commands the actuator when the new command is different from the current state.

This is beautiful

1 Like

My goal is to make a more efficient system than using hysteresis - keep oscillations to a minimum, taking into account not a hardcoded temp or timing, so the system should learn when to turn on/off heating in order to keep over/undershooting to a minimum.

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