Thermostat with PID controller

I’ll see if I can get it done this Weekend should have the time for it

I managed to get it working last night with the new climate system. Might need a few changes but it loads and doesn’t throw any errors. Will post the code as soon as I get to my laptop.

Not sure about the turn_on and turn_off from the PWM section… Just kinda copied them from the generic thermostat, unsure if its done properly, but it seems to work for my use case. I’m sure someone else can fix any small mistakes I’ve made, but atleast it loads now.

import asyncio
import logging
import time
import custom_components.smart_thermostat.pid_controller as pid_controller

import voluptuous as vol

from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_NAME, EVENT_HOMEASSISTANT_START,
    PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, SERVICE_TURN_OFF,
    SERVICE_TURN_ON, STATE_ON, STATE_UNKNOWN)
from homeassistant.core import DOMAIN as HA_DOMAIN, callback
from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
    async_track_state_change, async_track_time_interval)
from homeassistant.helpers.restore_state import RestoreEntity

from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.components.climate.const import (
    ATTR_PRESET_MODE, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE,
    CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF,
    PRESET_AWAY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE)

_LOGGER = logging.getLogger(__name__)

DEFAULT_TOLERANCE = 0.3
DEFAULT_NAME = 'Smart Thermostat'
#To Do: set default for pt1
DEFAULT_DIFFERENCE = 100
DEFAULT_PWM = 0
DEFAULT_KP = 0
DEFAULT_KI = 0
DEFAULT_KD = 0
DEFAULT_AUTOTUNE = "none"
DEFAULT_NOISEBAND = 0.5

CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration'
CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_HVAC_MODE = 'initial_hvac_mode'
CONF_AWAY_TEMP = 'away_temp'
CONF_PRECISION = 'precision'
CONF_DIFFERENCE = 'difference'
CONF_KP = 'kp'
CONF_KI = 'ki'
CONF_KD = 'kd'
CONF_PWM = 'pwm'
CONF_AUTOTUNE = 'autotune'
CONF_NOISEBAND = 'noiseband'

SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_HEATER): cv.entity_id,
    vol.Required(CONF_SENSOR): cv.entity_id,
    vol.Optional(CONF_AC_MODE): cv.boolean,
    vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
    vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
    vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(
        float),
    vol.Optional(CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(
        float),
    vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
    vol.Required(CONF_KEEP_ALIVE): vol.All(
        cv.time_period, cv.positive_timedelta),
    vol.Optional(CONF_INITIAL_HVAC_MODE):
        vol.In([HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF]),
    vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float),
    vol.Optional(CONF_PRECISION): vol.In(
        [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]),
    vol.Optional(CONF_DIFFERENCE, default=DEFAULT_DIFFERENCE): vol.Coerce(float),
    vol.Optional(CONF_KP, default=DEFAULT_KP): vol.Coerce(float),
    vol.Optional(CONF_KI, default=DEFAULT_KI): vol.Coerce(float),
    vol.Optional(CONF_KD, default=DEFAULT_KD): vol.Coerce(float),
    vol.Optional(CONF_PWM, default=DEFAULT_PWM): vol.Coerce(float),
    vol.Optional(CONF_AUTOTUNE, default=DEFAULT_AUTOTUNE): cv.string,
    vol.Optional(CONF_NOISEBAND, default=DEFAULT_NOISEBAND): vol.Coerce(float)
})


async def async_setup_platform(hass, config, async_add_entities,
                               discovery_info=None):
    """Set up the generic thermostat platform."""
    name = config.get(CONF_NAME)
    heater_entity_id = config.get(CONF_HEATER)
    sensor_entity_id = config.get(CONF_SENSOR)
    min_temp = config.get(CONF_MIN_TEMP)
    max_temp = config.get(CONF_MAX_TEMP)
    target_temp = config.get(CONF_TARGET_TEMP)
    ac_mode = config.get(CONF_AC_MODE)
    min_cycle_duration = config.get(CONF_MIN_DUR)
    cold_tolerance = config.get(CONF_COLD_TOLERANCE)
    hot_tolerance = config.get(CONF_HOT_TOLERANCE)
    keep_alive = config.get(CONF_KEEP_ALIVE)
    initial_hvac_mode = config.get(CONF_INITIAL_HVAC_MODE)
    away_temp = config.get(CONF_AWAY_TEMP)
    precision = config.get(CONF_PRECISION)
    unit = hass.config.units.temperature_unit
    difference = config.get(CONF_DIFFERENCE)
    kp = config.get(CONF_KP)
    ki = config.get(CONF_KI)
    kd = config.get(CONF_KD)
    pwm = config.get(CONF_PWM)
    autotune = config.get(CONF_AUTOTUNE)
    noiseband = config.get(CONF_NOISEBAND)

    async_add_entities([SmartThermostat(
        name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
        target_temp, ac_mode, min_cycle_duration, cold_tolerance,
        hot_tolerance, keep_alive, initial_hvac_mode, away_temp,
        precision, unit, difference, kp, ki, kd, pwm, autotune, noiseband)])


