Compare commits

...

11 Commits

Author SHA1 Message Date
Jean-Marc Collin d4a719a660 Issue #903 - Modify "follow underlying changes" behavior - Transform 'Auto' hvac_mode to 'Heating' 2025-02-10 17:30:05 +00:00
Jean-Marc Collin ea0d66f0c6 Vack to HA 2025.1.4. It doesn't work on Github. 2025-02-10 16:53:28 +00:00
adi90x 641801084d Correct window bypass (#899) 2025-02-10 17:44:28 +01:00
adi90x 1576e9c189 Add bypass to the window open check and correct description (#897) 2025-02-10 17:42:06 +01:00
adi90x 313fa26160 Save & Recover Window Bypass state (#898)
* Remove windows_bypass state from unrecorded

* Recover bypass_state
2025-02-10 17:29:53 +01:00
Jean-Marc Collin 832ea78a12 FIX select_option 2025-02-10 08:33:40 +01:00
Jean-Marc Collin 5f167af331 Fix select_option 2025-02-10 08:33:06 +01:00
Jean-Marc Collin c20e641ac1 Release 7.2.0- Heating is inverted on over_switch with inverted commands ? (#891)
* Release 7.2.0- Heating is inverted on `over_switch` with inverted commands ?
Fixes #889

* Release

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-02-08 11:56:24 +01:00
Jean-Marc Collin 87064882a3 Fix warning 2025-02-08 09:39:51 +00:00
Jean-Marc Collin 1c0a7cb4e8 homeassistant==2025.2.1 2025-02-08 09:35:06 +00:00
Jean-Marc Collin b1f2fcd66d Fix frost_protection for Nodon 2025-02-05 18:47:08 +00:00
12 changed files with 92 additions and 53 deletions
+18
View File
@@ -191,6 +191,9 @@ input_boolean:
fake_valve_sonoff_trvzb2: fake_valve_sonoff_trvzb2:
name: Valve Sonoff TRVZB2 name: Valve Sonoff TRVZB2
icon: mdi:valve icon: mdi:valve
fake_inversed_heater:
name: Inversed Heater
icon: mdi:radiator-off
climate: climate:
- platform: generic_thermostat - platform: generic_thermostat
@@ -314,6 +317,21 @@ switch:
option: comfort-2 option: comfort-2
target: target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
- platform: template
switches:
fake_inversed_switch:
friendly_name: "A fake inversed switch"
value_template: "{{ is_state('input_boolean.fake_inversed_heater', 'on') }}"
turn_on:
action: input_boolean.turn_on
data: {}
target:
entity_id: input_boolean.fake_inversed_heater
turn_off:
action: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.fake_inversed_heater
frontend: frontend:
extra_module_url: extra_module_url:
@@ -44,7 +44,6 @@ class FeatureWindowManager(BaseFeatureManager):
{ {
"window_sensor_entity_id", "window_sensor_entity_id",
"is_window_configured", "is_window_configured",
"is_window_bypass",
"window_delay_sec", "window_delay_sec",
"window_off_delay_sec", "window_off_delay_sec",
"window_auto_configured", "window_auto_configured",
@@ -130,6 +129,11 @@ class FeatureWindowManager(BaseFeatureManager):
@overrides @overrides
async def start_listening(self): async def start_listening(self):
"""Start listening the underlying entity""" """Start listening the underlying entity"""
# Try to get last window bypass state
old_state = await self._vtherm.async_get_last_state()
self._is_window_bypass = True if old_state and old_state.attributes and old_state.attributes.get("is_window_bypass") is True else False
if self._is_configured: if self._is_configured:
self.stop_listening() self.stop_listening()
if self._window_sensor_entity_id: if self._window_sensor_entity_id:
@@ -449,22 +453,23 @@ class FeatureWindowManager(BaseFeatureManager):
"""Set the window bypass flag """Set the window bypass flag
Return True if state have been changed""" Return True if state have been changed"""
self._is_window_bypass = window_bypass self._is_window_bypass = window_bypass
if not self._is_window_bypass 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._vtherm.save_hvac_mode()
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
return True
if self._is_window_bypass and self._window_state: if self._window_state == STATE_ON:
_LOGGER.info( if not self._is_window_bypass:
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode", _LOGGER.info(
self, "%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
) self,
await self._vtherm.restore_hvac_mode(True) HVACMode.OFF,
)
self._vtherm.save_hvac_mode()
await self._vtherm.async_set_hvac_mode(HVACMode.OFF)
if self._is_window_bypass:
_LOGGER.info(
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
self,
)
await self._vtherm.restore_hvac_mode(True)
return True return True
return False return False
@@ -504,10 +509,10 @@ class FeatureWindowManager(BaseFeatureManager):
@property @property
def is_window_detected(self) -> bool: def is_window_detected(self) -> bool:
"""Return true if the presence is configured and presence sensor is OFF""" """Return true if the window is configured and open and bypass is not ON"""
return self._is_configured and ( return self._is_configured and (
self._window_state == STATE_ON or self._window_auto_state == STATE_ON self._window_state == STATE_ON or self._window_auto_state == STATE_ON
) ) and not self._is_window_bypass
@property @property
def window_sensor_entity_id(self) -> bool: def window_sensor_entity_id(self) -> bool:
@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "7.2.0", "version": "7.2.1",
"zeroconf": [] "zeroconf": []
} }
@@ -625,6 +625,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
changes = False changes = False
new_hvac_mode = new_state.state new_hvac_mode = new_state.state
# Issue #903 - patch AUTO mode
if new_hvac_mode == HVACMode.AUTO:
new_hvac_mode = HVACMode.HEAT if not self.ac_mode else HVACMode.COOL
old_state = event.data.get("old_state") old_state = event.data.get("old_state")
# Issue #829 - refresh underlying command if it comes back to life # Issue #829 - refresh underlying command if it comes back to life
@@ -281,8 +281,8 @@ class UnderlyingSwitch(UnderlyingEntity):
# not self.is_inversed and real_state # not self.is_inversed and real_state
# ) # )
is_on = self._hass.states.is_state(self._entity_id, self._on_command.get("state")) is_on = self._hass.states.is_state(self._entity_id, self._on_command.get("state"))
if self.is_inversed: # if self.is_inversed:
return not is_on # return not is_on
return is_on return is_on
+6 -6
View File
@@ -71,12 +71,12 @@ To customize the commands, click on `Add` at the bottom of the page for both the
Then, specify the on and off commands using the format `command[/attribute[:value]]`. Then, specify the on and off commands using the format `command[/attribute[:value]]`.
The available commands depend on the type of underlying device: The available commands depend on the type of underlying device:
| Underlying Device Type | Possible On Commands | Possible Off Commands | Applies To | | Underlying Device Type | Possible On Commands | Possible Off Commands | Applies To |
| --------------------------- | ------------------------------------- | ----------------------------------- | ----------------------------- | | --------------------------- | ------------------------------------- | ---------------------------------------------- | ----------------------------- |
| `switch` or `input_boolean` | `turn_on` | `turn_off` | All switches | | `switch` or `input_boolean` | `turn_on` | `turn_off` | All switches |
| `select` or `input_select` | `select_option/option:comfort` | `set_option/option:frost` | Nodon SIN-4-FP-21 and similar | | `select` or `input_select` | `select_option/option:comfort` | `select_option/option:frost_protection` | Nodon SIN-4-FP-21 and similar |
| `climate` (hvac_mode) | `set_hvac_mode/hvac_mode:heat` | `set_hvac_mode/hvac_mode:off` | eCosy (via Tuya Local) | | `climate` (hvac_mode) | `set_hvac_mode/hvac_mode:heat` | `set_hvac_mode/hvac_mode:off` | eCosy (via Tuya Local) |
| `climate` (preset) | `set_preset_mode/preset_mode:comfort` | `set_preset_mode/preset_mode:frost` | Heatzy | | `climate` (preset) | `set_preset_mode/preset_mode:comfort` | `set_preset_mode/preset_mode:frost_protection` | Heatzy |
Of course, these examples can be adapted to your specific case. Of course, these examples can be adapted to your specific case.
+6 -6
View File
@@ -69,12 +69,12 @@ Pour personnaliser les commande, cliquez sur `Ajouter` en bas de page sur les co
et donner la commande d'allumage et d'exinction avec le format `commande[/attribut[:valeur]]`. et donner la commande d'allumage et d'exinction avec le format `commande[/attribut[:valeur]]`.
Les commandes possibles dépendent du type de sous-jacents : Les commandes possibles dépendent du type de sous-jacents :
| type de sous-jacent | commandes d'allumage possibles | commandes d'extinction possibles | S'applique à | | type de sous-jacent | commandes d'allumage possibles | commandes d'extinction possibles | S'applique à |
| --------------------------- | ------------------------------------- | ----------------------------------- | ------------------------------ | | --------------------------- | ------------------------------------- | ---------------------------------------------- | ------------------------------ |
| `switch` ou `input_boolean` | `turn_on` | `turn_off` | tous les switchs | | `switch` ou `input_boolean` | `turn_on` | `turn_off` | tous les switchs |
| `select` ou `input_select` | `select_option/option:comfort` | `set_option/option:frost` | Nodon SIN-4-FP-21 et assimilés | | `select` ou `input_select` | `select_option/option:comfort` | `select_option/option:frost_protection` | Nodon SIN-4-FP-21 et assimilés |
| `climate` (hvac_mode) | `set_hvac_mode/hvac_mode:heat` | `set_hvac_mode/hvac_mode:off` | eCosy (via Tuya Local) | | `climate` (hvac_mode) | `set_hvac_mode/hvac_mode:heat` | `set_hvac_mode/hvac_mode:off` | eCosy (via Tuya Local) |
| `climate` (preset) | `set_preset_mode/preset_mode:comfort` | `set_preset_mode/preset_mode:frost` | Heatzy | | `climate` (preset) | `set_preset_mode/preset_mode:comfort` | `set_preset_mode/preset_mode:frost_protection` | Heatzy |
Evidemment, tous ces exemples peuvent être adaptés à votre cas. Evidemment, tous ces exemples peuvent être adaptés à votre cas.
+1 -1
View File
@@ -1 +1 @@
homeassistant==2025.1.2 homeassistant==2025.1.4
+14 -15
View File
@@ -18,6 +18,12 @@ logging.getLogger().setLevel(logging.DEBUG)
async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state): async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window auto management""" """Test the Window auto management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 21,
}
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="TheOverSwitchMockName", title="TheOverSwitchMockName",
@@ -30,14 +36,11 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch", CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
@@ -51,14 +54,12 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
}, },
) )
with patch( # 0. Create the entity
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch( with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On "homeassistant.core.StateMachine.is_state", return_value=False # switch is On so is_state(switch, 'off') is False
): ):
entity: ThermostatOverSwitch = await create_thermostat( entity: ThermostatOverSwitch = await create_thermostat(hass, entry, "climate.theoverswitchmockname", temps)
hass, entry, "climate.theoverswitchmockname"
)
assert entity assert entity
assert entity.is_inversed assert entity.is_inversed
@@ -78,12 +79,10 @@ async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
assert mock_service_call.call_count == 0 assert mock_service_call.call_count == 0
# 1. Make the temperature down to activate the switch # 1. Make the temperature down to activate the switch
with patch( with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch( ) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off "homeassistant.core.StateMachine.is_state", return_value=True # switch is Off so is_state(switch, 'off') is True
): ):
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp) await send_temperature_change_event(entity, 19, event_timestamp)
+1 -1
View File
@@ -700,7 +700,7 @@ async def test_add_number_for_over_switch_use_central_presets_and_restore(
assert vtherm.use_central_config_temperature is True assert vtherm.use_central_config_temperature is True
# We should try to restore all 4 temp entities and the VTherm itself # We should try to restore all 4 temp entities and the VTherm itself
assert mock_restore_state.call_count == 4 + 1 assert mock_restore_state.call_count == 4 + 2
# 1. We search for NumberEntities # 1. We search for NumberEntities
for preset_name, value in temps.items(): for preset_name, value in temps.items():
+13
View File
@@ -71,6 +71,19 @@ from .commons import *
HVACMode.HEAT, HVACMode.HEAT,
True, True,
), ),
# Inversed switch without command personnalisations
(
True,
None,
None,
"turn_off",
{"entity_id": "switch.test"},
STATE_OFF,
"turn_on",
{"entity_id": "switch.test"},
STATE_ON,
True,
),
# Error cases invalid command # Error cases invalid command
( (
False, False,
+2 -2
View File
@@ -300,7 +300,7 @@ async def test_window_feature_manager_refresh_sensor_action_frost_only(
with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state: with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
# fmt:on # fmt:on
# Configurer les méthodes mockées # Configurer les méthodes mockées
fake_vtherm.save_target_temp = AsyncMock() fake_vtherm.save_target_temp = MagicMock()
fake_vtherm.set_hvac_off_reason = MagicMock() fake_vtherm.set_hvac_off_reason = MagicMock()
fake_vtherm.restore_target_temp = AsyncMock() fake_vtherm.restore_target_temp = AsyncMock()
fake_vtherm.change_target_temperature = AsyncMock() fake_vtherm.change_target_temperature = AsyncMock()
@@ -542,7 +542,7 @@ async def test_window_feature_manager_event_sensor_action_frost_only(
with patch("homeassistant.helpers.condition.state", return_value=long_enough): with patch("homeassistant.helpers.condition.state", return_value=long_enough):
# fmt:on # fmt:on
# Configurer les méthodes mockées # Configurer les méthodes mockées
fake_vtherm.save_target_temp = AsyncMock() fake_vtherm.save_target_temp = MagicMock()
fake_vtherm.set_hvac_off_reason = MagicMock() fake_vtherm.set_hvac_off_reason = MagicMock()
fake_vtherm.restore_target_temp = AsyncMock() fake_vtherm.restore_target_temp = AsyncMock()
fake_vtherm.change_target_temperature = AsyncMock() fake_vtherm.change_target_temperature = AsyncMock()