Skip to content

Commit

Permalink
Feature 128 activity preset (#132)
Browse files Browse the repository at this point in the history
* Issue #128 - add motion_off_delay

* Fix movement detection

---------

Co-authored-by: Jean-Marc Collin <[email protected]>
  • Loading branch information
jmcollin78 and Jean-Marc Collin authored Oct 21, 2023
1 parent 7e4e407 commit 043fd5f
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 94 deletions.
19 changes: 12 additions & 7 deletions README-fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une


> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour la gestion de l'activité.
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
Expand Down Expand Up @@ -242,17 +243,20 @@ Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliqu
Nous allons maintenant voir comment configurer le nouveau mode Activité.
Ce dont nous avons besoin:
- un **capteur de mouvement**. ID d'entité d'un capteur de mouvement. Les états du capteur de mouvement doivent être « on » (mouvement détecté) ou « off » (aucun mouvement détecté)
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement
- une durée de **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation du mouvement avant de considérer le mouvement. Ce paramètre peut être supérieur à la temporision de votre détecteur de mouvement, sinon la détection se fera à chaque mouvement signalé par le détecteur,
- une durée de fin **délai de mouvement** (en secondes) définissant combien de temps nous attendons la confirmation d'une fin de mouvement avant de ne plus considérer le mouvement.
- un **préréglage de "mouvement" **. Nous utiliserons la température de ce préréglage lorsqu'une activité sera détectée.
- un **préréglage "pas de mouvement"**. Nous utiliserons la température de ce deuxième préréglage lorsqu'aucune activité n'est détectée.

Alors imaginons que nous voulions avoir le comportement suivant :
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21.5C), le mode "pas de mouvement" choisi est Eco (18.5C) et la temporisation du mouvement est de 5 min.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5 C
- quelqu'un entre dans la pièce, une activité est détectée la température est fixée à 21,5 C
- la personne quitte la chambre, au bout de 5 min la température est ramenée à 18,5 C
- nous avons une pièce avec un thermostat réglé en mode activité, le mode "mouvement" choisi est confort (21,5°C), le mode "pas de mouvement" choisi est Eco (18.5°C) et la temporisation du mouvement est de 30 sec lors de la détection et de 5 minutes sur fin de détection.
- la pièce est vide depuis un moment (aucune activité détectée), la température de cette pièce est de 18,5°
- quelqu'un entre dans la pièce, une activité est détectée si le mouvement est présent pendant au moins 30 sec. La température passe alors à 21,5°
- si le mouvement est présent pendant moins de 30 sec (passage rapide), la température reste sur 18,5°,
- imaginons que la température soit passée sur 21,5°, lorsque la personne quitte la pièce, au bout de 5 min la température est ramenée à 18,5°.
- si la personne revient avant les 5 minutes, la température reste sur 21,5°

Pour que cela fonctionne, le thermostat climatique doit être en mode préréglé « Activité ».
Pour que cela fonctionne, le thermostat doit être en mode préréglé « Activité ».

> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
Expand Down Expand Up @@ -354,7 +358,8 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de détection automatique (en °/min) | X | X |
| ``window_auto_max_duration`` | Durée maximum d'une extinction automatique (en min) | X | X |
| ``motion_sensor_entity_id`` | Détecteur de mouvement entity id | X | X |
| ``motion_delay`` | Délai avant changement (seconds) | X | X |
| ``motion_delay`` | Délai avant prise en compte du mouvement (seconds) | X | X |
| ``motion_off_delay`` | Délai avant prise en compte de la fin de mouvement (seconds) | X | X |
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X |
| ``no_motion_preset`` | Preset à utiliser si pas de mouvement détecté | X | X |
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X |
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.

