Add ThermostatValve. All tests ok but test_valve
This commit is contained in:
@@ -24,7 +24,6 @@ from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_call_later,
|
||||
async_track_time_interval,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
@@ -63,10 +62,6 @@ from homeassistant.const import (
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
@@ -106,13 +101,6 @@ from .const import (
|
||||
CONF_TEMP_MAX,
|
||||
CONF_TEMP_MIN,
|
||||
HIDDEN_PRESETS,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
# CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_AC_MODE,
|
||||
UnknownEntity,
|
||||
EventType,
|
||||
@@ -121,7 +109,7 @@ from .const import (
|
||||
PRESET_AC_SUFFIX,
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingSwitch, UnderlyingClimate, UnderlyingEntity
|
||||
from .underlyings import UnderlyingEntity
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
@@ -257,43 +245,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
|
||||
|
||||
# Initialize underlying entities
|
||||
# Initialize underlying entities (will be done in subclasses)
|
||||
self._underlyings = []
|
||||
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
||||
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
for climate in [
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
]:
|
||||
if entry_infos.get(climate):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=entry_infos.get(climate),
|
||||
)
|
||||
)
|
||||
else:
|
||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
||||
if entry_infos.get(CONF_HEATER_2):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
||||
if entry_infos.get(CONF_HEATER_3):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
||||
if entry_infos.get(CONF_HEATER_4):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
self._underlyings.append(
|
||||
UnderlyingSwitch(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
)
|
||||
)
|
||||
|
||||
self._proportional_function = entry_infos.get(CONF_PROP_FUNCTION)
|
||||
self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR)
|
||||
@@ -419,7 +372,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._last_temperature_mesure = datetime.now(tz=self._current_tz)
|
||||
self._last_ext_temperature_mesure = datetime.now(tz=self._current_tz)
|
||||
self._security_state = False
|
||||
self._saved_hvac_mode = None
|
||||
|
||||
# Initiate the ProportionalAlgorithm
|
||||
if self._prop_algorithm is not None:
|
||||
@@ -472,22 +424,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
if self.is_over_climate:
|
||||
for climate in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [climate.entity_id], self._async_climate_changed
|
||||
)
|
||||
)
|
||||
else:
|
||||
for switch in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
@@ -707,18 +643,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
self.hass.create_task(self._check_switch_initial_state())
|
||||
# Start the control_heating
|
||||
# starts a cycle if we are in over_climate type
|
||||
if self.is_over_climate:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.hass.create_task(self._async_control_heating())
|
||||
|
||||
self.reset_last_change_time()
|
||||
|
||||
@@ -848,9 +772,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
if self.is_over_climate and self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
|
||||
return self._hvac_list
|
||||
|
||||
@property
|
||||
@@ -928,26 +849,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running hvac operation if supported.
|
||||
|
||||
Need to be one of CURRENT_HVAC_*.
|
||||
"""
|
||||
if self.is_over_climate:
|
||||
# if one not IDLE or OFF -> return it
|
||||
# else if one IDLE -> IDLE
|
||||
# else OFF
|
||||
one_idle = False
|
||||
for under in self._underlyings:
|
||||
if (action := under.hvac_action) not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]:
|
||||
return action
|
||||
if under.hvac_action == HVACAction.IDLE:
|
||||
one_idle = True
|
||||
if one_idle:
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return HVACAction.OFF
|
||||
if not self._is_device_active:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=too-many-lines
|
||||
# pylint: disable=invalid-name
|
||||
""" Implements the VersatileThermostat climate component """
|
||||
import logging
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
|
||||
from homeassistant.components.climate import HVACAction
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
from .const import CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4
|
||||
|
||||
from .underlyings import UnderlyingClimate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
|
||||
@@ -14,3 +26,74 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
def is_over_climate(self):
|
||||
""" True if the Thermostat is over_climate"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
""" Returns the current hvac_action by checking all hvac_action of the underlyings """
|
||||
|
||||
# if one not IDLE or OFF -> return it
|
||||
# else if one IDLE -> IDLE
|
||||
# else OFF
|
||||
one_idle = False
|
||||
for under in self._underlyings:
|
||||
if (action := under.hvac_action) not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]:
|
||||
return action
|
||||
if under.hvac_action == HVACAction.IDLE:
|
||||
one_idle = True
|
||||
if one_idle:
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
else:
|
||||
return super.hvac_modes
|
||||
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
for climate in [
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
]:
|
||||
if entry_infos.get(climate):
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
climate_entity_id=entry_infos.get(climate),
|
||||
)
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
_LOGGER.debug("Calling async_added_to_hass")
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
for climate in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [climate.entity_id], self._async_climate_changed
|
||||
)
|
||||
)
|
||||
|
||||
# Start the control_heating
|
||||
# starts a cycle
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
""" A climate over switch classe """
|
||||
|
||||
import logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import (
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
from .underlyings import UnderlyingSwitch
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
@@ -16,3 +27,42 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
def is_over_switch(self):
|
||||
""" True if the Thermostat is over_switch"""
|
||||
return True
|
||||
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
||||
if entry_infos.get(CONF_HEATER_2):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
||||
if entry_infos.get(CONF_HEATER_3):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
||||
if entry_infos.get(CONF_HEATER_4):
|
||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
||||
|
||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
self._underlyings.append(
|
||||
UnderlyingSwitch(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
_LOGGER.debug("Calling async_added_to_hass")
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
for switch in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
|
||||
self.hass.create_task(self._async_control_heating())
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4
|
||||
|
||||
from .underlyings import UnderlyingValve
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ThermostatOverValve(BaseThermostat):
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
@@ -14,3 +27,220 @@ class ThermostatOverValve(BaseThermostat):
|
||||
def is_over_valve(self):
|
||||
""" True if the Thermostat is over_valve"""
|
||||
return True
|
||||
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
||||
if entry_infos.get(CONF_VALVE_2):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
||||
if entry_infos.get(CONF_VALVE_3):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_3))
|
||||
if entry_infos.get(CONF_VALVE_4):
|
||||
lst_valves.append(entry_infos.get(CONF_VALVE_4))
|
||||
|
||||
for valve in enumerate(lst_valves):
|
||||
self._underlyings.append(
|
||||
UnderlyingValve(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
valve_entity_id=valve
|
||||
)
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
_LOGGER.debug("Calling async_added_to_hass")
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener to all underlying entities
|
||||
for valve in self._underlyings:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [valve.entity_id], self._async_valve_changed
|
||||
)
|
||||
)
|
||||
|
||||
# Start the control_heating
|
||||
# starts a cycle
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_control_heating,
|
||||
interval=timedelta(minutes=self._cycle_min),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
async def _async_valve_changed(self, event):
|
||||
"""Handle unerdlying valve 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()
|
||||
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
changes = False
|
||||
new_hvac_mode = new_state.state
|
||||
|
||||
old_state = event.data.get("old_state")
|
||||
old_hvac_action = (
|
||||
old_state.attributes.get("hvac_action")
|
||||
if old_state and old_state.attributes
|
||||
else None
|
||||
)
|
||||
new_hvac_action = (
|
||||
new_state.attributes.get("hvac_action")
|
||||
if new_state and new_state.attributes
|
||||
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
|
||||
)
|
||||
|
||||
# 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
|
||||
# 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")
|
||||
# new_hvac_mode = HVACMode.OFF
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||
self,
|
||||
new_hvac_mode,
|
||||
self._hvac_mode,
|
||||
new_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,
|
||||
)
|
||||
|
||||
# Interpretation of hvac action
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.COOLING,
|
||||
HVACAction.DRYING,
|
||||
HVACAction.FAN,
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
|
||||
self._underlying_climate_start_hvac_action_date = (
|
||||
self.get_last_updated_date_or_now(new_state)
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - underlying just switch ON. Set power and energy start date %s",
|
||||
self,
|
||||
self._underlying_climate_start_hvac_action_date.isoformat(),
|
||||
)
|
||||
changes = True
|
||||
|
||||
if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON:
|
||||
stop_power_date = self.get_last_updated_date_or_now(new_state)
|
||||
if self._underlying_climate_start_hvac_action_date:
|
||||
delta = (
|
||||
stop_power_date - self._underlying_climate_start_hvac_action_date
|
||||
)
|
||||
self._underlying_climate_delta_t = delta.total_seconds() / 3600.0
|
||||
|
||||
# increment energy at the end of the cycle
|
||||
self.incremente_energy()
|
||||
|
||||
self._underlying_climate_start_hvac_action_date = None
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - underlying just switch OFF at %s. delta_h=%.3f h",
|
||||
self,
|
||||
stop_power_date.isoformat(),
|
||||
self._underlying_climate_delta_t,
|
||||
)
|
||||
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)
|
||||
changes = True
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
|
||||
""" Underlying entities classes """
|
||||
import logging
|
||||
from typing import Any
|
||||
@@ -42,6 +44,9 @@ class UnderlyingEntityType(StrEnum):
|
||||
# a climate
|
||||
CLIMATE = "climate"
|
||||
|
||||
# a valve
|
||||
VALVE = "valve"
|
||||
|
||||
|
||||
class UnderlyingEntity:
|
||||
"""Represent a underlying device which could be a switch or a climate"""
|
||||
@@ -626,3 +631,182 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.turn_aux_heat_off()
|
||||
|
||||
class UnderlyingValve(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
|
||||
_hvac_mode: HVACMode
|
||||
# The percentage of opening the valve
|
||||
_percent_open: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
valve_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
thermostat=thermostat,
|
||||
entity_type=UnderlyingEntityType.VALVE,
|
||||
entity_id=valve_entity_id,
|
||||
)
|
||||
self._async_cancel_cycle = None
|
||||
self._should_relaunch_control_heating = False
|
||||
self._hvac_mode = None
|
||||
self._percent_open = 0
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode != hvac_mode:
|
||||
self._hvac_mode = hvac_mode
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
try:
|
||||
return float(self._hass.states.get(self._entity_id)) > 0
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
return False
|
||||
|
||||
async def start_cycle(
|
||||
self,
|
||||
hvac_mode: HVACMode,
|
||||
force=False,
|
||||
):
|
||||
"""Starting cycle for switch"""
|
||||
_LOGGER.debug(
|
||||
"%s - Starting new cycle hvac_mode=%s percent_open=%d force=%s",
|
||||
self,
|
||||
hvac_mode,
|
||||
self._percent_open,
|
||||
force,
|
||||
)
|
||||
|
||||
self._hvac_mode = hvac_mode
|
||||
|
||||
# Cancel eventual previous cycle if any
|
||||
if self._async_cancel_cycle is not None:
|
||||
if force:
|
||||
_LOGGER.debug("%s - we force a new cycle", self)
|
||||
self._cancel_cycle()
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - A previous cycle is alredy running and no force -> waits for its end",
|
||||
self,
|
||||
)
|
||||
# self._should_relaunch_control_heating = True
|
||||
_LOGGER.debug("%s - End of cycle (2)", self)
|
||||
return
|
||||
|
||||
# If we should heat, starts the cycle with delay
|
||||
if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and self._percent_open > 0:
|
||||
# Starts the cycle after the initial delay
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass, 0, self._turn_on_later
|
||||
)
|
||||
_LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle)
|
||||
|
||||
# if we not heat but device is active
|
||||
elif self.is_device_active:
|
||||
_LOGGER.info(
|
||||
"%s - stop heating (2)",
|
||||
self,
|
||||
)
|
||||
await self.turn_off()
|
||||
else:
|
||||
_LOGGER.debug("%s - nothing to do", self)
|
||||
|
||||
def _cancel_cycle(self):
|
||||
"""Cancel the cycle"""
|
||||
if self._async_cancel_cycle:
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
_LOGGER.debug("%s - Stopping cycle during calculation", self)
|
||||
|
||||
async def _turn_on_later(self, _):
|
||||
"""Turn the heater on after a delay"""
|
||||
_LOGGER.debug(
|
||||
"%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s",
|
||||
self,
|
||||
self._hvac_mode,
|
||||
self._should_relaunch_control_heating,
|
||||
)
|
||||
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
if await self._thermostat.check_overpowering():
|
||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||
return
|
||||
# Security mode could have change the on_time percent
|
||||
await self._thermostat.check_security()
|
||||
|
||||
action_label = "start"
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - %s heating",
|
||||
self,
|
||||
action_label,
|
||||
)
|
||||
await self.turn_on()
|
||||
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass,
|
||||
0,
|
||||
self._turn_off_later,
|
||||
)
|
||||
|
||||
async def _turn_off_later(self, _):
|
||||
"""Turn the heater off and call the next cycle after the delay"""
|
||||
_LOGGER.debug(
|
||||
"%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s",
|
||||
self,
|
||||
self._hvac_mode,
|
||||
self._should_relaunch_control_heating,
|
||||
)
|
||||
self._cancel_cycle()
|
||||
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
action_label = "stop"
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - %s heating",
|
||||
self,
|
||||
action_label
|
||||
)
|
||||
await self.turn_off()
|
||||
|
||||
self._async_cancel_cycle = self.call_later(
|
||||
self._hass,
|
||||
0,
|
||||
self._turn_on_later
|
||||
)
|
||||
|
||||
# increment energy at the end of the cycle
|
||||
self._thermostat.incremente_energy()
|
||||
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
|
||||
@@ -30,21 +30,39 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MOTION_SENSOR: "input_boolean.motion_sensor",
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_DELAY: 10,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: PRESET_COMFORT,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
CONF_POWER_SENSOR: "sensor.power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||
PRESET_ECO + PRESET_AWAY_SUFFIX + "_temp": 17.1,
|
||||
PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17.2,
|
||||
PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 17.3,
|
||||
CONF_PRESET_POWER: 10,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
# CONF_DEVICE_POWER: 100,
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_AC_MODE: False
|
||||
},
|
||||
)
|
||||
|
||||
@@ -76,10 +94,10 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st
|
||||
assert entity.is_over_switch is False
|
||||
assert entity.is_over_valve is True
|
||||
assert entity.ac_mode is False
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.hvac_modes == [HVACMode.COOL, HVACMode.OFF]
|
||||
assert entity.target_temperature == entity.max_temp
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.OFF]
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
|
||||
Reference in New Issue
Block a user