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

Window ByPass #143

Merged
merged 11 commits into from
Oct 30, 2023
Merged
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
21 changes: 17 additions & 4 deletions README-fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
```
service : thermostat_polyvalent.set_presence
service : versatile_thermostat.set_presence
Les données:
présence : "off"
cible:
Expand All @@ -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 :
```
service : thermostat_polyvalent.set_preset_temperature
service : versatile_thermostat.set_preset_temperature
date:
preset : boost
temperature : 17,8
Expand Down Expand Up @@ -576,15 +576,28 @@ 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 :
```
service : thermostat_polyvalent.set_security
date:
service : versatile_thermostat.set_security
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
target:
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
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:
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
service : thermostat_polyvalent.set_security
adi90x marked this conversation as resolved.
Show resolved Hide resolved
date:
data:
min_on_percent: "0.5"
default_on_percent: "0.1"
delay_min: 60
target:
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
Significant thermostat events are notified via the message bus.
Expand Down
41 changes: 41 additions & 0 deletions custom_components/versatile_thermostat/base_thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_motion_state: bool
_presence_state: bool
_window_auto_state: bool
#PR - Adding Window ByPass
_window_bypass_state: bool
_underlyings: list[UnderlyingEntity]
_last_change_time: datetime

Expand Down Expand Up @@ -229,6 +231,8 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
self._window_auto_state = False
self._window_auto_on = False
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)

Expand Down Expand Up @@ -961,6 +965,12 @@ def window_auto_state(self) -> bool | None:
"""Get the window_auto_state"""
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
def security_state(self) -> bool | None:
"""Get the security_state"""
Expand Down Expand Up @@ -1308,7 +1318,16 @@ async def try_window_condition(_):
_LOGGER.debug("%s - no change in window state. Forget the event")
return


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()
adi90x marked this conversation as resolved.
Show resolved Hide resolved
return

if self._window_state == STATE_OFF:
_LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'",
Expand Down Expand Up @@ -2107,6 +2126,8 @@ def update_custom_attributes(self):
"overpowering_state": self._overpowering_state,
"presence_state": self._presence_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_min_on_percent": self._security_min_on_percent,
"security_default_on_percent": self._security_default_on_percent,
Expand Down Expand Up @@ -2224,6 +2245,26 @@ async def service_set_security(self, delay_min, min_on_percent, default_on_perce
await self.async_control_heating()
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
adi90x marked this conversation as resolved.
Show resolved Hide resolved
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()
adi90x marked this conversation as resolved.
Show resolved Hide resolved

def send_event(self, event_type: EventType, data: dict):
"""Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
Expand Down
37 changes: 36 additions & 1 deletion custom_components/versatile_thermostat/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def async_setup_entry(
unique_id = entry.entry_id
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):
entities.append(MotionBinarySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_USE_WINDOW_FEATURE):
Expand Down Expand Up @@ -238,3 +238,38 @@ def icon(self) -> str | None:
return "mdi:home-account"
else:
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"
12 changes: 12 additions & 0 deletions custom_components/versatile_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
SERVICE_SET_PRESENCE,
SERVICE_SET_PRESET_TEMPERATURE,
SERVICE_SET_SECURITY,
#PR - Adding Window ByPass
SERVICE_SET_WINDOW_BYPASS,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
Expand Down Expand Up @@ -98,3 +100,13 @@ async def async_setup_entry(
},
"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",
)
2 changes: 2 additions & 0 deletions custom_components/versatile_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
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_DEFAULT_ON_PERCENT = 0.1
Expand Down
16 changes: 16 additions & 0 deletions custom_components/versatile_thermostat/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,19 @@ set_security:
step: 0.05
unit_of_measurement: "%"
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:
133 changes: 133 additions & 0 deletions tests/test_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,136 @@ async def test_window_auto_no_on_percent(

# Clean the entity
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()
Loading