Compare commits

..

2 Commits

Author SHA1 Message Date
Jean-Marc Collin
23ee8f3d7f NPE fix 2023-10-14 08:49:28 +02:00
Jean-Marc Collin
03723375e2 Issue #121 - loop when underlying is slow 2023-10-14 08:28:04 +02:00
9 changed files with 57 additions and 96 deletions

View File

@@ -114,22 +114,18 @@ climate:
name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat
name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat
name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat
name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3

View File

@@ -31,8 +31,6 @@ jobs:
- run: black .
tests:
# Tests don't run in Gitlab ci environment
if: 0
runs-on: "ubuntu-latest"
name: Run tests
steps:
@@ -43,10 +41,10 @@ jobs:
with:
python-version: "3.8"
- name: Install requirements
run: cd custom_components/versatile_thermostat && python3 -m pip install -r requirements_test.txt
run: python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
cd custom_components/versatile_thermostat && pytest \
pytest \
-qq \
--timeout=9 \
--durations=10 \

View File

@@ -956,6 +956,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Return current operation."""
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
# delta will be managed by climate_state_change event.
# TODO remove this when ok
# if self._is_over_climate:
# if one not OFF -> return it
# else OFF
@@ -1297,7 +1298,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
def reset_last_change_time(self, old_preset_mode=None):
"""Reset to now the last change time"""
self._last_change_time = datetime.now(tz=self._current_tz)
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
_LOGGER.warning("%s - last_change_time is now %s", self, self._last_change_time)
def reset_last_temperature_time(self, old_preset_mode=None):
"""Reset to now the last temperature time if conditions are satisfied"""
@@ -1597,22 +1598,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@callback
async def _async_climate_changed(self, event):
"""Handle unerdlying climate state changes.
This method takes the underlying values and update the VTherm with them.
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
less than 10 sec after the last command. What we want here is to take the values
from underlyings ONLY if someone have change directly on the underlying and not
as a return of the command. The only thing we take all the time is the HVACAction
which is important for feedaback and which cannot generates loops.
"""
async def end_climate_changed(changes):
""" To end the event management"""
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
"""Handle unerdlying climate state changes."""
new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
if not new_state:
@@ -1633,10 +1619,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else None
)
old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None
old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None
new_state_date_changed = new_state.last_changed if new_state and new_state.last_changed else None
new_state_date_updated = new_state.last_updated if new_state and new_state.last_updated else None
old_state_date_changed = old_state.last_changed if old_state.last_changed else None
old_state_date_updated = old_state.last_updated if old_state.last_updated else None
new_state_date_changed = new_state.last_changed if new_state.last_changed else None
new_state_date_updated = new_state.last_updated if new_state.last_updated else None
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
@@ -1653,9 +1639,26 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
old_hvac_action,
)
_LOGGER.debug("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated)
_LOGGER.warning("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated)
# Interpretation of hvac action
if new_hvac_mode in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None
] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
# Interpretation of hvac
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING,
HVACAction.DRYING,
@@ -1694,43 +1697,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
changes = True
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10:
_LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
)
await end_climate_changed(changes)
return
if new_hvac_mode in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None
] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
if not changes:
# try to manage new target temperature set if state
_LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes)
if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature:
_LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp)
await self.async_set_temperature(temperature = new_target_temp)
_LOGGER.warning("%s - Target temp have change to %s", self, new_target_temp)
# TODO temporary removes the temperature changes for test
# await self.async_set_temperature(temperature = new_target_temp)
changes = True
await end_climate_changed(changes)
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
@callback
async def _async_update_temp(self, state: State):
@@ -2145,6 +2124,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0
# TODO before change:
# mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
# fixed into this. Why if _is_over_climate we could into security even if HVACMode is OFF ?
mode_cond = self._hvac_mode != HVACMode.OFF
temp_cond: bool = (
@@ -2338,11 +2320,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
if self._is_over_climate:
self.update_custom_attributes()
return
_LOGGER.debug("%s - recalculate all", self)
if not self._is_over_climate:
self._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self._prop_algorithm.calculate(
self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.update_custom_attributes()
self.async_write_ha_state()
@@ -2417,18 +2402,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
.astimezone(self._current_tz)
.isoformat(),
"timezone": str(self._current_tz),
"window_sensor_entity_id": self._window_sensor_entity_id,
"window_delay_sec": self._window_delay_sec,
"window_auto_open_threshold": self._window_auto_open_threshold,
"window_auto_close_threshold": self._window_auto_close_threshold,
"window_auto_max_duration": self._window_auto_max_duration,
"motion_sensor_entity_id": self._motion_sensor_entity_id,
"presence_sensor_entity_id": self._presence_sensor_entity_id,
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
}
if self._is_over_climate:
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
0
].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
@@ -2529,9 +2509,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
)
# If the changed preset is active, change the current temperature
# Issue #119 - reload new preset temperature also in ac mode
if preset.startswith(self._attr_preset_mode):
await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
if self._attr_preset_mode == preset:
await self._async_set_preset_mode_internal(preset, force=True)
await self._async_control_heating(force=True)
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):

View File

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

View File

@@ -1,2 +1,2 @@
homeassistant==2023.10.3
homeassistant==2023.10.1
ffmpeg

View File

@@ -1,5 +1,4 @@
# Warning: For automatic run of test in Gitlab CI, we must not include other things that pytest-homeassistant-custom-component
-r requirements_dev.txt
aiodiscover
# aiodiscover
ulid_transform
pytest-homeassistant-custom-component

View File

@@ -530,16 +530,9 @@ async def test_bug_101(
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
# 2. Change the target temp of underlying thermostat
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
# Should NOT have been switched to Manual preset
assert entity.target_temperature == 17
assert entity.preset_mode is PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75)
# Should have been switched to Manual preset
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE

View File

@@ -163,7 +163,7 @@ async def test_one_switch_cycle(
await asyncio.sleep(0.1)
assert mock_heater_on.call_count == 1
# normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# TODO normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
@@ -522,9 +522,7 @@ async def test_multiple_climates_underlying_changes(
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, event_timestamp)
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, now)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
@@ -545,9 +543,7 @@ async def test_multiple_climates_underlying_changes(
# notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
) as mock_underlying_get_hvac_action:
# Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, event_timestamp)
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, now)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2023.10.3"
"homeassistant": "2023.7.3"
}