move AC mode config under the right configuration step (#108)
* move AC mode config under the right configuration step
This commit is contained in:
@@ -16,7 +16,6 @@
|
|||||||
"ms-python.vscode-pylance"
|
"ms-python.vscode-pylance"
|
||||||
],
|
],
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached",
|
|
||||||
"source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
|
"source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
|
||||||
"source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
"source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
||||||
],
|
],
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -103,4 +103,6 @@ dist
|
|||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
# init file required for unittest
|
||||||
|
custom_components/__init__.py
|
||||||
__pycache__
|
__pycache__
|
||||||
68
container
68
container
@@ -6,33 +6,45 @@
|
|||||||
|
|
||||||
cd $HA
|
cd $HA
|
||||||
|
|
||||||
echo "arguments are: "$*
|
function get_dev() {
|
||||||
# Post installation of container
|
cd /workspaces/versatile_thermostat/custom_components/versatile_thermostat/
|
||||||
command=$1
|
pip install pytest
|
||||||
if [ "$command" == "install" ]; then
|
pip install -r requirements_dev.txt
|
||||||
echo "Running container post installation"
|
pip install -r requirements_test.txt
|
||||||
script/setup
|
sudo chown -R vscode: /home/vscode/core
|
||||||
fi
|
cd -
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$command" == "start" ]; then
|
echo "arguments are: "$1
|
||||||
echo "Running container start"
|
|
||||||
hass -c ./config --debug
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$command" == "translations" ]; then
|
case $1 in
|
||||||
echo "Running container start"
|
start)
|
||||||
python3 -m script.translations develop
|
echo "Running container start"
|
||||||
fi
|
cd $HA
|
||||||
|
hass -c ./config --debug
|
||||||
if [ "$command" == "hassfest" ]; then
|
;;
|
||||||
echo "Running container start"
|
dev-setup)
|
||||||
python3 -m script.hassfest
|
get_dev
|
||||||
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
;;
|
||||||
fi
|
install)
|
||||||
|
echo "Running container post installation"
|
||||||
if [ "$command" == "restart" ]; then
|
script/setup
|
||||||
echo "Killing existing container"
|
;;
|
||||||
pkill hass
|
translations)
|
||||||
echo "Killing existing container"
|
echo "Running container start"
|
||||||
hass -c ./config
|
cd $HA
|
||||||
fi
|
python3 -m script.translations develop
|
||||||
|
;;
|
||||||
|
hassfest)
|
||||||
|
echo "Running container start"
|
||||||
|
python3 -m script.hassfest
|
||||||
|
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
echo "Killing existing container"
|
||||||
|
pkill hass
|
||||||
|
echo "Killing existing container"
|
||||||
|
cd $HA
|
||||||
|
hass -c ./config
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
entry_infos,
|
entry_infos,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._ac_mode = entry_infos.get(CONF_AC_MODE) == True
|
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True
|
||||||
# convert entry_infos into usable attributes
|
# convert entry_infos into usable attributes
|
||||||
presets = {}
|
presets = {}
|
||||||
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
||||||
@@ -339,7 +339,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
||||||
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||||
self._is_over_climate = True
|
self._is_over_climate = True
|
||||||
for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]:
|
for climate in [
|
||||||
|
CONF_CLIMATE,
|
||||||
|
CONF_CLIMATE_2,
|
||||||
|
CONF_CLIMATE_3,
|
||||||
|
CONF_CLIMATE_4,
|
||||||
|
]:
|
||||||
if entry_infos.get(climate):
|
if entry_infos.get(climate):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
@@ -769,7 +774,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
if self._prop_algorithm:
|
if self._prop_algorithm:
|
||||||
self._prop_algorithm.calculate(
|
self._prop_algorithm.calculate(
|
||||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
self._target_temp,
|
||||||
|
self._cur_temp,
|
||||||
|
self._cur_ext_temp,
|
||||||
|
self._hvac_mode == HVACMode.COOL,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.create_task(self._check_switch_initial_state())
|
self.hass.create_task(self._check_switch_initial_state())
|
||||||
@@ -962,8 +970,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
|
# 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.
|
# delta will be managed by climate_state_change event.
|
||||||
# if self._is_over_climate:
|
# if self._is_over_climate:
|
||||||
# if one not OFF -> return it
|
# if one not OFF -> return it
|
||||||
# else OFF
|
# else OFF
|
||||||
# for under in self._underlyings:
|
# for under in self._underlyings:
|
||||||
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
|
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
|
||||||
# return mode
|
# return mode
|
||||||
@@ -983,7 +991,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# else OFF
|
# else OFF
|
||||||
one_idle = False
|
one_idle = False
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]:
|
if (action := under.hvac_action) not in [
|
||||||
|
HVACAction.IDLE,
|
||||||
|
HVACAction.OFF,
|
||||||
|
]:
|
||||||
return action
|
return action
|
||||||
if under.hvac_action == HVACAction.IDLE:
|
if under.hvac_action == HVACAction.IDLE:
|
||||||
one_idle = True
|
one_idle = True
|
||||||
@@ -995,6 +1006,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
return HVACAction.OFF
|
return HVACAction.OFF
|
||||||
if not self._is_device_active:
|
if not self._is_device_active:
|
||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
if self._hvac_mode == HVACMode.COOL:
|
||||||
|
return HVACAction.COOLING
|
||||||
return HVACAction.HEATING
|
return HVACAction.HEATING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1066,7 +1079,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mean_cycle_power(self) -> float | None:
|
def mean_cycle_power(self) -> float | None:
|
||||||
"""Returns tne mean power consumption during the cycle"""
|
"""Returns the mean power consumption during the cycle"""
|
||||||
if not self._device_power or self._is_over_climate:
|
if not self._device_power or self._is_over_climate:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1613,7 +1626,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
||||||
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
|
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
|
||||||
# We need to do the same check for over_climate underlyings
|
# We need to do the same check for over_climate underlyings
|
||||||
#if self.is_over_climate:
|
# if self.is_over_climate:
|
||||||
# return
|
# return
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
await under.check_initial_state(self._hvac_mode)
|
await under.check_initial_state(self._hvac_mode)
|
||||||
@@ -1632,16 +1645,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@callback
|
@callback
|
||||||
async def _async_climate_changed(self, event):
|
async def _async_climate_changed(self, event):
|
||||||
"""Handle unerdlying climate state changes.
|
"""Handle unerdlying climate state changes.
|
||||||
This method takes the underlying values and update the VTherm with them.
|
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
|
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
|
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
|
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
|
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.
|
which is important for feedaback and which cannot generates loops.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def end_climate_changed(changes):
|
async def end_climate_changed(changes):
|
||||||
""" To end the event management"""
|
"""To end the event management"""
|
||||||
if changes:
|
if changes:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
@@ -1667,15 +1680,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None
|
old_state_date_changed = (
|
||||||
old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None
|
old_state.last_changed if old_state and old_state.last_changed 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_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
|
||||||
|
)
|
||||||
|
|
||||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
# 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
|
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||||
#if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
# _LOGGER.debug(
|
||||||
|
# "The underlying switch to idle instead of OFF. We will consider it as OFF"
|
||||||
|
# )
|
||||||
# new_hvac_mode = HVACMode.OFF
|
# new_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
@@ -1687,7 +1710,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
old_hvac_action,
|
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)
|
if new_hvac_mode in [
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.HEAT_COOL,
|
||||||
|
HVACMode.DRY,
|
||||||
|
HVACMode.AUTO,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
None,
|
||||||
|
]:
|
||||||
|
self._hvac_mode = new_hvac_mode
|
||||||
|
|
||||||
# Interpretation of hvac action
|
# Interpretation of hvac action
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
@@ -1733,21 +1766,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if new_state_date_updated and self._last_change_time:
|
if new_state_date_updated and self._last_change_time:
|
||||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||||
if delta < 10:
|
if delta < 10:
|
||||||
_LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
|
_LOGGER.info(
|
||||||
|
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
||||||
|
self,
|
||||||
)
|
)
|
||||||
await end_climate_changed(changes)
|
await end_climate_changed(changes)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_hvac_mode in [
|
if (
|
||||||
HVACMode.OFF,
|
new_hvac_mode
|
||||||
HVACMode.HEAT,
|
in [
|
||||||
HVACMode.COOL,
|
HVACMode.OFF,
|
||||||
HVACMode.HEAT_COOL,
|
HVACMode.HEAT,
|
||||||
HVACMode.DRY,
|
HVACMode.COOL,
|
||||||
HVACMode.AUTO,
|
HVACMode.HEAT_COOL,
|
||||||
HVACMode.FAN_ONLY,
|
HVACMode.DRY,
|
||||||
None
|
HVACMode.AUTO,
|
||||||
] and self._hvac_mode != new_hvac_mode:
|
HVACMode.FAN_ONLY,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
and self._hvac_mode != new_hvac_mode
|
||||||
|
):
|
||||||
changes = True
|
changes = True
|
||||||
self._hvac_mode = new_hvac_mode
|
self._hvac_mode = new_hvac_mode
|
||||||
# Update all underlyings state
|
# Update all underlyings state
|
||||||
@@ -1757,15 +1796,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
if not changes:
|
if not changes:
|
||||||
# try to manage new target temperature set if state
|
# 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)
|
_LOGGER.debug(
|
||||||
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:
|
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
||||||
_LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp)
|
self.target_temperature,
|
||||||
await self.async_set_temperature(temperature = new_target_temp)
|
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)
|
||||||
changes = True
|
changes = True
|
||||||
|
|
||||||
await end_climate_changed(changes)
|
await end_climate_changed(changes)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_temp(self, state: State):
|
async def _async_update_temp(self, state: State):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
@@ -2208,13 +2259,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
|
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
|
||||||
shouldClimateBeInSecurity = False # temp_cond and climate_cond
|
shouldClimateBeInSecurity = False # temp_cond and climate_cond
|
||||||
shouldSwitchBeInSecurity = temp_cond and switch_cond
|
shouldSwitchBeInSecurity = temp_cond and switch_cond
|
||||||
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
|
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
|
||||||
|
|
||||||
shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity
|
shouldStartSecurity = (
|
||||||
|
mode_cond and not self._security_state and shouldBeInSecurity
|
||||||
|
)
|
||||||
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
# attr_preset_mode is not necessary normaly. It is just here to be sure
|
||||||
shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY
|
shouldStopSecurity = (
|
||||||
|
self._security_state
|
||||||
|
and not shouldBeInSecurity
|
||||||
|
and self._attr_preset_mode == PRESET_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
# Logging and event
|
# Logging and event
|
||||||
if shouldStartSecurity:
|
if shouldStartSecurity:
|
||||||
@@ -2376,7 +2433,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug("%s - recalculate all", self)
|
_LOGGER.debug("%s - recalculate all", self)
|
||||||
if not self._is_over_climate:
|
if not self._is_over_climate:
|
||||||
self._prop_algorithm.calculate(
|
self._prop_algorithm.calculate(
|
||||||
self._target_temp, self._cur_temp, self._cur_ext_temp
|
self._target_temp,
|
||||||
|
self._cur_temp,
|
||||||
|
self._cur_ext_temp,
|
||||||
|
self._hvac_mode == HVACMode.COOL,
|
||||||
)
|
)
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -2463,18 +2523,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||||
}
|
}
|
||||||
if self._is_over_climate:
|
if self._is_over_climate:
|
||||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
self._attr_extra_state_attributes[
|
||||||
0
|
"underlying_climate_0"
|
||||||
].entity_id
|
] = self._underlyings[0].entity_id
|
||||||
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||||
1
|
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||||
].entity_id if len(self._underlyings) > 1 else None
|
)
|
||||||
self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||||
2
|
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||||
].entity_id if len(self._underlyings) > 2 else None
|
)
|
||||||
self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[
|
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||||
3
|
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||||
].entity_id if len(self._underlyings) > 3 else None
|
)
|
||||||
|
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"start_hvac_action_date"
|
"start_hvac_action_date"
|
||||||
@@ -2566,7 +2626,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# If the changed preset is active, change the current temperature
|
# If the changed preset is active, change the current temperature
|
||||||
# Issue #119 - reload new preset temperature also in ac mode
|
# Issue #119 - reload new preset temperature also in ac mode
|
||||||
if preset.startswith(self._attr_preset_mode):
|
if preset.startswith(self._attr_preset_mode):
|
||||||
await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
|
await self._async_set_preset_mode_internal(
|
||||||
|
preset.rstrip(PRESET_AC_SUFFIX), force=True
|
||||||
|
)
|
||||||
await self._async_control_heating(force=True)
|
await self._async_control_heating(force=True)
|
||||||
|
|
||||||
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):
|
async def service_set_security(self, delay_min, min_on_percent, default_on_percent):
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
PROPORTIONAL_FUNCTION_TPI,
|
PROPORTIONAL_FUNCTION_TPI,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -244,7 +245,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -45,19 +45,33 @@ class PropAlgorithm:
|
|||||||
self._default_on_percent = 0
|
self._default_on_percent = 0
|
||||||
|
|
||||||
def calculate(
|
def calculate(
|
||||||
self, target_temp: float, current_temp: float, ext_current_temp: float
|
self,
|
||||||
|
target_temp: float,
|
||||||
|
current_temp: float,
|
||||||
|
ext_current_temp: float,
|
||||||
|
cooling=False,
|
||||||
):
|
):
|
||||||
"""Do the calculation of the duration"""
|
"""Do the calculation of the duration"""
|
||||||
if target_temp is None or current_temp is None:
|
if target_temp is None or current_temp is None:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
|
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long
|
||||||
)
|
)
|
||||||
self._calculated_on_percent = 0
|
self._calculated_on_percent = 0
|
||||||
else:
|
else:
|
||||||
delta_temp = target_temp - current_temp
|
if cooling:
|
||||||
delta_ext_temp = (
|
delta_temp = current_temp - target_temp
|
||||||
target_temp - ext_current_temp if ext_current_temp is not None else 0
|
delta_ext_temp = (
|
||||||
)
|
ext_current_temp
|
||||||
|
if ext_current_temp is not None
|
||||||
|
else 0 - target_temp
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
delta_temp = target_temp - current_temp
|
||||||
|
delta_ext_temp = (
|
||||||
|
target_temp - ext_current_temp
|
||||||
|
if ext_current_temp is not None
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
if self._function == PROPORTIONAL_FUNCTION_TPI:
|
if self._function == PROPORTIONAL_FUNCTION_TPI:
|
||||||
self._calculated_on_percent = (
|
self._calculated_on_percent = (
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
|||||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_HEATER: "switch.mock_switch",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_AC_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||||
@@ -105,6 +106,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
|||||||
CONF_HEATER_3: "switch.mock_4switch2",
|
CONF_HEATER_3: "switch.mock_4switch2",
|
||||||
CONF_HEATER_4: "switch.mock_4switch3",
|
CONF_HEATER_4: "switch.mock_4switch3",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_AC_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||||
@@ -114,7 +116,6 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
|||||||
|
|
||||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
CONF_AC_MODE: False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_PRESETS_CONFIG = {
|
MOCK_PRESETS_CONFIG = {
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ async def test_user_config_flow_over_4_switches(
|
|||||||
CONF_HEATER_3: "switch.mock_switch3",
|
CONF_HEATER_3: "switch.mock_switch3",
|
||||||
CONF_HEATER_4: "switch.mock_switch4",
|
CONF_HEATER_4: "switch.mock_switch4",
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_AC_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state: None):
|
||||||
"""Test the TPI calculation"""
|
"""Test the TPI calculation"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@@ -50,36 +50,52 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
assert tpi_algo.off_time_sec == 0
|
assert tpi_algo.off_time_sec == 0
|
||||||
assert entity.mean_cycle_power is None # no device power configured
|
assert entity.mean_cycle_power is None # no device power configured
|
||||||
|
|
||||||
tpi_algo.calculate(15, 14, 5)
|
tpi_algo.calculate(15, 14, 5, False)
|
||||||
assert tpi_algo.on_percent == 0.4
|
assert tpi_algo.on_percent == 0.4
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 120
|
assert tpi_algo.on_time_sec == 120
|
||||||
assert tpi_algo.off_time_sec == 180
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
tpi_algo.set_security(0.1)
|
tpi_algo.set_security(0.1)
|
||||||
tpi_algo.calculate(15, 14, 5)
|
tpi_algo.calculate(15, 14, 5, False)
|
||||||
assert tpi_algo.on_percent == 0.1
|
assert tpi_algo.on_percent == 0.1
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30)
|
||||||
assert tpi_algo.off_time_sec == 270
|
assert tpi_algo.off_time_sec == 270
|
||||||
|
|
||||||
tpi_algo.unset_security()
|
tpi_algo.unset_security()
|
||||||
tpi_algo.calculate(15, 14, 5)
|
tpi_algo.calculate(15, 14, 5, False)
|
||||||
assert tpi_algo.on_percent == 0.4
|
assert tpi_algo.on_percent == 0.4
|
||||||
assert tpi_algo.calculated_on_percent == 0.4
|
assert tpi_algo.calculated_on_percent == 0.4
|
||||||
assert tpi_algo.on_time_sec == 120
|
assert tpi_algo.on_time_sec == 120
|
||||||
assert tpi_algo.off_time_sec == 180
|
assert tpi_algo.off_time_sec == 180
|
||||||
|
|
||||||
# Test minimal activation delay
|
# Test minimal activation delay
|
||||||
tpi_algo.calculate(15, 14.7, 15)
|
tpi_algo.calculate(15, 14.7, 15, False)
|
||||||
assert tpi_algo.on_percent == 0.09
|
assert tpi_algo.on_percent == 0.09
|
||||||
assert tpi_algo.calculated_on_percent == 0.09
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
assert tpi_algo.on_time_sec == 0
|
assert tpi_algo.on_time_sec == 0
|
||||||
assert tpi_algo.off_time_sec == 300
|
assert tpi_algo.off_time_sec == 300
|
||||||
|
|
||||||
tpi_algo.set_security(0.09)
|
tpi_algo.set_security(0.09)
|
||||||
tpi_algo.calculate(15, 14.7, 15)
|
tpi_algo.calculate(15, 14.7, 15, False)
|
||||||
assert tpi_algo.on_percent == 0.09
|
assert tpi_algo.on_percent == 0.09
|
||||||
assert tpi_algo.calculated_on_percent == 0.09
|
assert tpi_algo.calculated_on_percent == 0.09
|
||||||
assert tpi_algo.on_time_sec == 0
|
assert tpi_algo.on_time_sec == 0
|
||||||
assert tpi_algo.off_time_sec == 300
|
assert tpi_algo.off_time_sec == 300
|
||||||
|
|
||||||
|
tpi_algo.unset_security()
|
||||||
|
tpi_algo.calculate(25, 30, 35, True)
|
||||||
|
assert tpi_algo.on_percent == 1
|
||||||
|
assert tpi_algo.calculated_on_percent == 1
|
||||||
|
assert tpi_algo.on_time_sec == 300
|
||||||
|
assert tpi_algo.off_time_sec == 0
|
||||||
|
assert entity.mean_cycle_power is None # no device power configured
|
||||||
|
|
||||||
|
tpi_algo.set_security(0.09)
|
||||||
|
tpi_algo.calculate(25, 30, 35, True)
|
||||||
|
assert tpi_algo.on_percent == 0.09
|
||||||
|
assert tpi_algo.calculated_on_percent == 1
|
||||||
|
assert tpi_algo.on_time_sec == 0
|
||||||
|
assert tpi_algo.off_time_sec == 300
|
||||||
|
assert entity.mean_cycle_power is None # no device power configured
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# If we should heat, starts the cycle with delay
|
# If we should heat, starts the cycle with delay
|
||||||
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0:
|
||||||
# Starts the cycle after the initial delay
|
# Starts the cycle after the initial delay
|
||||||
self._async_cancel_cycle = self.call_later(
|
self._async_cancel_cycle = self.call_later(
|
||||||
self._hass, self._initial_delay_sec, self._turn_on_later
|
self._hass, self._initial_delay_sec, self._turn_on_later
|
||||||
|
|||||||
Reference in New Issue
Block a user