Compare commits

..

1 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
5 changed files with 50 additions and 42 deletions

View File

@@ -130,10 +130,10 @@ class FeatureWindowManager(BaseFeatureManager):
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 # Try to get last window bypass state
old_state = await self._vtherm.async_get_last_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") == True else False 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:
@@ -453,7 +453,7 @@ 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 self._window_state == STATE_ON: if self._window_state == STATE_ON:
if not self._is_window_bypass: if not self._is_window_bypass:
_LOGGER.info( _LOGGER.info(

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "7.2.2", "version": "7.2.1",
"zeroconf": [] "zeroconf": []
} }

View File

@@ -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

View File

@@ -968,10 +968,10 @@ class UnderlyingValve(UnderlyingEntity):
# This could happens in unit test if input_number domain is not yet loaded # This could happens in unit test if input_number domain is not yet loaded
# raise err # raise err
async def send_percent_open(self, fixed_value: float = None): async def send_percent_open(self):
"""Send the percent open to the underlying valve""" """Send the percent open to the underlying valve"""
# This may fails if called after shutdown # This may fails if called after shutdown
return await self._send_value_to_number(self._entity_id, self._percent_open if fixed_value is None else fixed_value) return await self._send_value_to_number(self._entity_id, self._percent_open)
async def turn_off(self): async def turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
@@ -1108,15 +1108,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
self._max_offset_calibration: float = None self._max_offset_calibration: float = None
self._min_opening_degree: int = min_opening_degree self._min_opening_degree: int = min_opening_degree
def _normalize_opening_closing_degree(self, opening: float) -> float: async def send_percent_open(self):
"""Issue #902 - Normalize the opening and closing degree"""
new_opening = max(opening - 1, 0)
new_closing = max(self._max_opening_degree - 1 - new_opening, 0)
return new_opening, new_closing
async def send_percent_open(self, _: float = None):
"""Send the percent open to the underlying valve""" """Send the percent open to the underlying valve"""
if not self._is_min_max_initialized: if not self._is_min_max_initialized:
_LOGGER.debug( _LOGGER.debug(
@@ -1158,16 +1150,16 @@ class UnderlyingValveRegulation(UnderlyingValve):
else: else:
self._percent_open = 0 self._percent_open = 0
# Send closing_degree if set
opening_degree, closing_degree = self._normalize_opening_closing_degree(self._percent_open)
# We should not change the _percent_open because it is used to check if value has bchanged
# self._percent_open = opening_degree
# Send opening_degree # Send opening_degree
await super().send_percent_open(opening_degree) await super().send_percent_open()
# Send closing_degree if set
closing_degree = None
if self.have_closing_degree_entity: if self.have_closing_degree_entity:
await self._send_value_to_number(self._closing_degree_entity_id, closing_degree) await self._send_value_to_number(
self._closing_degree_entity_id,
closing_degree := self._max_opening_degree - self._percent_open,
)
# send offset_calibration to the difference between target temp and local temp # send offset_calibration to the difference between target temp and local temp
offset = None offset = None

View File

@@ -139,7 +139,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
call("climate","set_temperature",{ call("climate","set_temperature",{
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 15, # temp-min "temperature": 15, # temp-min
@@ -187,7 +187,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}),
call(domain='number', service='set_value', service_data={'value': 39}, target={'entity_id': 'number.mock_opening_degree'}), call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}), call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}),
# 3 = 18 (room) - 15 (current of underlying) + 0 (current offset) # 3 = 18 (room) - 15 (current of underlying) + 0 (current offset)
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'}) call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'})
@@ -233,7 +233,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
assert mock_service_call.call_count == 3 assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_opening_degree'}), call(domain='number', service='set_value', service_data={'value': 13}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}), call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}),
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'}) call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'})
@@ -280,7 +280,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset) # 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'}) call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'})
] ]
@@ -365,13 +365,25 @@ async def test_over_climate_valve_multi_presence(
mock_get_state_side_effect = SideEffects( mock_get_state_side_effect = SideEffects(
{ {
# Valve 1 is open # Valve 1 is open
"number.mock_opening_degree1": State("number.mock_opening_degree1", "10", {"min": 0, "max": 100}), "number.mock_opening_degree1": State(
"number.mock_closing_degree1": State("number.mock_closing_degree1", "89", {"min": 0, "max": 100}), "number.mock_opening_degree1", "10", {"min": 0, "max": 100}
"number.mock_offset_calibration1": State("number.mock_offset_calibration1", "0", {"min": -12, "max": 12}), ),
"number.mock_closing_degree1": State(
"number.mock_closing_degree1", "90", {"min": 0, "max": 100}
),
"number.mock_offset_calibration1": State(
"number.mock_offset_calibration1", "0", {"min": -12, "max": 12}
),
# Valve 2 is closed # Valve 2 is closed
"number.mock_opening_degree2": State("number.mock_opening_degree2", "0", {"min": 0, "max": 100}), "number.mock_opening_degree2": State(
"number.mock_closing_degree2": State("number.mock_closing_degree2", "99", {"min": 0, "max": 100}), "number.mock_opening_degree2", "0", {"min": 0, "max": 100}
"number.mock_offset_calibration2": State("number.mock_offset_calibration2", "10", {"min": -12, "max": 12}), ),
"number.mock_closing_degree2": State(
"number.mock_closing_degree2", "100", {"min": 0, "max": 100}
),
"number.mock_offset_calibration2": State(
"number.mock_offset_calibration2", "10", {"min": -12, "max": 12}
),
}, },
State("unknown.entity_id", "unknown"), State("unknown.entity_id", "unknown"),
) )
@@ -430,10 +442,10 @@ async def test_over_climate_valve_multi_presence(
mock_service_call.assert_has_calls([ mock_service_call.assert_has_calls([
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}),
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}),
call(domain='number', service='set_value', service_data={'value': 39}, target={'entity_id': 'number.mock_opening_degree1'}), call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}), call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 39}, target={'entity_id': 'number.mock_opening_degree2'}), call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}), call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
] ]
@@ -457,15 +469,15 @@ async def test_over_climate_valve_multi_presence(
assert vtherm.valve_open_percent == 0 assert vtherm.valve_open_percent == 0
# the underlying set temperature call and the call to the valve # the underlying set temperature call and the call to the valve
# assert mock_service_call.call_count == 8 assert mock_service_call.call_count == 8
mock_service_call.assert_has_calls([ mock_service_call.assert_has_calls([
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 17.2}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 17.2}),
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 17.2}), call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 17.2}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree1'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree2'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
] ]
) )
@@ -615,10 +627,10 @@ async def test_over_climate_valve_multi_min_opening_degrees(
assert mock_service_call.call_count == 6 assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([ mock_service_call.assert_has_calls([
# min is 60 # min is 60
call(domain='number', service='set_value', service_data={'value': 67}, target={'entity_id': 'number.mock_opening_degree1'}), call(domain='number', service='set_value', service_data={'value': 68}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 32}, target={'entity_id': 'number.mock_closing_degree1'}), call(domain='number', service='set_value', service_data={'value': 32}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}), call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 75}, target={'entity_id': 'number.mock_opening_degree2'}), call(domain='number', service='set_value', service_data={'value': 76}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 24}, target={'entity_id': 'number.mock_closing_degree2'}), call(domain='number', service='set_value', service_data={'value': 24}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
] ]
@@ -645,10 +657,10 @@ async def test_over_climate_valve_multi_min_opening_degrees(
assert mock_service_call.call_count == 6 assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([ mock_service_call.assert_has_calls([
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree1'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}), call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}), call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 99}, target={'entity_id': 'number.mock_closing_degree2'}), call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'}) call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
] ]
) )