# HASmartThermostat: a PID-controlled virtual thermostat for Home Assistant

> HASmartThermostat is a Home Assistant custom integration that turns any switch, valve or pilot-wire heater into a virtual climate entity driven by a PID controller, with pulse width modulation over a configurable period and optional outdoor-temperature compensation. It ships a setup wizard and three worked YAML examples.

**ScratMan/HASmartThermostat** — Smart Thermostat with PID controller for HomeAssistant

- Repository: https://github.com/ScratMan/HASmartThermostat
- Stars: 538 · Forks: 83
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/scratman-hasmartthermostat

## What the integration actually controls

HASmartThermostat is a smart thermostat with a PID controller for Home Assistant, and the README's opening description is that you create a virtual thermostat with accurate and reactive temperature control through a PID controller.

The actuation target is the important part of the scope. Any heater or air conditioner unit with an ON/OFF switch or pilot wire can be controlled using a pulse width modulation that depends on the temperature error and its variation over time.

That is a precise constraint and it defines the class of hardware this works with. A relay-controlled radiator valve, a boiler with a dry contact, a heater wired to a smart switch, an air conditioner unit with a pilot wire. What does not fit is a modulating valve, because the integration's output is a duty cycle rather than a position.

The mechanism is described in the usage section. The target sensor measures the ambient temperature while the heater switch controls an ON/OFF heating system. The PID controller computes the amount of time the heater should remain ON over the PWM period to reach the temperature set point.

The example given is the clearest way to grasp it. With PWM set to 15 minutes, if the output is 100% the heater will be kept on for the next 15 minutes PWM period. If the PID output is 33%, the heater will be switched ON for 5 minutes only.

So the integration is not a bang-bang thermostat that turns the heater fully on until the set point is reached and then fully off. It computes a proportional duty cycle over a window, and the size of that window is the pwm setting. A fifteen minute window is coarse for a room, and the shorter the window the more the behaviour approaches ordinary bang-bang control, while the longer the window the more the system behaves like a slow proportional controller with a noticeable delay between the decision and the heat.

The primary language is Python, the licence is MIT, the default branch is master, and the recent releases are v2026.10.2 from 2026-10-19, v2026.10.2-beta4 from 2026-10-18 and v2026.10.2-beta3 from 2026-10-13, which is a date-based versioning scheme with a beta train ahead of each release.

## The PID terms and how the output is limited

The README spells out the control law, and it is worth reading precisely because the sign conventions matter when you tune it.

The PID output value is the weighted sum of the control terms. The error is target_temp minus current_temperature. The di term is the temperature change between the last two samples, and dt is the time elapsed between the last two samples. The proportional term is P = Kp * error. The integral term is I = last_I + (Ki * error * dt). The derivative term is D = -(Kd * di) / dt. The output is P + I + D.

Output is then limited to the 0% to 100% range to control the PWM.

Two details in those formulas are the ones people get wrong when they port the equations from a textbook. The integral is accumulated rather than recomputed from scratch, which is what makes it an integral, and the derivative is negative because di is the measured temperature change rather than the error change. If the temperature is rising, di is positive, and the derivative term subtracts, which is the intended behaviour.

The gains are described in terms of what to change when the system misbehaves, which is more useful than the textbook description.

Kp gain adjusts the proportional part of the error compensation. Higher values means stronger reaction to error. Increase the value for faster rise time.

Ki gain adjusts the integral part, and it compensates the residual error when temperature settles in a cumulative way. The longer the temperature remains below the set point, the higher the integral compensation will be. If your system settles below the set point, increase the Ki value. If it settles over the set point, decrease the Ki value.

Kd gain adjusts the derivative part of the compensation. Derivative compensates the inertia of the system. If the sensor temperature increases quickly between two samples, the PID will decrease the PWM level accordingly to limit the overshoot.

