First commit not completed
This commit is contained in:
@@ -4,8 +4,6 @@ import logging
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
# from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -23,7 +21,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
|
||||
@@ -40,7 +37,6 @@ from homeassistant.helpers import (
|
||||
) # , config_validation as cv
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
ATTR_PRESET_MODE,
|
||||
# ATTR_FAN_MODE,
|
||||
HVACMode,
|
||||
@@ -57,14 +53,6 @@ from homeassistant.components.climate import (
|
||||
PRESET_NONE,
|
||||
# PRESET_SLEEP,
|
||||
ClimateEntityFeature,
|
||||
# ClimateEntityFeature.PRESET_MODE,
|
||||
# SUPPORT_TARGET_TEMPERATURE,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
# SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
# from homeassistant.components.climate import (
|
||||
@@ -86,7 +74,6 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
@@ -97,6 +84,9 @@ from .const import (
|
||||
PLATFORMS,
|
||||
DEVICE_MANUFACTURER,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
@@ -146,6 +136,8 @@ from .const import (
|
||||
ATTR_TOTAL_ENERGY,
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingSwitch, UnderlyingClimate, UnderlyingEntity
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
|
||||
@@ -212,6 +204,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
# The list of VersatileThermostat entities
|
||||
# No more needed
|
||||
# _registry: dict[str, object] = {}
|
||||
_hass: HomeAssistant
|
||||
_last_temperature_mesure: datetime
|
||||
_last_ext_temperature_mesure: datetime
|
||||
_total_energy: float
|
||||
@@ -219,8 +212,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_window_state: bool
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_security_state: bool
|
||||
_window_auto_state: bool
|
||||
_underlyings: list[UnderlyingEntity]
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
@@ -264,14 +257,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._security_state = None
|
||||
|
||||
self._thermostat_type = None
|
||||
self._heater_entity_id = None
|
||||
self._climate_entity_id = None
|
||||
self._is_over_climate = False
|
||||
# TODO should be delegated to underlying climate
|
||||
self._heater_entity_id = None
|
||||
# self._climate_entity_id = None
|
||||
self._underlying_climate = None
|
||||
|
||||
self._attr_translation_key = "versatile_thermostat"
|
||||
|
||||
self._total_energy = None
|
||||
|
||||
# TODO should be delegated to underlying climate
|
||||
self._underlying_climate_start_hvac_action_date = None
|
||||
self._underlying_climate_delta_t = 0
|
||||
|
||||
@@ -286,18 +282,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
self.post_init(entry_infos)
|
||||
self._underlyings = []
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._unique_id)},
|
||||
name=self._name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
self.post_init(entry_infos)
|
||||
|
||||
def post_init(self, entry_infos):
|
||||
"""Finish the initialization of the thermostast"""
|
||||
@@ -335,16 +322,40 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
# Exploit usable attributs
|
||||
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
|
||||
|
||||
# Initialize underlying entities
|
||||
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
|
||||
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
self._is_over_climate = True
|
||||
self._climate_entity_id = entry_infos.get(CONF_CLIMATE)
|
||||
self._underlyings.append(
|
||||
UnderlyingClimate(
|
||||
hass=self._hass,
|
||||
thermostat_name=str(self),
|
||||
climate_entity_id=entry_infos.get(CONF_CLIMATE),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._heater_entity_id = entry_infos.get(CONF_HEATER)
|
||||
self._is_over_climate = False
|
||||
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)
|
||||
self._underlyings = []
|
||||
for idx, switch in enumerate(lst_switches):
|
||||
self._underlyings.append(
|
||||
UnderlyingSwitch(
|
||||
hass=self._hass,
|
||||
thermostat_name=str(self),
|
||||
switch_entity_id=switch,
|
||||
initial_delay_sec=idx * delta_cycle,
|
||||
)
|
||||
)
|
||||
|
||||
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
|
||||
self._proportional_function = entry_infos.get(CONF_PROP_FUNCTION)
|
||||
self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR)
|
||||
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
|
||||
@@ -523,19 +534,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Add listener
|
||||
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._climate_entity_id], self._async_climate_changed
|
||||
# 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:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._heater_entity_id], self._async_switch_changed
|
||||
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(
|
||||
@@ -623,14 +636,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
|
||||
def find_underlying_climate(self, climate_entity_id) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if climate_entity_id == entity.entity_id:
|
||||
return entity
|
||||
return None
|
||||
|
||||
async def async_startup(self):
|
||||
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
||||
_LOGGER.debug("%s - Calling async_startup", self)
|
||||
@@ -640,28 +645,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||
need_write_state = False
|
||||
|
||||
# Get the underlying thermostat
|
||||
if self._is_over_climate:
|
||||
self._underlying_climate = self.find_underlying_climate(
|
||||
self._climate_entity_id
|
||||
)
|
||||
if self._underlying_climate:
|
||||
_LOGGER.info(
|
||||
"%s - The underlying climate entity: %s have been succesfully found",
|
||||
self,
|
||||
self._underlying_climate,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||
self,
|
||||
self._climate_entity_id,
|
||||
)
|
||||
# #56 keep the over_climate and try periodically to find the underlying climate
|
||||
# self._is_over_climate = False
|
||||
raise UnknownEntity(
|
||||
f"Underlying thermostat {self._climate_entity_id} not found"
|
||||
)
|
||||
# Initialize all UnderlyingEntities
|
||||
for under in self._underlyings:
|
||||
under.startup()
|
||||
|
||||
temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
|
||||
if temperature_state and temperature_state.state not in (
|
||||
@@ -888,6 +874,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._unique_id)},
|
||||
name=self._name,
|
||||
manufacturer=DEVICE_MANUFACTURER,
|
||||
model=DOMAIN,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self._unique_id
|
||||
@@ -1000,17 +997,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self._is_over_climate:
|
||||
if self._underlying_climate:
|
||||
return self._underlying_climate.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return self._hass.states.is_state(self._heater_entity_id, STATE_ON)
|
||||
"""Returns true if one underlying is active"""
|
||||
for under in self._underlyings:
|
||||
if under.is_device_active():
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -1156,6 +1147,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"""True if the Window auto feature is enabled"""
|
||||
return self._window_auto_on
|
||||
|
||||
@property
|
||||
def nb_underlying_entities(self) -> int:
|
||||
"""Returns the number of underlying entities"""
|
||||
return len(self._underlyings)
|
||||
|
||||
def underlying_entity_id(self, index=0) -> str | None:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
return self.underlying_entity(index).entity_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def underlying_entity(self, index=0) -> UnderlyingEntity | None:
|
||||
"""Get the underlying entity at specified index"""
|
||||
if index < self.nb_underlying_entities:
|
||||
return self._underlyings[index]
|
||||
else:
|
||||
return None
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
@@ -1191,28 +1201,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if hvac_mode is None:
|
||||
return
|
||||
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
data = {ATTR_ENTITY_ID: self._climate_entity_id, "hvac_mode": hvac_mode}
|
||||
await self.hass.services.async_call(
|
||||
CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, context=self._context
|
||||
)
|
||||
# await self._underlying_climate.async_set_hvac_mode(hvac_mode)
|
||||
self._hvac_mode = hvac_mode # self._underlying_climate.hvac_mode
|
||||
else:
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
self._hvac_mode = HVACMode.HEAT
|
||||
await self._async_control_heating(force=True)
|
||||
elif hvac_mode == HVACMode.COOL:
|
||||
self._hvac_mode = HVACMode.COOL
|
||||
await self._async_control_heating(force=True)
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
if self._is_device_active:
|
||||
await self._async_underlying_entity_turn_off()
|
||||
await self._async_control_heating(force=True)
|
||||
else:
|
||||
_LOGGER.error("Unrecognized hvac mode: %s", hvac_mode)
|
||||
return
|
||||
# Delegate to all underlying
|
||||
for under in self._underlyings:
|
||||
await under.set_have_mode(hvac_mode)
|
||||
|
||||
self._hvac_mode = hvac_mode
|
||||
await self._async_control_heating(force=True)
|
||||
|
||||
# Ensure we update the current operation after changing the mode
|
||||
self.reset_last_temperature_time()
|
||||
|
||||
@@ -1304,52 +1299,32 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
if fan_mode is None:
|
||||
if fan_mode is None or not self._is_over_climate:
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_fan_mode(fan_mode)
|
||||
self._fan_mode = fan_mode
|
||||
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._climate_entity_id,
|
||||
"fan_mode": fan_mode,
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, data, context=self._context
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_humidity(self, humidity: int):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
if humidity is None:
|
||||
if humidity is None or not self._is_over_climate:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_humidity(humidity)
|
||||
self._humidity = humidity
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._climate_entity_id,
|
||||
"humidity": humidity,
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, data, context=self._context
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
if swing_mode is None:
|
||||
if swing_mode is None or not self._is_over_climate:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_swing_mode(swing_mode)
|
||||
self._swing_mode = swing_mode
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._climate_entity_id,
|
||||
"swing_mode": swing_mode,
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, data, context=self._context
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
@@ -1366,16 +1341,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
self._target_temp = temperature
|
||||
if self._is_over_climate and self._underlying_climate:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._climate_entity_id,
|
||||
"temperature": temperature,
|
||||
"target_temp_high": self._attr_max_temp,
|
||||
"target_temp_low": self._attr_min_temp,
|
||||
}
|
||||
if not self._is_over_climate:
|
||||
return
|
||||
|
||||
await self.hass.services.async_call(
|
||||
CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, context=self._context
|
||||
for under in self._underlyings:
|
||||
await under.set_temperature(
|
||||
temperature, self._attr_max_temp, self._attr_min_temp
|
||||
)
|
||||
|
||||
def get_state_date_or_now(self, state: State):
|
||||
@@ -1865,22 +1836,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
async def _async_underlying_entity_turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
if not self._is_over_climate:
|
||||
_LOGGER.debug(
|
||||
"%s - Stopping underlying switch %s", self, self._heater_entity_id
|
||||
)
|
||||
data = {ATTR_ENTITY_ID: self._heater_entity_id}
|
||||
await self.hass.services.async_call(
|
||||
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - Stopping underlying switch %s", self, self._climate_entity_id
|
||||
)
|
||||
data = {ATTR_ENTITY_ID: self._climate_entity_id}
|
||||
await self.hass.services.async_call(
|
||||
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
|
||||
)
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.turn_off()
|
||||
|
||||
async def _async_manage_window_auto(self):
|
||||
"""The management of the window auto feature"""
|
||||
@@ -2039,7 +1997,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
ret = self._current_power + self._device_power >= self._current_power_max
|
||||
if not self._overpowering_state and ret and not self._hvac_mode == HVACMode.OFF:
|
||||
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
self,
|
||||
@@ -2241,15 +2199,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
# Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it
|
||||
if self._is_over_climate and self._underlying_climate is None:
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate is not initialized. Try to initialize it", self
|
||||
)
|
||||
try:
|
||||
await self.async_startup()
|
||||
except UnknownEntity as err:
|
||||
# still not found, we an stop here
|
||||
raise err
|
||||
for under in self._underlyings:
|
||||
if not under.is_initialized:
|
||||
_LOGGER.info(
|
||||
"%s - Underlying %s is not initialized. Try to initialize it",
|
||||
self,
|
||||
under.entity_id,
|
||||
)
|
||||
try:
|
||||
under.startup()
|
||||
except UnknownEntity as err:
|
||||
# still not found, we an stop here
|
||||
raise err
|
||||
|
||||
# Check overpowering condition
|
||||
overpowering: bool = await self.check_overpowering()
|
||||
|
||||
@@ -19,7 +19,7 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
_my_climate: VersatileThermostat
|
||||
hass: HomeAssistant
|
||||
_config_id: str
|
||||
_devince_name: str
|
||||
_device_name: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
|
||||
"""The CTOR"""
|
||||
|
||||
@@ -41,6 +41,9 @@ from .const import (
|
||||
DOMAIN,
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_EXTERNAL_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
@@ -224,6 +227,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
|
||||
),
|
||||
),
|
||||
vol.Required(
|
||||
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
|
||||
): vol.In(
|
||||
|
||||
@@ -30,6 +30,9 @@ DOMAIN = "versatile_thermostat"
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
CONF_HEATER = "heater_entity_id"
|
||||
CONF_HEATER_2 = "heater_entity2_id"
|
||||
CONF_HEATER_3 = "heater_entity3_id"
|
||||
CONF_HEATER_4 = "heater_entity4_id"
|
||||
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
|
||||
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
|
||||
CONF_POWER_SENSOR = "power_sensor_entity_id"
|
||||
|
||||
@@ -19,11 +19,14 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from ..climate import VersatileThermostat
|
||||
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from ..underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
MOCK_PRESETS_CONFIG,
|
||||
@@ -59,6 +62,18 @@ PARTIAL_CLIMATE_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_4SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_WINDOW_CONFIG
|
||||
| MOCK_MOTION_CONFIG
|
||||
| MOCK_POWER_CONFIG
|
||||
| MOCK_PRESENCE_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
|
||||
class MockClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
@@ -87,6 +87,15 @@ def skip_control_heating_fixture():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="skip_find_underlying_climate")
|
||||
def skip_find_underlying_climate_fixture():
|
||||
"""Skip the find_underlying_climate of VersatileThermostat"""
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate"
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="skip_hass_states_is_state")
|
||||
def skip_hass_states_is_state_fixture():
|
||||
"""Skip the is_state in HomeAssistant"""
|
||||
|
||||
@@ -9,6 +9,9 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i
|
||||
from custom_components.versatile_thermostat.const import (
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_HEATER_2,
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
@@ -62,6 +65,21 @@ MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
@@ -79,6 +97,14 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_4switch0",
|
||||
CONF_HEATER_2: "switch.mock_4switch1",
|
||||
CONF_HEATER_3: "switch.mock_4switch2",
|
||||
CONF_HEATER_4: "switch.mock_4switch3",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
|
||||
@@ -442,7 +442,7 @@ async def test_binary_sensors_over_climate_minimal(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
|
||||
@@ -18,7 +18,7 @@ async def test_bug_56(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -70,7 +70,7 @@ async def test_bug_56(
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call _async_control_heating
|
||||
|
||||
@@ -369,3 +369,92 @@ async def test_user_config_flow_window_auto_ko(
|
||||
assert result["errors"] == {
|
||||
"window_sensor_entity_id": "window_open_detection_method"
|
||||
}
|
||||
|
||||
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
|
||||
):
|
||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||
|
||||
SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=SOURCE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=TYPE_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== SOURCE_CONFIG
|
||||
| TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
assert result["result"].title == "TheOver4SwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
@@ -357,7 +357,7 @@ async def test_power_management_energy_over_climate(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_sensors_over_climate(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
@@ -324,7 +324,7 @@ async def test_sensors_over_climate_minimal(
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying,
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
|
||||
@@ -91,7 +91,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entry.add_to_hass(hass)
|
||||
@@ -139,7 +139,76 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call("climate.mock_climate")
|
||||
mock_find_climate.assert_has_calls(
|
||||
[call.find_underlying_entity("climate.mock_climate")]
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
|
||||
async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch with 4 switches type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOver4SwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data=FULL_4SWITCH_CONFIG,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
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: VersatileThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOver4SwitchMockName"
|
||||
assert entity._is_over_climate is False
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
PRESET_ACTIVITY,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
# Checks that we have the 4 UnderlyingEntity correctly configured
|
||||
for idx in range(4):
|
||||
under = entity.underlying_entity(idx)
|
||||
assert under is not None
|
||||
assert isinstance(under, UnderlyingSwitch)
|
||||
assert under.entity_id == "switch.mock_4switch" + str(idx)
|
||||
assert under.initial_delay_sec == 8 * 60 / 4 * idx
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
281
custom_components/versatile_thermostat/underlyings.py
Normal file
281
custom_components/versatile_thermostat/underlyings.py
Normal file
@@ -0,0 +1,281 @@
|
||||
""" Underlying entities classes """
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
||||
|
||||
from homeassistant.backports.enum import StrEnum
|
||||
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
HVACMode,
|
||||
HVACAction,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from .const import UnknownEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnderlyingEntityType(StrEnum):
|
||||
"""All underlying device type"""
|
||||
|
||||
# A switch
|
||||
SWITCH = "switch"
|
||||
|
||||
# a climate
|
||||
CLIMATE = "climate"
|
||||
|
||||
|
||||
class UnderlyingEntity:
|
||||
"""Represent a underlying device which could be a switch or a climate"""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_thermostat_name: str
|
||||
_entity_id: str
|
||||
_type: UnderlyingEntityType
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat_name: str,
|
||||
entity_type: UnderlyingEntityType,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the underlying entity"""
|
||||
self._hass = hass
|
||||
self._thermostat_name = thermostat_name
|
||||
self._type = entity_type
|
||||
self._entity_id = entity_id
|
||||
|
||||
def __str__(self):
|
||||
return self._thermostat_name
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""The entiy id represented by this class"""
|
||||
return self._entity_id
|
||||
|
||||
@property
|
||||
def entity_type(self) -> UnderlyingEntityType:
|
||||
"""The entity type represented by this class"""
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""True if the underlying is initialized"""
|
||||
return True
|
||||
|
||||
def startup(self):
|
||||
"""Startup the Entity"""
|
||||
return
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode):
|
||||
"""Set the HVACmode"""
|
||||
return
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool | None:
|
||||
"""If the toggleable device is currently active."""
|
||||
return None
|
||||
|
||||
async def turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
_LOGGER.debug("%s - Stopping underlying switch %s", self, self._entity_id)
|
||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||
await self._hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
data, # TODO needed ? context=self._context
|
||||
)
|
||||
|
||||
async def set_temperature(self, temperature, max_temp, min_temp):
|
||||
"""Set the target temperature"""
|
||||
return
|
||||
|
||||
|
||||
class UnderlyingSwitch(UnderlyingEntity):
|
||||
"""Represent a underlying switch"""
|
||||
|
||||
_initialDelaySec: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat_name: str,
|
||||
switch_entity_id: str,
|
||||
initial_delay_sec: int,
|
||||
) -> None:
|
||||
"""Initialize the underlying switch"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
thermostat_name=thermostat_name,
|
||||
entity_type=UnderlyingEntityType.SWITCH,
|
||||
entity_id=switch_entity_id,
|
||||
)
|
||||
self._initial_delay_sec = initial_delay_sec
|
||||
|
||||
@property
|
||||
def initial_delay_sec(self):
|
||||
"""The initial delay for this class"""
|
||||
return self._initial_delay_sec
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode):
|
||||
"""Set the HVACmode"""
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
if self.is_device_active:
|
||||
await self.turn_off()
|
||||
return
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
return self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||
|
||||
|
||||
class UnderlyingClimate(UnderlyingEntity):
|
||||
"""Represent a underlying climate"""
|
||||
|
||||
_initialDelaySec: int
|
||||
_underlying_climate: ClimateEntity
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, thermostat_name: str, climate_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the underlying climate"""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
thermostat_name=thermostat_name,
|
||||
entity_type=UnderlyingEntityType.CLIMATE,
|
||||
entity_id=climate_entity_id,
|
||||
)
|
||||
|
||||
def find_underlying_climate(self) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
component: EntityComponent[ClimateEntity] = self._hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if self.entity_id == entity.entity_id:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def startup(self):
|
||||
"""Startup the Entity"""
|
||||
# Get the underlying climate
|
||||
self._underlying_climate = self.find_underlying_climate()
|
||||
if self._underlying_climate:
|
||||
_LOGGER.info(
|
||||
"%s - The underlying climate entity: %s have been succesfully found",
|
||||
self,
|
||||
self._underlying_climate,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
|
||||
self,
|
||||
self.entity_id,
|
||||
)
|
||||
# #56 keep the over_climate and try periodically to find the underlying climate
|
||||
# self._is_over_climate = False
|
||||
raise UnknownEntity(f"Underlying entity {self.entity_id} not found")
|
||||
return
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""True if the underlying climate was found"""
|
||||
return self._underlying_climate is not None
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode):
|
||||
"""Set the HVACmode of the underlying climate"""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
|
||||
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
data, # TODO Needed ?, context=self._context
|
||||
)
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
if self.is_initialized:
|
||||
return self._underlying_climate.hvac_action not in [
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
]
|
||||
else:
|
||||
return None
|
||||
|
||||
async def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"fan_mode": fan_mode,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
data, # TODO needed ? context=self._context
|
||||
)
|
||||
|
||||
async def set_humidity(self, humidity: int):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"humidity": humidity,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
data, # TODO needed ? context=self._context
|
||||
)
|
||||
|
||||
async def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"swing_mode": swing_mode,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
data, # TODO needed ? context=self._context
|
||||
)
|
||||
|
||||
async def set_temperature(self, temperature, max_temp, min_temp):
|
||||
"""Set the target temperature"""
|
||||
if not self.is_initialized:
|
||||
return
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": temperature,
|
||||
"target_temp_high": max_temp,
|
||||
"target_temp_low": min_temp,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
data, # TODO needed ? context=self._context
|
||||
)
|
||||
Reference in New Issue
Block a user