class SmartThermostat(ClimateDevice, RestoreEntity):
    """Representation of a Smart Thermostat device."""

    def __init__(self, name, heater_entity_id, sensor_entity_id,
                 min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
                 cold_tolerance, hot_tolerance, keep_alive,
                 initial_hvac_mode, away_temp, precision, unit,
                 difference, kp, ki, kd, pwm, autotune, noiseband):
        """Initialize the thermostat."""
        self._name = name
        self.heater_entity_id = heater_entity_id
        self.sensor_entity_id = sensor_entity_id
        self.ac_mode = ac_mode
        self.min_cycle_duration = min_cycle_duration
        self._cold_tolerance = cold_tolerance
        self._hot_tolerance = hot_tolerance
        self._keep_alive = keep_alive
        self._hvac_mode = initial_hvac_mode
        self._saved_target_temp = target_temp or away_temp
        self._temp_precision = precision
        if self.ac_mode:
            self._hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
            self.minOut = -difference
            self.maxOut = 0
        else:
            self._hvac_list = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
            self.minOut = 0
            self.maxOut = difference
        self._active = False
        self._cur_temp = None
        self._temp_lock = asyncio.Lock()
        self._min_temp = min_temp
        self._max_temp = max_temp
        self._target_temp = target_temp
        self._unit = unit
        self._support_flags = SUPPORT_FLAGS
        if away_temp:
            self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE
        self._away_temp = away_temp
        self._is_away = False
        self.difference = difference
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.pwm = pwm
        self.autotune = autotune
        self.sensor_entity_id = sensor_entity_id
        self.time_changed = time.time()
        if self.autotune != "none":
            self.pidAutotune = pid_controller.PIDAutotune(self._target_temp, self.difference,
            self._keep_alive.seconds, self._keep_alive.seconds, self.minOut, self.maxOut,
            noiseband, time.time)
            _LOGGER.warning("Autotune will run with the next Setpoint Value you set."
            "changes, submited after doesn't have any effekt until it's finished")
        else:
            self.pidController = pid_controller.PIDArduino(self._keep_alive.seconds,
            self.kp, self.ki, self.kd, self.minOut, self.maxOut, time.time)

    async def async_added_to_hass(self):
        """Run when entity about to be added."""
        await super().async_added_to_hass()

        # Add listener
        async_track_state_change(
            self.hass, self.sensor_entity_id, self._async_sensor_changed)
        async_track_state_change(
            self.hass, self.heater_entity_id, self._async_switch_changed)

        if self._keep_alive:
            async_track_time_interval(
                self.hass, self._async_control_heating, self._keep_alive)

        @callback
        def _async_startup(event):
            """Init on startup."""
            sensor_state = self.hass.states.get(self.sensor_entity_id)
            if sensor_state and sensor_state.state != STATE_UNKNOWN:
                self._async_update_temp(sensor_state)

        self.hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_START, _async_startup)


        # Check If we have an old state
        old_state = await self.async_get_last_state()
        if old_state is not None:
            # If we have no initial temperature, restore
            if self._target_temp is None:
                # If we have a previously saved temperature
                if old_state.attributes.get(ATTR_TEMPERATURE) is None:
                    if self.ac_mode:
                        self._target_temp = self.max_temp
                    else:
                        self._target_temp = self.min_temp
                    _LOGGER.warning("Undefined target temperature,"
                                    "falling back to %s", self._target_temp)
                else:
                    self._target_temp = float(
                        old_state.attributes[ATTR_TEMPERATURE])
            if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY:
                self._is_away = True
            if not self._hvac_mode and old_state.state:
                self._hvac_mode = old_state.state

        else:
            # No previous state, try and restore defaults
            if self._target_temp is None:
                if self.ac_mode:
                    self._target_temp = self.max_temp
                else:
                    self._target_temp = self.min_temp
            _LOGGER.warning("No previously saved temperature, setting to %s",
                            self._target_temp)

        # Set default state to off
        if not self._hvac_mode:
            self._hvac_mode = HVAC_MODE_OFF

    @property
    def should_poll(self):
        """Return the polling state."""
        return False

    @property
    def name(self):
        """Return the name of the thermostat."""
        return self._name

    @property
    def precision(self):
        """Return the precision of the system."""
        if self._temp_precision is not None:
            return self._temp_precision
        return super().precision

    @property
    def temperature_unit(self):
        """Return the unit of measurement."""
        return self._unit

    @property
    def current_temperature(self):
        """Return the sensor temperature."""
        return self._cur_temp

    @property
    def hvac_mode(self):
        """Return current operation."""
        return self._hvac_mode

    @property
    def hvac_action(self):
        """Return the current running hvac operation if supported.
        Need to be one of CURRENT_HVAC_*.
        """
        if self._hvac_mode == HVAC_MODE_OFF:
            return CURRENT_HVAC_OFF
        if not self._is_device_active:
            return CURRENT_HVAC_IDLE
        if self.ac_mode:
            return CURRENT_HVAC_COOL
        return CURRENT_HVAC_HEAT
    @property
    def target_temperature(self):
        """Return the temperature we try to reach."""
        return self._target_temp

    @property
    def hvac_modes(self):
        """List of available operation modes."""
        return self._hvac_list

    @property
    def preset_mode(self):
        """Return the current preset mode, e.g., home, away, temp."""
        if self._is_away:
            return PRESET_AWAY
        return None

    @property
    def preset_modes(self):
        """Return a list of available preset modes."""
        if self._away_temp:
            return [PRESET_AWAY]
        return None

    @property
    def pid_parm(self):
        """Return the pid parameters of the thermostat."""
        return (self.kp, self.ki, self.kd)

    @property
    def pid_control_output(self):
        """Return the pid control output of the thermostat."""
        return self.control_output

    async def async_set_hvac_mode(self, hvac_mode):
        """Set hvac mode."""
        if hvac_mode == HVAC_MODE_HEAT:
            self._hvac_mode = HVAC_MODE_HEAT
            await self._async_control_heating(force=True)
        elif hvac_mode == HVAC_MODE_COOL:
            self._hvac_mode = HVAC_MODE_COOL
            await self._async_control_heating(force=True)
        elif hvac_mode == HVAC_MODE_OFF:
            self._hvac_mode = HVAC_MODE_OFF
            if self._is_device_active:
                await self._async_async_heater_turn_off()
        else:
            _LOGGER.error("Unrecognized hvac mode: %s", hvac_mode)
            return
        # Ensure we update the current operation after changing the mode
        self.schedule_update_ha_state()

    async def async_set_temperature(self, **kwargs):
        """Set new target temperature."""
        temperature = kwargs.get(ATTR_TEMPERATURE)
        if temperature is None:
            return
        self._target_temp = temperature
        await self._async_control_heating(force=True)
        await self.async_update_ha_state()

    @asyncio.coroutine
    def async_set_pid(self, kp, ki, kd):
        """Set PID parameters."""

        self.kp = kp
        self.ki = ki
        self.kd = kd
        self._async_control_heating()
        yield from self.async_update_ha_state()

    @property
    def min_temp(self):
        """Return the minimum temperature."""
        if self._min_temp:
            return self._min_temp

        # get default temp from super class
        return super().min_temp

    @property
    def max_temp(self):
        """Return the maximum temperature."""
        if self._max_temp:
            return self._max_temp

        # Get default temp from super class
        return super().max_temp

    async def _async_sensor_changed(self, entity_id, old_state, new_state):
        """Handle temperature changes."""
        if new_state is None:
            return

        self._async_update_temp(new_state)
        #await self._async_control_heating()
        await self.async_update_ha_state()

    @callback
    def _async_switch_changed(self, entity_id, old_state, new_state):
        """Handle heater switch state changes."""
        if new_state is None:
            return
        self.async_schedule_update_ha_state()

    @callback
    def _async_update_temp(self, state):
        """Update thermostat with latest state from sensor."""
        try:
            self._cur_temp = float(state.state)
        except ValueError as ex:
            _LOGGER.error("Unable to update from sensor: %s", ex)

    async def _async_control_heating(self, time=None, force=False):
        """Run PID controller, optional autotune for faster integration"""
        async with self._temp_lock:
            if not self._active and None not in (self._cur_temp,
                                                 self._target_temp):
                self._active = True
                _LOGGER.info("Obtained current and target temperature. "
                             "Smart thermostat active. %s, %s",
                             self._cur_temp, self._target_temp)

            if not self._active or self._hvac_mode == HVAC_MODE_OFF:
                return

            #if not force and time is None:
            #    # If the `force` argument is True, we
            #    # ignore `min_cycle_duration`.
            #    # If the `time` argument is not none, we were invoked for
            #    # keep-alive purposes, and `min_cycle_duration` is irrelevant.
            #    if self.min_cycle_duration:
            #        if self._is_device_active:
            #            current_state = STATE_ON
            #        else:
            #            current_state = STATE_OFF
            #        long_enough = condition.state(
            #            self.hass, self.heater_entity_id, current_state,
            #            self.min_cycle_duration)
            #        if not long_enough:
            #            return

            # self.calc_output()
            await self.calc_output()

    @property
    def _is_device_active(self):
        """If the toggleable device is currently active."""
        return self.hass.states.is_state(self.heater_entity_id, STATE_ON)

    @property
    def supported_features(self):
        """Return the list of supported features."""
        return self._support_flags

    async def _async_heater_turn_on(self):
        """Turn heater toggleable device on."""
        data = {ATTR_ENTITY_ID: self.heater_entity_id}
        await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data)

    async def _async_heater_turn_off(self):
        """Turn heater toggleable device off."""
        data = {ATTR_ENTITY_ID: self.heater_entity_id}
        await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data)

    async def async_set_preset_mode(self, preset_mode: str):
        """Set new preset mode.
        This method must be run in the event loop and returns a coroutine.
        """
        if preset_mode == PRESET_AWAY and not self._is_away:
            self._is_away = True
            self._saved_target_temp = self._target_temp
            self._target_temp = self._away_temp
            await self._async_control_heating(force=True)
        elif not preset_mode and self._is_away:
            self._is_away = False
            self._target_temp = self._saved_target_temp
            await self._async_control_heating(force=True)


        await self.async_update_ha_state()

    async def calc_output(self):
        """calculate control output and handle autotune"""
        if self.autotune != "none" :
            if self.pidAutotune.run(self._cur_temp):
                params = self.pidAutotune.get_pid_parameters(self.autotune)
                self.kp = params.Kp
                self.ki = params.Ki
                self.kd = params.Kd
                _LOGGER.info("Set Kd, Ki, Kd. "
                             "Smart thermostat now runs on PID Controller. %s,  %s,  %s",
                             self.kp , self.ki, self.kd)
                self.pidController = pid_controller.PIDArduino(self._keep_alive.seconds, self.kp, self.ki,
                self.kd, self.minOut, self.maxOut, time.time)
                self.autotune = "none"
            self.control_output = self.pidAutotune.output
        else:
            self.control_output = self.pidController.calc(self._cur_temp,
            self._target_temp)
        _LOGGER.info("Obtained current control output. %s", self.control_output)
        await self.set_controlvalue();

    async def set_controlvalue(self):
        """Set Outputvalue for heater"""
        if self.pwm:
            if self.control_output == self.difference or self.control_output == -self.difference:
                if not self._is_device_active:
                    _LOGGER.info("Turning on AC %s", self.heater_entity_id)
                    await self._async_heater_turn_on()
                    self.time_changed = time.time()
            elif self.control_output > 0:
                await self.pwm_switch(self.pwm * self.control_output / self.maxOut, self.pwm * (self.maxOut - self.control_output) / self.maxOut, time.time() - self.time_changed)
            elif self.control_output < 0:
                await self.pwm_switch(self.pwm * self.control_output / self.minOut, self.pwm * self.minOut / self.control_outpu, time.time() - self.time_changedt)
            else:
                if self._active:
                    _LOGGER.info("Turning off heater %s", self.heater_entity_id)
                    await self._async_heater_turn_off()
                    self.time_changed = time.time()
        else:
            _LOGGER.info("Change state of heater %s to %s", self.heater_entity_id, self.control_output)
            self.hass.states.async_set(self.heater_entity_id, self.control_output)

    async def pwm_switch(self, time_on, time_off, time_passed):
        """turn off and on the heater proportionally to controlvalue."""
        if self._is_device_active:
            if time_on < time_passed:
                _LOGGER.info("Turning off AC %s", self.heater_entity_id)
                await self._async_heater_turn_off()
                self.time_changed = time.time()
            else:
                _LOGGER.info("Time until %s turns off: %s sec", self.heater_entity_id, time_on - time_passed)
        else:
            if time_off < time_passed:
                _LOGGER.info("Turning on AC %s", self.heater_entity_id)
                await self._async_heater_turn_on()
                self.time_changed = time.time()
            else:
                _LOGGER.info("Time until %s turns on: %s sec", self.heater_entity_id, time_off - time_passed)