The output_min and output_max settings, which appear in two of the example configurations, bound the output further. That is what you use to stop a radiator valve from being pulsed for a very short time at a very high rate, since a duty cycle of a few percent over a short PWM window is mechanically hard on a valve and produces visible clicking.

The README also links a set of PID tuning references, including a guide for on/off gas boilers, an Eurotherm explainer, a practical PID dynamics write-up and a PID Tuner tool, and it notes that you can find many tutorials for guidance on the web.

## How often the controller runs, and the sampling period

There is one operational detail in the README that is easy to skim and expensive to ignore.

By default, the PID controller will be called each time the target sensor is updated. When using a mains powered sensor with high sampling rate, the sampling_period parameter should be used to slow down the PID controller refresh rate.

This is a real problem with an obvious mechanism. A cheap temperature sensor that reports every few seconds from a mains-powered transmitter will trigger the controller that often by default, and a PID controller fed samples far more frequently than the plant can respond to will produce a very noisy derivative term, because di over a short interval is dominated by sensor noise rather than by the building's actual temperature change.

The derivative term is the part that suffers. D = -(Kd * di) / dt, so with a tiny dt and a noisy di, the output swings on noise. That produces a heater that cycles on and off for no reason, which is both wasteful and audible, and on a gas boiler it is the kind of short-cycling that wears the appliance.

So the sampling_period parameter is not a performance tuning knob. It is what makes the derivative term meaningful. Setting it to something longer than the building's thermal response time makes the controller see the temperature trend rather than the sensor jitter.

That also explains why the two example configurations that use multiple radiator valves and cascading PID set kd to 500 while the single ON/OFF heater example sets kd to 2000. The examples are not tuned for identical plants: a single relay-controlled heater in one room responds fast to a command, while a hydronic system with three valves has a large thermal mass and a slower response, so a large derivative gain that would be useful for the fast case becomes noise amplification in the slow one.

## Installing through HACS and adding it by hand

The recommended install path is HACS, which is the standard installer for Home Assistant custom integrations, and the README says so directly.

The HACS route is a sequence of clicks. Go to HACS, select Integrations, click the three dots menu and select Custom repositories. Add the path to the Github repository in the first field, select Integration as category and click Add. Once back in the integrations panel, click the Install button on the Smart Thermostat PID card to install. After installation, add the integration from Settings, Devices and Services, Add Integration and search for Smart thermostat.

The manual route is four steps and is what you do if you do not run HACS.

First, go to the configuration directory, which defaults to /homeassistant/.homeassistant/ and is where your configuration.yaml lives. Second, create the custom_components/ directory in it if it does not already exist. Third, copy the smart_thermostat folder into that directory. Fourth, set up the smart thermostat.

The repository layout supports both routes. There is a custom_components/ directory at the root holding the integration itself, a hacs.json file describing the HACS metadata, and a tests/ directory. The tests directory is worth noting because it means the integration's behaviour is covered by automated tests rather than only by manual use.

The manual copy is the same directory the HACS install ends up writing to, so an installation done by hand and one done through HACS end up in the same place. The difference is that HACS handles the download and the updates, and the manual route means you are responsible for knowing when a new version has been released.

After installation, the next step is configuration, and the README presents two ways of doing that.

## The setup wizard versus the YAML configuration

Configuration is possible two ways, and the README marks the UI route as recommended.

Through the UI, you open Settings, Devices and Services, Add Integration, search for Smart thermostat, then follow the setup wizard to select your temperature sensor, heater and cooler actuators, and PID settings. You can adjust settings later via Configure on the integration card.

The manual route is to add a climate section to your configuration.yaml file.

The single ON/OFF heater example is the smallest one and shows the core keys.

```yaml
climate:
  - platform: smart_thermostat
    name: Smart Thermostat Single ON/OFF Heater Example
    unique_id: smart_thermostat_single_on_off_heat_example
    heater: switch.on_off_heater
    target_sensor: sensor.ambient_temperature
    min_temp: 7
    max_temp: 28
    ac_mode: False
    target_temp: 19
    keep_alive:
      seconds: 60
    away_temp: 14
    kp: 50
    ki: 0.01
    kd: 2000
    pwm: 00:15:00
```

