Window ByPass (#143)

* Add Window ByPass
This commit is contained in:
adi90x
2023-10-30 07:47:51 +01:00
committed by GitHub
parent 88760dbec9
commit 2786a6e5ae
8 changed files with 269 additions and 6 deletions

View File

@@ -532,7 +532,7 @@ Ce service permet de forcer l'état de présence indépendamment du capteur de p
Le code pour appeler ce service est le suivant : Le code pour appeler ce service est le suivant :
``` ```
service : thermostat_polyvalent.set_presence service : versatile_thermostat.set_presence
Les données: Les données:
présence : "off" présence : "off"
cible: cible:
@@ -547,7 +547,7 @@ Vous pouvez modifier l'une ou les deux températures (lorsqu'elles sont présent
Utilisez le code suivant pour régler la température du préréglage : Utilisez le code suivant pour régler la température du préréglage :
``` ```
service : thermostat_polyvalent.set_preset_temperature service : versatile_thermostat.set_preset_temperature
date: date:
preset : boost preset : boost
temperature : 17,8 temperature : 17,8
@@ -576,8 +576,8 @@ Si le thermostat est en mode ``security`` les nouveaux paramètres sont appliqu
Pour changer les paramètres de sécurité utilisez le code suivant : Pour changer les paramètres de sécurité utilisez le code suivant :
``` ```
service : thermostat_polyvalent.set_security service : versatile_thermostat.set_security
date: data:
min_on_percent: "0.5" min_on_percent: "0.5"
default_on_percent: "0.1" default_on_percent: "0.1"
delay_min: 60 delay_min: 60
@@ -585,6 +585,19 @@ target:
entity_id : climate.my_thermostat entity_id : climate.my_thermostat
``` ```
## ByPass Window Check
Ce service permet d'activer ou non un bypass de la vérification des fenetres.
Il permet de continuer à chauffer même si la fenetre est detectée ouverte.
Mis à ``true`` les modifications de status de la fenetre n'auront plus d'effet sur le thermostat, remis à ``false`` cela s'assurera de désactiver le thermostat si la fenetre est toujours ouverte.
Pour changer le paramètre de bypass utilisez le code suivant :
```
service : versatile_thermostat.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
# Notifications # Notifications
Les évènements marquant du thermostat sont notifiés par l'intermédiaire du bus de message. Les évènements marquant du thermostat sont notifiés par l'intermédiaire du bus de message.
Les évènements notifiés sont les suivants: Les évènements notifiés sont les suivants:

View File

@@ -564,13 +564,24 @@ If the thermostat is in ``security`` mode the new settings are applied immediate
To change the security settings use the following code: To change the security settings use the following code:
``` ```
service : thermostat_polyvalent.set_security service : thermostat_polyvalent.set_security
date: data:
min_on_percent: "0.5" min_on_percent: "0.5"
default_on_percent: "0.1" default_on_percent: "0.1"
delay_min: 60 delay_min: 60
target: target:
entity_id : climate.my_thermostat entity_id : climate.my_thermostat
``` ```
## ByPass Window Check
This service is used to bypass the window check implemented to stop thermostat when an open window is detected.
When set to ``true`` window event won't have any effect on the thermostat, when set back to ``false`` it will make sure to disable the thermostat if window is still open.
To change the bypass setting use the following code:
```
service : thermostat_polyvalent.set_window_bypass
data:
window_bypass: true
target:
entity_id : climate.my_thermostat
# Notifications # Notifications
Significant thermostat events are notified via the message bus. Significant thermostat events are notified via the message bus.

View File

@@ -130,6 +130,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_motion_state: bool _motion_state: bool
_presence_state: bool _presence_state: bool
_window_auto_state: bool _window_auto_state: bool
#PR - Adding Window ByPass
_window_bypass_state: bool
_underlyings: list[UnderlyingEntity] _underlyings: list[UnderlyingEntity]
_last_change_time: datetime _last_change_time: datetime
@@ -229,6 +231,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._window_auto_state = False self._window_auto_state = False
self._window_auto_on = False self._window_auto_on = False
self._window_auto_algo = None self._window_auto_algo = None
# PR - Adding Window ByPass
self._window_bypass_state = False
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone) self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
@@ -961,6 +965,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Get the window_auto_state""" """Get the window_auto_state"""
return STATE_ON if self._window_auto_state else STATE_OFF return STATE_ON if self._window_auto_state else STATE_OFF
#PR - Adding Window ByPass
@property
def window_bypass_state(self) -> bool | None:
"""Get the Window Bypass"""
return self._window_bypass_state
@property @property
def security_state(self) -> bool | None: def security_state(self) -> bool | None:
"""Get the security_state""" """Get the security_state"""
@@ -1308,7 +1318,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("%s - no change in window state. Forget the event") _LOGGER.debug("%s - no change in window state. Forget the event")
return return
self._window_state = new_state.state self._window_state = new_state.state
#PR - Adding Window ByPass
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
if self._window_bypass_state:
_LOGGER.info("Window ByPass is activated. Ignore window event")
self.update_custom_attributes()
return
if self._window_state == STATE_OFF: if self._window_state == STATE_OFF:
_LOGGER.info( _LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'", "%s - Window is closed. Restoring hvac_mode '%s'",
@@ -2107,6 +2126,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"overpowering_state": self._overpowering_state, "overpowering_state": self._overpowering_state,
"presence_state": self._presence_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, "security_delay_min": self._security_delay_min,
"security_min_on_percent": self._security_min_on_percent, "security_min_on_percent": self._security_min_on_percent,
"security_default_on_percent": self._security_default_on_percent, "security_default_on_percent": self._security_default_on_percent,
@@ -2224,6 +2245,26 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating() await self.async_control_heating()
self.update_custom_attributes() self.update_custom_attributes()
#PR - Adding Window ByPass
async def service_set_window_bypass_state(self, window_bypass):
"""Called by a service call:
service: versatile_thermostat.set_window_bypass
data:
window_bypass: True
target:
entity_id: climate.thermostat_1
"""
_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:
_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:
_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()
def send_event(self, event_type: EventType, data: dict): def send_event(self, event_type: EventType, data: dict):
"""Send an event""" """Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data) _LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)