great project. I started reading about this. I started using it :smiley: let’s see how it behaves :smiley:

I planed to use this PID Thermostat. Do you think it will include hass soon ?

@fabian.n Nice work. easy to install and setup.

Will it be possible to make a more detailed step by step guide on how to use autotune? I am unable to find the logger.info and also not sure how to make autotune run for the first time and how to disable it when values are secured.

Thanks again.

I tried to use it. I have floor heating and it just doesn’t work for me because I get some division by zero error. I tried fixing it but I don’t know the math behind it… I need to read more about formula and how I can apply it…

@larsbach You have to enable the logging for info in home assistant. Right now default is warning and the pid logging is info

At https://github.com/home-assistant/home-assistant/pull/27833 a new thermostat is proposed. Maybe we can also bring the PWM/PID functions in this new thermostat?

Is there somebody who can give me a little bit more context on how this works? Does it work well with underfloor heating? Can somebody give a review on how it compares to the original generic_thermostat?

Thanks in advance,
Ronald

I’m working on making this easier to use for everyone. Basically the normal Thermostat uses a very simple “if colder then 20°C, heat” A PID controller uses 3 different methodes Potential, Integral and Differential. Basically if set up correctly it predicts the Temperatur change over time and controls the heating accordingly. This eliminats the overshoot that happens if you heat until it e.g. reaches 22°C. Therefor it can safe you a lot of energy. Especially floor heating has a very large overshoot because of the mass.
However, PID controller are not “plug and play”, set up wrong they don’t work.
Small note, floor heating can explode if you set the temperatur too high.