>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 3.6**: Add a `motion_off_delay` parameter for activity management,
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
Expand Down Expand Up @@ -228,15 +229,17 @@ We will now see how to configure the new Activity mode.
What we need:
- a **motion sensor**. The entity id of a motion sensor. Motion sensor states should be 'on' (motion detected) or 'off' (no motion detected)
- a **motion delay** (in seconds) duration defining how long we wait for motion confirmation before considering the motion
- a **end of motion delay** (in seconds) duration defining how long we wait for end of motion confirmation before considering the end of motion
- a **target "motion" preset**. We will used the temperature of this preset when an activity is detected.
- a **target "no motion" preset**. We will used the temperature of this second preset when no activity is detected.

So imagine we want to have the following behavior :
- we have room with a thermostat set in activity mode, the "motion" mode chosen is comfort (21.5C), the "no motion" mode chosen is Eco (18.5 C) and the motion delay is 5 min.
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
- the person leaves the room, after 5 min the temperature is set back to 18.5 C

- we have a room with a thermostat set to activity mode, the "movement" mode chosen is comfort (21.5°C), the "no movement" mode chosen is Eco (18.5°C) and the movement delay is 30 sec during detection and 5 minutes at the end of detection.
- the room has been empty for a while (no activity detected), the temperature of this room is 18.5°
- someone enters the room, activity is detected if movement is present for at least 30 seconds. The temperature then rises to 21.5°
- if the movement is present for less than 30 seconds (rapid passage), the temperature remains at 18.5°,
- imagine that the temperature has risen to 21.5°, when the person leaves the room, after 5 minutes the temperature is reduced to 18.5°.
- if the person returns before 5 minutes, the temperature remains at 21.5°

For this to work, the climate thermostat should be in ``Activity`` preset mode.

> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
Expand Down Expand Up @@ -339,7 +342,8 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic detection (in °/min) | X | X |
| ``window_auto_max_duration`` | Maximum duration of automatic window open detection (in min) | X | X |
| ``motion_sensor_entity_id`` | Motion sensor entity id | X | X |
| ``motion_delay`` | Motion delay (seconds) | X | X |
| ``motion_delay`` | Delay before considering the motion (seconds) | X | X |
| ``motion_off_delay`` | Delay before considering the end of motion (seconds) | X | X |
| ``motion_preset`` | Preset to use when motion is detected | X | X |
| ``no_motion_preset`` | Preset to use when no motion is detected | X | X |
| ``power_sensor_entity_id`` | Power sensor entity id | X | X |
Expand Down
90 changes: 62 additions & 28 deletions custom_components/versatile_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
Expand Down Expand Up @@ -399,6 +400,10 @@ def post_init(self, entry_infos):

self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY)
if not self._motion_off_delay_sec:
self._motion_off_delay_sec = self._motion_delay_sec