The heater key takes a single entity, the target_sensor is the ambient temperature, and the min and max temperature set the range the entity will accept. The ac_mode flag switches the direction of control for cooling. The keep_alive block sets how long the entity stays active, and away_temp is the set point used when the entity is in away state.

The second example handles multiple valves, and the heater key becomes a list.

```yaml
    heater:
      - valve.radiator_valve_1
      - valve.radiator_valve_2
      - valve.radiator_valve_3
```

It also sets output_min: 0 and output_max: 99 instead of a pwm value, and pwm: 0, which means no fixed period.

The third example is the interesting one, because it shows the integration composing with itself. The heater key there is another climate entity, climate.smart_thermostat_single_on_off_heat_example, and the README calls it the cascading PID example.

```yaml
    heater: climate.smart_thermostat_single_on_off_heat_example
```

So one virtual thermostat can drive another, and the outer one uses a different sensor, sensor.ambient_temperature2, and a different output range, output_min: 7 and output_max: 28. The inner thermostat's duty cycle becomes the outer thermostat's input, which is how you build a two-stage cascade where a room controller drives a zone controller.

## Outdoor temperature compensation and its extra gain

There is one optional addition to the control law, and it is the feature that most changes how the thermostat feels in a real house.

Optionally, when an outdoor temperature sensor entity is provided and ke is set, the thermostat can automatically compensate building losses based on the difference between target temperature and outdoor temperature. An external component E is added to the PID output, with E = Ke * (target_temp - outdoor_temp), and the output becomes P + I + D + E, still limited to the 0% to 100% range to control the PWM.

The reasoning behind it is stated: the Ke gain depends on the insulation of the building, and on recent buildings with good insulation a gain of 0.6 is recommended. The compensation will act like the integral of the PID, but with faster response.

That description is accurate and worth unpacking. The integral term compensates a steady error, and a steady error is what a heat loss creates: the indoor temperature settles below the set point by an amount proportional to how cold it is outside, and the integral term eventually cancels it, but only after the system has settled and only as slowly as Ki allows.

The E term attacks the same error directly and proportionally to the outdoor temperature difference, so the compensation starts immediately rather than after the integral has wound up. In a well-insulated house on a mild day, the difference between target and outdoor temperature is small, so E contributes little. In a cold day in a poorly insulated house, E is large from the start, which is exactly when the extra heat is needed.

The 0.6 recommendation for a well-insulated building has an intuitive reading. Good insulation means a smaller temperature difference produces most of the required heat, so the gain that converts degrees of outdoor difference into duty cycle can be modest. In a leaky house the same outdoor difference needs proportionally more heat, so the gain would need to be higher.

The caveat is that this requires an outdoor temperature sensor entity, and the README does not describe how that entity is discovered or what happens when it goes stale. If the outdoor sensor stops updating, the E term holds its last value and the controller keeps compensating for a weather condition that may no longer be true.

## Where HASmartThermostat is the wrong choice

There are three limits, and the first is about the class of hardware.

The first is that the integration expects an ON/OFF actuator. Any heater or air conditioner unit with an ON/OFF switch or pilot wire can be controlled using pulse width modulation, and that is the whole actuation model. If your heating is already driven by a modulating valve, a weather-compensated controller or a thermostat that modulates itself, there is nothing here to add, and forcing PWM onto a system that expects a position command will produce worse control than the original controller.

The second is that the integration is a control loop with a real physical effect, and the tuning has to be done against your own hardware. Kp, Ki and Kd are not portable. The example configurations ship values, kd: 2000 for a single ON/OFF heater and kd: 500 for a multi-valve system, and the difference is a factor of four that reflects two different thermal responses. Copying the example's numbers into your own configuration is starting from a guess.