1 Like

I really like the idea but in my head it misses support for other states. Outdoor temperature, illuminance, weather, sun azimuth and angle are all factors that have a great role in my house.

I already think for a long time, my thermostat should use deep reinforcement learning but I don’t have a clue on how to start :slight_smile:.

Exactly this it what takes me so long to programm. Because I want to include all that in a new version instead of fixing the old one. My expirence in building automation has shown me it’s acutally very easy to do. However, I’m not a big programmer so I have a hard time including this in the UI to make it accessable for others.
If someone else wants to work with me that has that expirence, I gladly do so.

Then we should perhaps team up!

I’ve got experience in the Lovelace frontend and know my way around some custom components.

I was missing that info as well so I’ll share my progress here…

I’ve been using it now for the last two weeks with floor heating and it keeps my temperature quite stable within limits.

My situation:
The house is a recently build (2017-18) 140m2 house with district heating (stadsverwarming) so I don’t have to control the heating system itself, I just have to open and close the main valve.

My config:

  • I have to say that I modified the climate.py to fix some typos and to match with the ‘generic-thermostat’
  • modified PID controller my wishes :wink: (added moving average D part (velocity) and changed some on the limits for the I part) … so my settings should be used I think with my code only.
  • PWM and PID settings tuned to roughly mimic honeywell behaviour
  • Visiualised in Lovelace by ‘simple-thermostat’

