Skip to content

Commit

Permalink
Issue #168 - Adds slow auto-new_slow_regulation (#170)
Browse files Browse the repository at this point in the history
Issue #169 - Adds support for Versatile Thermostat UI Card

Co-authored-by: Jean-Marc Collin <[email protected]>
  • Loading branch information
jmcollin78 and Jean-Marc Collin authored Nov 6, 2023
1 parent 69a0572 commit 0c8d80f
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ switch:

frontend:
extra_module_url:
- /config/www/community/better-thermostat-ui-card/better-thermostat-ui-card.js
- /config/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
Expand Down
6 changes: 4 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"postCreateCommand": "./container dev-setup",

"mounts": [
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],

"customizations": {
Expand All @@ -22,7 +24,7 @@
"ms-python.vscode-pylance"
],
// "mounts": [
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
// ],
"settings": {
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,5 @@ dist
custom_components/__init__.py
__pycache__

config/**
config/**
custom_components/hacs
30 changes: 15 additions & 15 deletions custom_components/versatile_thermostat/base_thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ async def _async_startup_internal(*_):
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._window_state = window_state.state
self._window_state = (window_state.state == STATE_ON)
_LOGGER.debug(
"%s - Window state have been retrieved: %s",
self,
Expand Down Expand Up @@ -954,12 +954,12 @@ def overpowering_state(self) -> bool | None:
return self._overpowering_state

@property
def window_state(self) -> bool | None:
def window_state(self) -> str | None:
"""Get the window_state"""
return self._window_state
return STATE_ON if self._window_state else STATE_OFF

@property
def window_auto_state(self) -> bool | None:
def window_auto_state(self) -> str | None:
"""Get the window_auto_state"""
return STATE_ON if self._window_auto_state else STATE_OFF

Expand Down Expand Up @@ -1307,33 +1307,33 @@ async def try_window_condition(_):
_LOGGER.debug(
"Window delay condition is not satisfied. Ignore window event"
)
self._window_state = old_state.state
self._window_state = (old_state.state == STATE_ON)
return

_LOGGER.debug("%s - Window delay condition is satisfied", self)
# if not self._saved_hvac_mode:
# self._saved_hvac_mode = self._hvac_mode

if self._window_state == new_state.state:
if self._window_state == (new_state.state == STATE_ON):
_LOGGER.debug("%s - no change in window state. Forget the event")
return


self._window_state = new_state.state
self._window_state = (new_state.state == STATE_ON)

#PR - Adding Window ByPass
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
if self._window_bypass_state:
_LOGGER.info("%s - Window ByPass is activated. Ignore window event", self)
else:
if self._window_state == STATE_OFF:
if not self._window_state:
_LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'",
self,
self._saved_hvac_mode,
)
await self.restore_hvac_mode(True)
elif self._window_state == STATE_ON:
elif self._window_state:
_LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
)
Expand Down Expand Up @@ -1827,7 +1827,7 @@ async def check_overpowering(self) -> bool:
self._device_power,
)

ret = self._current_power + self._device_power >= self._current_power_max
ret = (self._current_power + self._device_power) >= self._current_power_max
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
_LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'",
Expand Down Expand Up @@ -2124,11 +2124,11 @@ def update_custom_attributes(self):
"saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode,
"window_state": self._window_state,
"window_state": self.window_state,
"motion_state": self._motion_state,
"overpowering_state": self._overpowering_state,
"overpowering_state": self.overpowering_state,
"presence_state": self._presence_state,
"window_auto_state": self._window_auto_state,
"window_auto_state": self.window_auto_state,
#PR - Adding Window ByPass
"window_bypass_state": self._window_bypass_state,
"security_delay_min": self._security_delay_min,
Expand Down Expand Up @@ -2258,11 +2258,11 @@ async def service_set_window_bypass_state(self, window_bypass):
"""
_LOGGER.info("%s - Calling service_set_window_bypass, window_bypass: %s", self, window_bypass)
self._window_bypass_state = window_bypass
if not self._window_bypass_state and self._window_state == STATE_ON:
if not self._window_bypass_state and self._window_state:
_LOGGER.info("%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'", self, HVACMode.OFF)
self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF)
if self._window_bypass_state and self._window_state == STATE_ON:
if self._window_bypass_state and self._window_state:
_LOGGER.info("%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode", self)
await self.restore_hvac_mode(True)
self.update_custom_attributes()
Expand Down
2 changes: 1 addition & 1 deletion custom_components/versatile_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def async_setup_entry(
platform.async_register_entity_service(
SERVICE_SET_AUTO_REGULATION_MODE,
{
vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong"]),
vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong", "Slow"]),
},
"service_set_auto_regulation_mode",
)
13 changes: 12 additions & 1 deletion custom_components/versatile_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
CONF_AUTO_REGULATION_SLOW= "auto_regulation_slow"
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
Expand Down Expand Up @@ -207,7 +208,7 @@
PROPORTIONAL_FUNCTION_TPI,
]

CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG]
CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_SLOW]

CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]

Expand All @@ -225,6 +226,16 @@
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"

# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow:
""" Light parameters for slow latency regulation"""
kp:float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki:float = 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext:float = 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max:float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold:float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold:float = 2.0 * 288 # this allows up to 2°C long term offset in both directions

class RegulationParamLight:
""" Light parameters for regulation"""
kp:float = 0.2
Expand Down
3 changes: 2 additions & 1 deletion custom_components/versatile_thermostat/pi_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def calculate_regulated_temperature(self, internal_temp: float, external_temp:fl
offset = self.kp * error + self.ki * self.accumulated_error

# Calculate the exterior offset
offset_ext = self.k_ext * (self.target_temp - external_temp)
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp
offset_ext = self.k_ext * (internal_temp - external_temp)

# Capping of offset_ext
total_offset = offset + offset_ext
Expand Down
1 change: 1 addition & 0 deletions custom_components/versatile_thermostat/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,4 @@ set_auto_regulation_mode:
- "Light"
- "Medium"
- "Strong"
- "Slow"
13 changes: 13 additions & 0 deletions custom_components/versatile_thermostat/thermostat_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
CONF_CLIMATE_4,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
RegulationParamSlow,
RegulationParamLight,
RegulationParamMedium,
RegulationParamStrong
Expand Down Expand Up @@ -176,6 +178,15 @@ def choose_auto_regulation_mode(self, auto_regulation_mode):
RegulationParamStrong.offset_max,
RegulationParamStrong.stabilization_threshold,
RegulationParamStrong.accumulated_error_threshold)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamSlow.kp,
RegulationParamSlow.ki,
RegulationParamSlow.k_ext,
RegulationParamSlow.offset_max,
RegulationParamSlow.stabilization_threshold,
RegulationParamSlow.accumulated_error_threshold)
else:
# A default empty algo (which does nothing)
self._regulation_algo = PITemperatureRegulator(
Expand Down Expand Up @@ -666,6 +677,8 @@ async def service_set_auto_regulation_mode(self, auto_regulation_mode):
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM)
elif auto_regulation_mode == "Strong":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG)
elif auto_regulation_mode == "Slow":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)

await self._send_regulated_temperature()
self.update_custom_attributes()
8 changes: 4 additions & 4 deletions tests/test_auto_regulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def find_my_entity(entity_id) -> ClimateEntity:
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
):
await send_temperature_change_event(entity, 22, event_timestamp)
await send_temperature_change_event(entity, 23, event_timestamp)
await send_ext_temperature_change_event(entity, 19, event_timestamp)

# the regulated temperature should be under
Expand Down Expand Up @@ -212,14 +212,14 @@ def find_my_entity(entity_id) -> ClimateEntity:

# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 25-2.5 # +2.3 without round_to_nearest
assert entity.regulated_target_temp == 25-2 # +2.3 without round_to_nearest

# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
):
await send_temperature_change_event(entity, 20, event_timestamp)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 25, event_timestamp)

# the regulated temperature should be greater
Expand Down Expand Up @@ -331,4 +331,4 @@ def find_my_entity(entity_id) -> ClimateEntity:
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 17 + 1 # 0.7 without round_to_nearest
assert entity.regulated_target_temp == 17 + 1.5 # 0.7 without round_to_nearest
2 changes: 1 addition & 1 deletion tests/test_binary_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ async def test_window_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.window_state is None
assert entity.window_state is STATE_OFF

await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state is STATE_OFF
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ async def test_bug_66(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

# Open the window and let the thermostat shut down
with patch(
Expand Down
10 changes: 5 additions & 5 deletions tests/test_multiple_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def test_one_switch_cycle(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
Expand Down Expand Up @@ -282,7 +282,7 @@ async def test_multiple_switchs(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
Expand Down Expand Up @@ -418,7 +418,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
Expand All @@ -443,7 +443,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
Expand Down Expand Up @@ -518,7 +518,7 @@ async def test_multiple_climates_underlying_changes(
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
assert entity.window_state is STATE_OFF

event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
Expand Down
Loading

0 comments on commit 0c8d80f

Please sign in to comment.