self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
self._motion_on = (
Expand Down Expand Up @@ -1528,11 +1533,12 @@ async def _async_motion_changed(self, event):
# Check delay condition
async def try_motion_condition(_):
try:
delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
long_enough = condition.state(
self.hass,
self._motion_sensor_entity_id,
new_state.state,
timedelta(seconds=self._motion_delay_sec),
timedelta(seconds=delay),
)
except ConditionError:
long_enough = False
Expand All @@ -1541,38 +1547,66 @@ async def try_motion_condition(_):
_LOGGER.debug(
"Motion delay condition is not satisfied. Ignore motion event"
)
return
else:
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
self._motion_state = new_state.state
if self._attr_preset_mode == PRESET_ACTIVITY:
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
_LOGGER.info(
"%s - Motion condition have changes. New preset temp will be %s",
self,
new_preset,
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
self._motion_call_cancel = None

_LOGGER.debug("%s - Motion delay condition is satisfied", self)
self._motion_state = new_state.state
if self._attr_preset_mode == PRESET_ACTIVITY:
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
_LOGGER.info(
"%s - Motion condition have changes. New preset temp will be %s",
self,
new_preset,
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
im_on = (self._motion_state == STATE_ON)
delay_running = (self._motion_call_cancel is not None)
event_on = (new_state.state == STATE_ON)

if self._motion_call_cancel:
def arm():
""" Arm the timer"""
delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
self._motion_call_cancel = async_call_later(
self.hass, timedelta(seconds=delay), try_motion_condition
)

def desarm():
# restart the timer
self._motion_call_cancel()
self._motion_call_cancel = None
self._motion_call_cancel = async_call_later(
self.hass, timedelta(seconds=self._motion_delay_sec), try_motion_condition
)

# For testing purpose we need to access the inner function
return try_motion_condition
# if I'm off
if not im_on:
if event_on and not delay_running:
_LOGGER.debug("%s - Arm delay cause i'm off and event is on and no delay is running", self)
arm()
return try_motion_condition
# Ignore the event
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
return None
else: # I'm On
if not event_on and not delay_running:
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
arm()
return try_motion_condition
if event_on and delay_running:
_LOGGER.debug("%s - Desarm off delay cause i'm on and event is on and a delay is running", self)
desarm()
return None
# Ignore the event
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
return None

@callback
async def _check_switch_initial_state(self):
Expand Down
2 changes: 2 additions & 0 deletions custom_components/versatile_thermostat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
Expand Down Expand Up @@ -292,6 +293,7 @@ def __init__(self, infos) -> None:
),
),
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_MOTION_OFF_DELAY, default=300): cv.positive_int,
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
CONF_PRESETS_SELECTIONABLE
),
Expand Down
1 change: 1 addition & 0 deletions custom_components/versatile_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CONF_PROP_FUNCTION = "proportional_function"
CONF_WINDOW_DELAY = "window_delay"
CONF_MOTION_DELAY = "motion_delay"
CONF_MOTION_OFF_DELAY = "motion_off_delay"
CONF_MOTION_PRESET = "motion_preset"
CONF_NO_MOTION_PRESET = "no_motion_preset"
CONF_TPI_COEF_INT = "tpi_coef_int"
Expand Down
20 changes: 18 additions & 2 deletions custom_components/versatile_thermostat/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
Expand Down Expand Up @@ -237,7 +245,15 @@
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": {
"motion_sensor_entity_id": "Motion sensor entity id",
"motion_delay": "Motion delay (seconds)",
"motion_delay": "Activation delay",
"motion_off_delay": "Deactivation delay",
"motion_preset": "Motion preset",
"no_motion_preset": "No motion preset"
},
"data_description": {
"motion_sensor_entity_id": "The entity id of the motion sensor",
"motion_delay": "Motion activation activation delay (seconds)",
"motion_off_delay": "Motion deactivation delay (seconds)",
"motion_preset": "Preset to use when motion is detected",
"no_motion_preset": "Preset to use when no motion is detected"
}
Expand Down
2 changes: 2 additions & 0 deletions custom_components/versatile_thermostat/tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_OFF_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_POWER_SENSOR,
Expand Down Expand Up @@ -136,6 +137,7 @@
MOCK_MOTION_CONFIG = {
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
CONF_MOTION_DELAY: 10,
CONF_MOTION_OFF_DELAY: 30,
CONF_MOTION_PRESET: PRESET_COMFORT,
CONF_NO_MOTION_PRESET: PRESET_ECO,
}
Expand Down
32 changes: 0 additions & 32 deletions custom_components/versatile_thermostat/tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,38 +157,6 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)

# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "window"
# assert result["errors"] == {}

# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_WINDOW_CONFIG
# )

# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "motion"
# assert result["errors"] == {}

# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_MOTION_CONFIG
# )

# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "power"
# assert result["errors"] == {}

# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_POWER_CONFIG
# )

# assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
# assert result["step_id"] == "presence"
# assert result["errors"] == {}

# result = await hass.config_entries.flow.async_configure(
# result["flow_id"], user_input=MOCK_PRESENCE_CONFIG
# )

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
Expand Down
Loading

0 comments on commit 043fd5f

Please sign in to comment.