There is also the derivative noise issue in a second form. The sampling_period parameter exists because a fast-reporting sensor makes the derivative term meaningless, and if you leave it unset with a fast sensor the heater will cycle on noise. That is a symptom you can watch for, a valve clicking on and off, but it is a misconfiguration you have to know to look for.

The third is that the README documents the control law and the configuration keys but says very little about failure handling. What happens when the target sensor becomes unavailable, or reports a value outside the plausible range, is not described, and in a heating system that question matters more than in most integrations. The keep_alive setting, with its 60 seconds in the examples, appears to be the mechanism that keeps the entity from being considered stale, and it is worth understanding what it does before relying on it.

The honest alternative depends on what you actually want. If the goal is proportional control of a relay-driven heater, this is a direct and well-scoped implementation of a PID loop with the modulation done in software, and writing your own PID node in Home Assistant's template engine or Node-RED gives you more control over the terms but none of the UI, the configuration flow or the climate entity. If your heating plant has a modulating controller already, the better investment is usually in the plant rather than in the software, and this integration will not change what the hardware can do. And for a heater that simply needs to be on until the room is warm, a built-in Home Assistant climate entity with a switch and a heater helper is enough, and has no tuning to get wrong.

What HASmartThermostat adds over those options is the specific behaviour of holding a heater on for a computed fraction of a period rather than fully on or fully off, which reduces the temperature swing in a well-insulated room and, with the outdoor compensation term, anticipates heat loss instead of chasing it.

## Conclusion

Adopt HASmartThermostat if your Home Assistant setup drives a heater or air conditioner through a switch or valve and you want proportional control instead of a bang-bang thermostat, since the PWM output holds the heater on for a computed fraction of each period. Do not adopt it if your heating is already controlled by a modulating valve or by a thermostat that does the modulating itself, because the integration expects an ON/OFF actuator. Before you rely on it, tune Kp, Ki and Kd against your own sensor and actuator, and note the last push was on 2026-10-19 with release v2026.10.2 the same day.

## FAQ

### What does HASmartThermostat do in Home Assistant?

It creates a virtual thermostat entity that controls a heater or air conditioner with a PID controller, using pulse width modulation over a configurable period so the actuator is held on for a computed fraction of that period rather than fully on or fully off.

### What kind of heater can HASmartThermostat control?

Any heater or air conditioner unit with an ON/OFF switch or pilot wire, including a relay-controlled radiator valve, a boiler with a dry contact, or a heater on a smart switch. It expects an ON/OFF actuator rather than a modulating one.

### How do I install HASmartThermostat?

The recommended route is HACS: add the repository under Custom repositories with the Integration category, install the Smart Thermostat PID card, then add it from Settings, Devices and Services, Add Integration. Manually, create the custom_components/ directory under your configuration directory, which defaults to /homeassistant/.homeassistant/, and copy the smart_thermostat folder into it.

### What does the sampling_period parameter do in HASmartThermostat?

By default the PID controller is called each time the target sensor updates, so with a mains powered sensor with a high sampling rate the parameter should be used to slow down the PID controller refresh rate. This keeps the derivative term meaningful rather than amplifying sensor noise.

### What is outdoor temperature compensation in HASmartThermostat?

When an outdoor temperature sensor entity is provided and ke is set, an external component E = Ke * (target_temp - outdoor_temp) is added to the PID output. The Ke gain depends on building insulation, with 0.6 recommended for recent well insulated buildings, and the compensation behaves like the integral but with faster response.

## Sources

- [Issues](https://github.com/ScratMan/HASmartThermostat/issues)
- [License: MIT](https://github.com/ScratMan/HASmartThermostat/blob/master/LICENSE)
- [README](https://github.com/ScratMan/HASmartThermostat/blob/master/README.md)
- [Releases](https://github.com/ScratMan/HASmartThermostat/releases)
- [ScratMan/HASmartThermostat on GitHub](https://github.com/ScratMan/HASmartThermostat)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/scratman-hasmartthermostat