modified climate and PID settings

  - platform: smart_thermostat
    name: woonkamer
    heater: switch.hoofdklep
    target_sensor: sensor.temp_wk
    min_temp: 15
    max_temp: 23
    ac_mode: False
    target_temp: 18
    
    keep_alive: #time before update of PID controller
      seconds: 60
    away_temp: 16
    kp : 50
    ki : 0.005 #0.01
    kd : 150000 #0
    
    #duration of cycle
    pwm : 600
    # autotune : ziegler-nichols
    difference : 100
    noiseband : 0.5

However some generic settings for the default PID and floor heating could be. I think I used something like this at the beginning (use on your own risk!)

  - platform: smart_thermostat
    name: woonkamer
    heater: switch.hoofdklep
    target_sensor: sensor.temp_wk
    min_temp: 15
    max_temp: 23
    ac_mode: False
    target_temp: 18
    
    keep_alive: #time before update of PID controller
      seconds: 60
    away_temp: 16
    kp : 50
    ki : 0.01
    kd : 0
    
    #duration of cycle
    pwm : 600
    # autotune : ziegler-nichols
    difference : 100
    noiseband : 0.5

the critical note :slight_smile:
I still use it as a test. If I understand it correctly no or limited safety routines are included in the ‘generic thermostat’ for failing sensors etc. thus also not in this code! So for testing and not long periods away from home I’m using it but I’m not convinced I want to use it as standard. It has to be more robust. There is some development on a new code ’ virtual_thermostat’ which ,if I remember correctly, should be more robust, but I’m a too bad programmer to check-validate it.

