Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cascading PID #203

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ climate:
pwm: 0
```

```
climate:
- platform: smart_thermostat
name: Smart Thermostat Cascading PID Example
unique_id: smart_thermostat_outer_pid_example
heater: climate.smart_thermostat_single_on_off_heat_example
target_sensor: sensor.ambient_temperature2
min_temp: 7
max_temp: 28
ac_mode: False
target_temp: 19
keep_alive:
seconds: 60
away_temp: 14
kp: 5
ki: 0.01
kd: 500
output_min: 7
output_max: 28
pwm: 0
```

## Usage:
The target sensor measures the ambient temperature while the heater switch controls an ON/OFF
heating system.\
Expand Down Expand Up @@ -123,6 +145,15 @@ PID output value is the weighted sum of the control terms:\
`output = P + I + D`\
Output is then limited to 0% to 100% range to control the PWM.

#### Cascading PID
Optionally, 2 (or more) PIDs can be used in a cascading matter, e.g. for underfloor heating define an outer
PID between the room temperature and the floor temperature, and an inner PID between the floor
temperature and the PWM. See details [here](
https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller#Cascade_control).

To enable, create the inner thermostate as detailed above, and then create another thermostate that will
control the inner one.

#### Outdoor temperature compensation
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
Expand Down
28 changes: 24 additions & 4 deletions custom_components/smart_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@

from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
DOMAIN as CLIMATE_DOMAIN,
HVACMode,
HVACAction,
PRESET_AWAY,
Expand All @@ -59,6 +61,8 @@
PRESET_HOME,
PRESET_SLEEP,
PRESET_ACTIVITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
)

from . import DOMAIN, PLATFORMS
Expand Down Expand Up @@ -750,7 +754,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
self.entity_id,
self._control_output,
hvac_mode)
await self._async_set_valve_value(self._control_output)
await self._async_set_valve_value(self._control_output, self._hvac_mode)
# Clear the samples to avoid integrating the off period
self._previous_temp = None
self._previous_temp_time = None
Expand Down Expand Up @@ -910,7 +914,7 @@ async def _async_control_heating(
await self._async_heater_turn_off(force=True)
else:
self._control_output = self._output_min
await self._async_set_valve_value(self._control_output)
await self._async_set_valve_value(self._control_output, self._hvac_mode)
self.async_write_ha_state()
return

Expand Down Expand Up @@ -938,6 +942,8 @@ def _is_device_active(self):
try: # do not throw an error if the state is not yet available on startup
for heater_or_cooler_entity in self.heater_or_cooler_entity:
state = self.hass.states.get(heater_or_cooler_entity).state
if heater_or_cooler_entity[0:8] == 'climate.':
return state != HVACMode.OFF
try:
value = float(state)
if value > 0:
Expand Down Expand Up @@ -1008,7 +1014,7 @@ async def _async_heater_turn_off(self, force=False):
service = SERVICE_TURN_OFF
await self.hass.services.async_call(HA_DOMAIN, service, data)

async def _async_set_valve_value(self, value: float):
async def _async_set_valve_value(self, value: float, hvac_mode: HVACMode):
_LOGGER.info("%s: Change state of %s to %s", self.entity_id,
", ".join([entity for entity in self.heater_or_cooler_entity]), value)
for heater_or_cooler_entity in self.heater_or_cooler_entity:
Expand All @@ -1024,6 +1030,20 @@ async def _async_set_valve_value(self, value: float):
VALVE_DOMAIN,
SERVICE_SET_VALVE_POSITION,
data)
elif heater_or_cooler_entity[0:8] == 'climate.':
state = self.hass.states.get(heater_or_cooler_entity).state
if hvac_mode != state:
data = {ATTR_ENTITY_ID: heater_or_cooler_entity, ATTR_HVAC_MODE: hvac_mode}
await self.hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
data)
if hvac_mode != HVACMode.OFF:
data = {ATTR_ENTITY_ID: heater_or_cooler_entity, ATTR_TEMPERATURE: value, ATTR_HVAC_MODE: hvac_mode}
await self.hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
data)
else:
data = {ATTR_ENTITY_ID: heater_or_cooler_entity, ATTR_VALUE: value}
await self.hass.services.async_call(
Expand Down Expand Up @@ -1136,7 +1156,7 @@ async def set_control_value(self):
self._time_changed = time.time()
await self._async_heater_turn_off()
else:
await self._async_set_valve_value(abs(self._control_output))
await self._async_set_valve_value(abs(self._control_output), self._hvac_mode)

async def pwm_switch(self):
"""turn off and on the heater proportionally to control_value."""
Expand Down