View File

@@ -38,7 +38,7 @@ async def async_setup_entry(
unique_id = entry.entry_id unique_id = entry.entry_id
name = entry.data.get(CONF_NAME) name = entry.data.get(CONF_NAME)
entities = [SecurityBinarySensor(hass, unique_id, name, entry.data)] entities = [SecurityBinarySensor(hass, unique_id, name, entry.data),WindowByPassBinarySensor(hass, unique_id, name, entry.data)]
if entry.data.get(CONF_USE_MOTION_FEATURE): if entry.data.get(CONF_USE_MOTION_FEATURE):
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data)) entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_WINDOW_FEATURE): if entry.data.get(CONF_USE_WINDOW_FEATURE):
@@ -238,3 +238,38 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
return "mdi:home-account" return "mdi:home-account"
else: else:
return "mdi:nature-people" return "mdi:nature-people"
#PR - Adding Window ByPass
class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the Window ByPass state"""
def __init__(
self, hass: HomeAssistant, unique_id, name, entry_infos
) -> None: # pylint: disable=unused-argument
"""Initialize the WindowByPass Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window bypass"
self._attr_unique_id = f"{self._device_name}_window_bypass_state"
self._attr_is_on = False
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
if self.my_climate.window_bypass_state in [True, False]:
self._attr_is_on = self.my_climate.window_bypass_state
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def device_class(self) -> BinarySensorDeviceClass | None:
return BinarySensorDeviceClass.RUNNING
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:window-shutter-cog"
else:
return "mdi:window-shutter-auto"

View File

@@ -24,6 +24,8 @@ from .const import (
SERVICE_SET_PRESENCE, SERVICE_SET_PRESENCE,
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
SERVICE_SET_SECURITY, SERVICE_SET_SECURITY,
#PR - Adding Window ByPass
SERVICE_SET_WINDOW_BYPASS,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
@@ -98,3 +100,13 @@ async def async_setup_entry(
}, },
"service_set_security", "service_set_security",
) )
#PR - Adding Window ByPass
platform.async_register_entity_service(
SERVICE_SET_WINDOW_BYPASS,
{
vol.Required("window_bypass"): vol.In([True, False]
),
},
"service_set_window_bypass_state",
)

View File

@@ -202,6 +202,8 @@ SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security" SERVICE_SET_SECURITY = "set_security"
#PR - Adding Window ByPass
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5 DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1 DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1

View File

@@ -122,3 +122,19 @@ set_security:
step: 0.05 step: 0.05
unit_of_measurement: "%" unit_of_measurement: "%"
mode: slider mode: slider
set_window_bypass:
name: Set Window ByPass
description: Bypass the window state to enable heating with window open.
target:
entity:
integration: versatile_thermostat
fields:
window_bypass:
name: Window ByPass
description: ByPass value
required: true
advanced: false
default: true
selector:
boolean:

View File

@@ -671,3 +671,136 @@ async def test_window_auto_no_on_percent(
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()
#PR - Adding Window Bypass
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management when bypass enabled"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
assert entity.window_state is None
# change temperature to force turning on the heater
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
# Heater shoud turn-on
assert mock_heater_on.call_count >= 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
#Set Window ByPass to true
entity._window_bypass_state = True
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_window_change_event(entity, True, False, datetime.now())
assert mock_send_event.call_count == 0
# Heater should not be on
assert mock_heater_on.call_count == 0
# One call in set_hvac_mode turn_off and one call in the control_heating for security
assert mock_heater_off.call_count == 0
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.HEAT
assert entity.window_state == STATE_ON
# Close the window
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_function = await send_window_change_event(
entity, False, True, datetime.now(), sleep=False
)
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
assert entity.window_state == STATE_OFF
assert mock_heater_on.call_count == 0
assert mock_send_event.call_count == 0
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()