and as last a graph of the temperature of before and after.

  • yellow is the PWM signal
  • green main floor temp
  • HA controlled when yellow line is present.
  • before yellow line Honeywell chronotherm

1 Like

Wauw, I like your way of doing stuff - especially the before and after graph… this is a data scientist at work.

My situation is similar: it’s a house from 1840 which we completely renovated and isolated. We have large windows on the south side and these interfere a lot with the generic thermostat.

As for heating, I have a Itho Daalderop Central Heater (instead of your stadsverwarming) and for each room, I can control the valves of the underfloor heating. All is controlled by HomeAssistant.

  • If any room is requesting heating, the valves of that room open and central heating is turned on.
  • If no room is requesting heating, the central heating is turned off.

The valves itself are thermo-electric (e.g. candlewax valves: https://en.wikipedia.org/wiki/Wax_motor). The response times of the valves is about 2-3 minutes to open and 10 minutes to close.

So, my gut feeling says:

  • The PWM won’t work as response time of the wax valves is too slow / I have fear the wax motors will die faster if I use PWM
  • The influence of the sun/weather is large due to the windows on the south side and should be taken into account

And to be honest: I don’t know anything about a PID thermostat :smirk:. I should put it on my todo-learning list soon but my gut feeling says it won’t work due to the external influences. Or am I wrong?

Thanks for your input, I’ll think of making it robust.
This shows me, I need to include safety, didn’t really think of that. Your graph doesn’t really look good. It should definitely be more stable. However it depends on your case and what happend.
If it’s ok for both of you. I’ll make a flowchart on everything with an regular floor heating as an example. And upload it here to get some feedback if it’s what you would want and if there’s anything missing?

1 Like

We have large windows on the south side and these interfere a lot with the generic thermostat.

PID will probably improve it a lot but a limited effects will still be there.

The influence of the sun/weather is large due to the windows on the south side and should be taken into account

Here the same. Most optimal would be weather dependant regulation in combination with room temperature but I expect it will be more difficult to set parameters. With weather controlled heating you need to know the relation between heat loss and required heating.

The PWM won’t work as response time of the wax valves is too slow / I have fear the wax motors will die faster if I use PWM

The generic thermostat uses on-off. So it already a kind of PWM but less accurate: it opens and closes for certain periods depending the room temp limits. Do you log the frequency it is opening and closing right now?

When using slow actuators the code might needs an update instead of just longer PWM cycle time or other kp, ki, kd settings… I think… (but better ask @fabian.n, he probably knows this a lot better as I’m more of trail and error). You might have to add a time addition to the PWM to include the opening and closing time. As a short PWM heating moment will have a relative long open-close interval and at a long PWM heating moment the open-closing will have a relative limited influence. You might need a min cycle period as well.

1 Like

Thanks for your input

Your welcome. It would be great to have a good working thermostat with PID control.

I’ll think of making it robust.
This shows me, I need to include safety, didn’t really think of that.

Yes safety and robustness is very very important (at least for me) for such an important house function. Did you already have a look on the virtual_themostat, might this one a better basis? It seems that a lot of the code is the same as the generic_thermostat. virtual thermostat

Your graph doesn’t really look good. It should definitely be more stable. However it depends on your case and what happend.

The temperture variations are not too bad for me. The part before the PWM signal is controlled by a honeywell chronotherm, which is it believe, a quite reasonable thermostat. The 2-3 extreme peaks are created by not normal conditions (more people or door long time open).
The heating has some trouble catching up with the changes due to the quick heating by the sun and on the other side the slow low temp heating by the floor. To solve this a ‘differenent’ or more complex controller is required with info of the weather, but you are working on that one I saw :wink:

But please let me know if you have any tips to improve the behaviour and to achieve a more stable heating! I will test it!

Just started using this superb PID Thermostat, its fascinating. I am using standard Gas Central Heating system, on-off boiler control relay. Have been playing with the PID Autotune, not sure its doing exactly what I thought it should be doing, so have switched it off and starting to play with PID values now. Its a fascinating and absorbing subject, will post results here when I get some, thanks for the great work, REALLY wish I could help with Python design stuff, but only a begginner, however, VERY happy to provide Testing and Feedback… Keep coding! :slight_smile:

1 Like