FIX issue #272 and #24ç - min and max values depending of the underlying
This commit is contained in:
@@ -59,8 +59,8 @@ input_number:
|
|||||||
unit_of_measurement: kW
|
unit_of_measurement: kW
|
||||||
fake_valve1:
|
fake_valve1:
|
||||||
name: The valve 1
|
name: The valve 1
|
||||||
min: 0
|
min: 10
|
||||||
max: 100
|
max: 90
|
||||||
icon: mdi:pipe-valve
|
icon: mdi:pipe-valve
|
||||||
unit_of_measurement: percentage
|
unit_of_measurement: percentage
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,8 @@
|
|||||||
"waderyan.gitblame",
|
"waderyan.gitblame",
|
||||||
"keesschollaart.vscode-home-assistant",
|
"keesschollaart.vscode-home-assistant",
|
||||||
"vscode.markdown-math",
|
"vscode.markdown-math",
|
||||||
"yzhang.markdown-all-in-one",
|
"yzhang.markdown-all-in-one"
|
||||||
"ms-python.vscode-pylance"
|
|
||||||
],
|
],
|
||||||
// "mounts": [
|
|
||||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
|
||||||
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
|
||||||
// ],
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -14,7 +14,8 @@
|
|||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.analysis.extraPaths": [
|
"python.analysis.extraPaths": [
|
||||||
// "/home/vscode/core",
|
// "/home/vscode/core",
|
||||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
|
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||||
|
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
|
||||||
],
|
],
|
||||||
"python.formatting.provider": "none"
|
"python.formatting.provider": "none"
|
||||||
}
|
}
|
||||||
@@ -234,45 +234,45 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
for climate in [
|
for climate in [
|
||||||
CONF_CLIMATE,
|
CONF_CLIMATE,
|
||||||
CONF_CLIMATE_2,
|
CONF_CLIMATE_2,
|
||||||
CONF_CLIMATE_3,
|
CONF_CLIMATE_3,
|
||||||
CONF_CLIMATE_4,
|
CONF_CLIMATE_4,
|
||||||
]:
|
]:
|
||||||
if entry_infos.get(climate):
|
if config_entry.get(climate):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
hass=self._hass,
|
hass=self._hass,
|
||||||
thermostat=self,
|
thermostat=self,
|
||||||
climate_entity_id=entry_infos.get(climate),
|
climate_entity_id=config_entry.get(climate),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.choose_auto_regulation_mode(
|
self.choose_auto_regulation_mode(
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_MODE)
|
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||||
else CONF_AUTO_REGULATION_NONE
|
else CONF_AUTO_REGULATION_NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
self._auto_regulation_dtemp = (
|
self._auto_regulation_dtemp = (
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
|
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||||
else 0.5
|
else 0.5
|
||||||
)
|
)
|
||||||
self._auto_regulation_period_min = (
|
self._auto_regulation_period_min = (
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||||
else 5
|
else 5
|
||||||
)
|
)
|
||||||
|
|
||||||
self._auto_fan_mode = (
|
self._auto_fan_mode = (
|
||||||
entry_infos.get(CONF_AUTO_FAN_MODE)
|
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||||
if entry_infos.get(CONF_AUTO_FAN_MODE) is not None
|
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||||
else CONF_AUTO_FAN_NONE
|
else CONF_AUTO_FAN_NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# useless for now
|
# useless for now
|
||||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||||
# """Initialize the thermostat over switch."""
|
# """Initialize the thermostat over switch."""
|
||||||
# super().__init__(hass, unique_id, name, entry_infos)
|
# super().__init__(hass, unique_id, name, config_entry)
|
||||||
_is_inversed: bool = None
|
_is_inversed: bool = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
|
|
||||||
self._prop_algorithm = PropAlgorithm(
|
self._prop_algorithm = PropAlgorithm(
|
||||||
self._proportional_function,
|
self._proportional_function,
|
||||||
@@ -85,13 +85,13 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
self._minimal_activation_delay,
|
self._minimal_activation_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
lst_switches = [config_entry.get(CONF_HEATER)]
|
||||||
if entry_infos.get(CONF_HEATER_2):
|
if config_entry.get(CONF_HEATER_2):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
lst_switches.append(config_entry.get(CONF_HEATER_2))
|
||||||
if entry_infos.get(CONF_HEATER_3):
|
if config_entry.get(CONF_HEATER_3):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
lst_switches.append(config_entry.get(CONF_HEATER_3))
|
||||||
if entry_infos.get(CONF_HEATER_4):
|
if config_entry.get(CONF_HEATER_4):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
lst_switches.append(config_entry.get(CONF_HEATER_4))
|
||||||
|
|
||||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||||
for idx, switch in enumerate(lst_switches):
|
for idx, switch in enumerate(lst_switches):
|
||||||
@@ -104,7 +104,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
|
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
|
||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change_event,
|
||||||
|
async_track_time_interval,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
@@ -16,39 +19,53 @@ from .underlyings import UnderlyingValve
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ThermostatOverValve(BaseThermostat):
|
class ThermostatOverValve(BaseThermostat):
|
||||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||||
|
|
||||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
_entity_component_unrecorded_attributes = (
|
||||||
{
|
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||||
"is_over_valve", "underlying_valve_0", "underlying_valve_1",
|
frozenset(
|
||||||
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec",
|
{
|
||||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
"is_over_valve",
|
||||||
}))
|
"underlying_valve_0",
|
||||||
|
"underlying_valve_1",
|
||||||
|
"underlying_valve_2",
|
||||||
|
"underlying_valve_3",
|
||||||
|
"on_time_sec",
|
||||||
|
"off_time_sec",
|
||||||
|
"cycle_min",
|
||||||
|
"function",
|
||||||
|
"tpi_coef_int",
|
||||||
|
"tpi_coef_ext",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Useless for now
|
# Useless for now
|
||||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||||
# """Initialize the thermostat over switch."""
|
# """Initialize the thermostat over switch."""
|
||||||
# super().__init__(hass, unique_id, name, entry_infos)
|
# super().__init__(hass, unique_id, name, config_entry)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_valve(self) -> bool:
|
def is_over_valve(self) -> bool:
|
||||||
""" True if the Thermostat is over_valve"""
|
"""True if the Thermostat is over_valve"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valve_open_percent(self) -> int:
|
def valve_open_percent(self) -> int:
|
||||||
""" Gives the percentage of valve needed"""
|
"""Gives the percentage of valve needed"""
|
||||||
if self._hvac_mode == HVACMode.OFF:
|
if self._hvac_mode == HVACMode.OFF:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
""" Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
self._prop_algorithm = PropAlgorithm(
|
self._prop_algorithm = PropAlgorithm(
|
||||||
self._proportional_function,
|
self._proportional_function,
|
||||||
self._tpi_coef_int,
|
self._tpi_coef_int,
|
||||||
@@ -57,21 +74,17 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
self._minimal_activation_delay,
|
self._minimal_activation_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
lst_valves = [config_entry.get(CONF_VALVE)]
|
||||||
if entry_infos.get(CONF_VALVE_2):
|
if config_entry.get(CONF_VALVE_2):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
lst_valves.append(config_entry.get(CONF_VALVE_2))
|
||||||
if entry_infos.get(CONF_VALVE_3):
|
if config_entry.get(CONF_VALVE_3):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_3))
|
lst_valves.append(config_entry.get(CONF_VALVE_3))
|
||||||
if entry_infos.get(CONF_VALVE_4):
|
if config_entry.get(CONF_VALVE_4):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_4))
|
lst_valves.append(config_entry.get(CONF_VALVE_4))
|
||||||
|
|
||||||
for _, valve in enumerate(lst_valves):
|
for _, valve in enumerate(lst_valves):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingValve(
|
UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
|
||||||
hass=self._hass,
|
|
||||||
thermostat=self,
|
|
||||||
valve_entity_id=valve
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
@@ -89,7 +102,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
async_track_state_change_event(
|
async_track_state_change_event(
|
||||||
self.hass, [valve.entity_id], self._async_valve_changed
|
self.hass, [valve.entity_id], self._async_valve_changed
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the control_heating
|
# Start the control_heating
|
||||||
# starts a cycle
|
# starts a cycle
|
||||||
@@ -107,29 +120,34 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
This method just log the change. It changes nothing to avoid loops.
|
This method just log the change. It changes nothing to avoid loops.
|
||||||
"""
|
"""
|
||||||
new_state = event.data.get("new_state")
|
new_state = event.data.get("new_state")
|
||||||
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
_LOGGER.debug(
|
||||||
|
"%s - _async_valve_changed new_state is %s", self, new_state.state
|
||||||
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
""" Custom attributes """
|
"""Custom attributes"""
|
||||||
super().update_custom_attributes()
|
super().update_custom_attributes()
|
||||||
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
|
self._attr_extra_state_attributes[
|
||||||
|
"valve_open_percent"
|
||||||
|
] = self.valve_open_percent
|
||||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||||
self._attr_extra_state_attributes["underlying_valve_0"] = (
|
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
|
||||||
self._underlyings[0].entity_id)
|
0
|
||||||
|
].entity_id
|
||||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
||||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||||
)
|
)
|
||||||
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
self._attr_extra_state_attributes["underlying_valve_2"] = (
|
||||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||||
)
|
)
|
||||||
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
self._attr_extra_state_attributes["underlying_valve_3"] = (
|
||||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"on_percent"
|
"on_percent"
|
||||||
] = self._prop_algorithm.on_percent
|
] = self._prop_algorithm.on_percent
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"on_time_sec"
|
"on_time_sec"
|
||||||
] = self._prop_algorithm.on_time_sec
|
] = self._prop_algorithm.on_time_sec
|
||||||
@@ -162,9 +180,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
under.set_valve_open_percent(
|
under.set_valve_open_percent()
|
||||||
self._prop_algorithm.on_percent
|
|
||||||
)
|
|
||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -185,4 +201,4 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
self,
|
self,
|
||||||
added_energy,
|
added_energy,
|
||||||
self._total_energy,
|
self._total_energy,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||||
|
from homeassistant.core import State
|
||||||
|
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
|
|
||||||
@@ -111,18 +112,18 @@ class UnderlyingEntity:
|
|||||||
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
||||||
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
||||||
async def turn_off(self):
|
async def turn_off(self):
|
||||||
""" Turn off the underlying equipement.
|
"""Turn off the underlying equipement.
|
||||||
Need to be overriden"""
|
Need to be overriden"""
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
async def turn_on(self):
|
async def turn_on(self):
|
||||||
""" Turn off the underlying equipement.
|
"""Turn off the underlying equipement.
|
||||||
Need to be overriden"""
|
Need to be overriden"""
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_inversed(self):
|
def is_inversed(self):
|
||||||
""" Tells if the switch command should be inversed"""
|
"""Tells if the switch command should be inversed"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_entity(self):
|
def remove_entity(self):
|
||||||
@@ -164,7 +165,11 @@ class UnderlyingEntity:
|
|||||||
"""Starting cycle for switch"""
|
"""Starting cycle for switch"""
|
||||||
|
|
||||||
def _cancel_cycle(self):
|
def _cancel_cycle(self):
|
||||||
""" Stops an eventual cycle """
|
"""Stops an eventual cycle"""
|
||||||
|
|
||||||
|
def cap_sent_value(self, value) -> float:
|
||||||
|
"""capping of the value send to the underlying eqt"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingSwitch(UnderlyingEntity):
|
class UnderlyingSwitch(UnderlyingEntity):
|
||||||
@@ -205,7 +210,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
@overrides
|
@overrides
|
||||||
@property
|
@property
|
||||||
def is_inversed(self):
|
def is_inversed(self):
|
||||||
""" Tells if the switch command should be inversed"""
|
"""Tells if the switch command should be inversed"""
|
||||||
return self._thermostat.is_inversed
|
return self._thermostat.is_inversed
|
||||||
|
|
||||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||||
@@ -227,14 +232,16 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
def is_device_active(self):
|
def is_device_active(self):
|
||||||
"""If the toggleable device is currently active."""
|
"""If the toggleable device is currently active."""
|
||||||
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||||
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
|
return (self.is_inversed and not real_state) or (
|
||||||
|
not self.is_inversed and real_state
|
||||||
|
)
|
||||||
|
|
||||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||||
async def turn_off(self):
|
async def turn_off(self):
|
||||||
"""Turn heater toggleable device off."""
|
"""Turn heater toggleable device off."""
|
||||||
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
||||||
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
# This may fails if called after shutdown
|
# This may fails if called after shutdown
|
||||||
try:
|
try:
|
||||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||||
@@ -250,7 +257,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
"""Turn heater toggleable device on."""
|
"""Turn heater toggleable device on."""
|
||||||
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
||||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
try:
|
try:
|
||||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
@@ -261,7 +268,6 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
except ServiceNotFound as err:
|
except ServiceNotFound as err:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
async def start_cycle(
|
async def start_cycle(
|
||||||
self,
|
self,
|
||||||
@@ -490,10 +496,14 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
def is_device_active(self):
|
def is_device_active(self):
|
||||||
"""If the toggleable device is currently active."""
|
"""If the toggleable device is currently active."""
|
||||||
if self.is_initialized:
|
if self.is_initialized:
|
||||||
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
|
return (
|
||||||
HVACAction.IDLE,
|
self._underlying_climate.hvac_mode != HVACMode.OFF
|
||||||
HVACAction.OFF,
|
and self._underlying_climate.hvac_action
|
||||||
]
|
not in [
|
||||||
|
HVACAction.IDLE,
|
||||||
|
HVACAction.OFF,
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -550,7 +560,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
return
|
return
|
||||||
data = {
|
data = {
|
||||||
ATTR_ENTITY_ID: self._entity_id,
|
ATTR_ENTITY_ID: self._entity_id,
|
||||||
"temperature": temperature,
|
"temperature": self.cap_sent_value(temperature),
|
||||||
"target_temp_high": max_temp,
|
"target_temp_high": max_temp,
|
||||||
"target_temp_low": min_temp,
|
"target_temp_low": min_temp,
|
||||||
}
|
}
|
||||||
@@ -664,6 +674,40 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
return None
|
return None
|
||||||
return self._underlying_climate.turn_aux_heat_off()
|
return self._underlying_climate.turn_aux_heat_off()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def cap_sent_value(self, value) -> float:
|
||||||
|
"""Try to adapt the target temp value to the min_temp / max_temp found
|
||||||
|
in the underlying entity (if any)"""
|
||||||
|
|
||||||
|
if not self.is_initialized:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Gets the min_temp and max_temp
|
||||||
|
if (
|
||||||
|
self._underlying_climate.min_temp is not None
|
||||||
|
and self._underlying_climate is not None
|
||||||
|
):
|
||||||
|
min_val = self._underlying_climate.min_temp
|
||||||
|
max_val = self._underlying_climate.max_temp
|
||||||
|
|
||||||
|
new_value = round(max(min_val, min(value, max_val)))
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||||
|
new_value = value
|
||||||
|
|
||||||
|
if new_value != value:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||||
|
self,
|
||||||
|
new_value,
|
||||||
|
value,
|
||||||
|
min_val,
|
||||||
|
max_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingValve(UnderlyingEntity):
|
class UnderlyingValve(UnderlyingEntity):
|
||||||
"""Represent a underlying switch"""
|
"""Represent a underlying switch"""
|
||||||
|
|
||||||
@@ -672,10 +716,7 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
_percent_open: int
|
_percent_open: int
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||||
hass: HomeAssistant,
|
|
||||||
thermostat: Any,
|
|
||||||
valve_entity_id: str
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the underlying switch"""
|
"""Initialize the underlying switch"""
|
||||||
|
|
||||||
@@ -689,13 +730,14 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
self._hvac_mode = None
|
self._hvac_mode = None
|
||||||
self._percent_open = self._thermostat.valve_open_percent
|
self._percent_open = self._thermostat.valve_open_percent
|
||||||
|
self._valve_entity_id = valve_entity_id
|
||||||
|
|
||||||
async def send_percent_open(self):
|
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
|
||||||
try:
|
try:
|
||||||
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
|
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
domain,
|
domain,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@@ -734,7 +776,7 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
# To test if real device is open but this is causing some side effect
|
# To test if real device is open but this is causing some side effect
|
||||||
# because the activation can be deferred -
|
# because the activation can be deferred -
|
||||||
# or float(self._hass.states.get(self._entity_id).state) > 0
|
# or float(self._hass.states.get(self._entity_id).state) > 0
|
||||||
except Exception: # pylint: disable=broad-exception-caught
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
@@ -750,9 +792,40 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
if force:
|
if force:
|
||||||
await self.send_percent_open()
|
await self.send_percent_open()
|
||||||
|
|
||||||
def set_valve_open_percent(self, percent):
|
@overrides
|
||||||
""" Update the valve open percent """
|
def cap_sent_value(self, value) -> float:
|
||||||
caped_val = self._thermostat.valve_open_percent
|
"""Try to adapt the open_percent value to the min / max found
|
||||||
|
in the underlying entity (if any)"""
|
||||||
|
|
||||||
|
# Gets the last number state
|
||||||
|
valve_state: State = self._hass.states.get(self._valve_entity_id)
|
||||||
|
if valve_state is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if "min" in valve_state.attributes and "max" in valve_state.attributes:
|
||||||
|
min_val = valve_state.attributes["min"]
|
||||||
|
max_val = valve_state.attributes["max"]
|
||||||
|
|
||||||
|
new_value = round(max(min_val, min(value, max_val)))
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||||
|
new_value = value
|
||||||
|
|
||||||
|
if new_value != value:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||||
|
self,
|
||||||
|
new_value,
|
||||||
|
value,
|
||||||
|
min_val,
|
||||||
|
max_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
def set_valve_open_percent(self):
|
||||||
|
"""Update the valve open percent"""
|
||||||
|
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||||
if self._percent_open == caped_val:
|
if self._percent_open == caped_val:
|
||||||
# No changes
|
# No changes
|
||||||
return
|
return
|
||||||
@@ -760,7 +833,9 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
self._percent_open = caped_val
|
self._percent_open = caped_val
|
||||||
# Send the new command to valve via a service call
|
# Send the new command to valve via a service call
|
||||||
|
|
||||||
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
|
_LOGGER.info(
|
||||||
|
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||||
|
)
|
||||||
# Send the change to the valve, in background
|
# Send the change to the valve, in background
|
||||||
self._hass.create_task(self.send_percent_open())
|
self._hass.create_task(self.send_percent_open())
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,14 @@ class MagicMockClimate(MagicMock):
|
|||||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||||
|
return 15
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||||
|
return 19
|
||||||
|
|
||||||
|
|
||||||
async def create_thermostat(
|
async def create_thermostat(
|
||||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
)
|
||||||
|
|
||||||
from .commons import *
|
from .commons import *
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
@@ -568,3 +572,136 @@ async def test_bug_101(
|
|||||||
)
|
)
|
||||||
assert entity.target_temperature == 12.75
|
assert entity.target_temperature == 12.75
|
||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_bug_272(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||||
|
)
|
||||||
|
|
||||||
|
# Min_temp is 15 and max_temp is 19
|
||||||
|
fake_underlying_climate = MagicMockClimate()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
|
return_value=fake_underlying_climate,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
|
) as mock_service_call:
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
|
"""Find my new entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
|
||||||
|
entity = find_my_entity("climate.theoverclimatemockname")
|
||||||
|
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate is True
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
assert entity.target_temperature == entity.min_temp
|
||||||
|
assert entity.is_regulated is True
|
||||||
|
|
||||||
|
assert mock_service_call.call_count == 0
|
||||||
|
|
||||||
|
# Set the hvac_mode to HEAT
|
||||||
|
entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
|
||||||
|
# In the accepted interval
|
||||||
|
await entity.async_set_temperature(temperature=17)
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 17,
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||||
|
# Set room temperature to something very cold
|
||||||
|
event_timestamp = now + timedelta(minutes=1)
|
||||||
|
|
||||||
|
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||||
|
|
||||||
|
# In the accepted interval
|
||||||
|
await entity.async_set_temperature(temperature=10)
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 15, # the minimum acceptable
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||||
|
# Set room temperature to something very cold
|
||||||
|
event_timestamp = now + timedelta(minutes=1)
|
||||||
|
|
||||||
|
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||||
|
|
||||||
|
# In the accepted interval
|
||||||
|
await entity.async_set_temperature(temperature=20)
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 19, # the maximum acceptable
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.components.climate import HVACAction, HVACMode
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
|
||||||
@@ -214,20 +214,60 @@ async def test_over_valve_full_start(
|
|||||||
assert entity.hvac_action == HVACAction.HEATING
|
assert entity.hvac_action == HVACAction.HEATING
|
||||||
|
|
||||||
# Change internal temperature
|
# Change internal temperature
|
||||||
|
expected_state = State(
|
||||||
|
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
|
||||||
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
) as mock_send_event, patch(
|
) as mock_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.get", return_value=0
|
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||||
):
|
):
|
||||||
event_timestamp = now - timedelta(minutes=3)
|
event_timestamp = now - timedelta(minutes=3)
|
||||||
await send_temperature_change_event(entity, 20, datetime.now())
|
await send_temperature_change_event(entity, 20, datetime.now())
|
||||||
assert entity.valve_open_percent == 0
|
assert entity.valve_open_percent == 0
|
||||||
assert entity.is_device_active is False
|
assert entity.is_device_active is True # Should be 0 but in fact 10 is send
|
||||||
assert entity.hvac_action == HVACAction.IDLE
|
assert (
|
||||||
|
entity.hvac_action == HVACAction.HEATING
|
||||||
|
) # Should be IDLE but heating due to 10
|
||||||
|
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": "number.mock_valve", "value": 10},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await send_temperature_change_event(entity, 17, datetime.now())
|
await send_temperature_change_event(entity, 17, datetime.now())
|
||||||
|
assert mock_service_call.call_count == 2
|
||||||
|
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{
|
||||||
|
"entity_id": "number.mock_valve",
|
||||||
|
"value": 10,
|
||||||
|
}, # the min allowed value
|
||||||
|
),
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{
|
||||||
|
"entity_id": "number.mock_valve",
|
||||||
|
"value": 50,
|
||||||
|
}, # the max allowed value
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
# switch to Eco
|
# switch to Eco
|
||||||
await entity.async_set_preset_mode(PRESET_ECO)
|
await entity.async_set_preset_mode(PRESET_ECO)
|
||||||
assert entity.preset_mode is PRESET_ECO
|
assert entity.preset_mode is PRESET_ECO
|
||||||
|
|||||||
Reference in New Issue
Block a user