Release 3.2 beta1 - an error in multi-testu
This commit is contained in:
@@ -54,11 +54,17 @@ input_boolean:
|
|||||||
fake_heater_switch1:
|
fake_heater_switch1:
|
||||||
name: Heater 1 (Linear)
|
name: Heater 1 (Linear)
|
||||||
icon: mdi:radiator
|
icon: mdi:radiator
|
||||||
fake_heater_switch2:
|
fake_heater_4switch1:
|
||||||
name: Heater (TPI with presence preset)
|
name: Heater (multiswitch1)
|
||||||
icon: mdi:radiator
|
icon: mdi:radiator
|
||||||
fake_heater_switch3:
|
fake_heater_4switch2:
|
||||||
name: Heater (TPI with offset)
|
name: Heater (multiswitch2)
|
||||||
|
icon: mdi:radiator
|
||||||
|
fake_heater_4switch3:
|
||||||
|
name: Heater (multiswitch3)
|
||||||
|
icon: mdi:radiator
|
||||||
|
fake_heater_4switch4:
|
||||||
|
name: Heater (multiswitch4)
|
||||||
icon: mdi:radiator
|
icon: mdi:radiator
|
||||||
# input_boolean to simulate the motion sensor entity. Only for development environment.
|
# input_boolean to simulate the motion sensor entity. Only for development environment.
|
||||||
fake_motion_sensor1:
|
fake_motion_sensor1:
|
||||||
|
|||||||
@@ -140,8 +140,7 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# TODO remove this
|
# _LOGGER.setLevel(logging.DEBUG)
|
||||||
_LOGGER.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -258,16 +257,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self._thermostat_type = None
|
self._thermostat_type = None
|
||||||
self._is_over_climate = False
|
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._attr_translation_key = "versatile_thermostat"
|
||||||
|
|
||||||
self._total_energy = None
|
self._total_energy = None
|
||||||
|
|
||||||
# TODO should be delegated to underlying climate
|
# because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity
|
||||||
self._underlying_climate_start_hvac_action_date = None
|
self._underlying_climate_start_hvac_action_date = None
|
||||||
self._underlying_climate_delta_t = 0
|
self._underlying_climate_delta_t = 0
|
||||||
|
|
||||||
@@ -327,7 +322,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
hass=self._hass,
|
hass=self._hass,
|
||||||
thermostat_name=str(self),
|
thermostat=self,
|
||||||
climate_entity_id=entry_infos.get(CONF_CLIMATE),
|
climate_entity_id=entry_infos.get(CONF_CLIMATE),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -346,7 +341,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingSwitch(
|
UnderlyingSwitch(
|
||||||
hass=self._hass,
|
hass=self._hass,
|
||||||
thermostat_name=str(self),
|
thermostat=self,
|
||||||
switch_entity_id=switch,
|
switch_entity_id=switch,
|
||||||
initial_delay_sec=idx * delta_cycle,
|
initial_delay_sec=idx * delta_cycle,
|
||||||
)
|
)
|
||||||
@@ -614,16 +609,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Ingore this error which is possible if underlying climate is not found temporary
|
# Ingore this error which is possible if underlying climate is not found temporary
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 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),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def async_remove_thermostat(self):
|
def async_remove_thermostat(self):
|
||||||
"""Called when the thermostat will be removed"""
|
"""Called when the thermostat will be removed"""
|
||||||
_LOGGER.info("%s - Removing thermostat", self)
|
_LOGGER.info("%s - Removing thermostat", self)
|
||||||
@@ -767,7 +752,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.hass.create_task(self._check_switch_initial_state())
|
self.hass.create_task(self._check_switch_initial_state())
|
||||||
self.hass.create_task(self._async_control_heating())
|
# 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())
|
||||||
|
|
||||||
await self.get_my_previous_state()
|
await self.get_my_previous_state()
|
||||||
|
|
||||||
@@ -880,8 +876,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def hvac_modes(self):
|
def hvac_modes(self):
|
||||||
"""List of available operation modes."""
|
"""List of available operation modes."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.hvac_modes
|
return self.underlying_entity(0).hvac_modes
|
||||||
|
|
||||||
return self._hvac_list
|
return self._hvac_list
|
||||||
|
|
||||||
@@ -891,8 +887,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.FAN_MODE.
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.fan_mode
|
return self.underlying_entity(0).fan_mode
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -902,8 +898,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.FAN_MODE.
|
Requires ClimateEntityFeature.FAN_MODE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.fan_modes
|
return self.underlying_entity(0).fan_modes
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -913,8 +909,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.SWING_MODE.
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.swing_mode
|
return self.underlying_entity(0).swing_mode
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -924,16 +920,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.SWING_MODE.
|
Requires ClimateEntityFeature.SWING_MODE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.swing_modes
|
return self.underlying_entity(0).swing_modes
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self):
|
def temperature_unit(self) -> str:
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.temperature_unit
|
return self.underlying_entity(0).temperature_unit
|
||||||
|
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
@@ -984,8 +980,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.supported_features | self._support_flags
|
return self.underlying_entity(0).supported_features | self._support_flags
|
||||||
|
|
||||||
return self._support_flags
|
return self._support_flags
|
||||||
|
|
||||||
@@ -1005,8 +1001,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def target_temperature_step(self) -> float | None:
|
def target_temperature_step(self) -> float | None:
|
||||||
"""Return the supported step of target temperature."""
|
"""Return the supported step of target temperature."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.target_temperature_step
|
return self.underlying_entity(0).target_temperature_step
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1016,8 +1012,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.target_temperature_high
|
return self.underlying_entity(0).target_temperature_high
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1027,8 +1023,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.target_temperature_low
|
return self.underlying_entity(0).target_temperature_low
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1038,8 +1034,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
Requires ClimateEntityFeature.AUX_HEAT.
|
Requires ClimateEntityFeature.AUX_HEAT.
|
||||||
"""
|
"""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.is_aux_heat
|
return self.underlying_entity(0).is_aux_heat
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1049,7 +1045,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if not self._device_power or self._is_over_climate:
|
if not self._device_power or self._is_over_climate:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return float(self._device_power * self._prop_algorithm.on_percent)
|
return float(
|
||||||
|
self.nb_underlying_entities
|
||||||
|
* self._device_power
|
||||||
|
* self._prop_algorithm.on_percent
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_energy(self) -> float | None:
|
def total_energy(self) -> float | None:
|
||||||
@@ -1162,29 +1162,32 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
def turn_aux_heat_on(self) -> None:
|
def turn_aux_heat_on(self) -> None:
|
||||||
"""Turn auxiliary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate and self.underlying_entity(0):
|
||||||
return self._underlying_climate.turn_aux_heat_on()
|
return self.underlying_entity(0).turn_aux_heat_on()
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def async_turn_aux_heat_on(self) -> None:
|
async def async_turn_aux_heat_on(self) -> None:
|
||||||
"""Turn auxiliary heater on."""
|
"""Turn auxiliary heater on."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate:
|
||||||
await self._underlying_climate.async_turn_aux_heat_on()
|
for under in self._underlyings:
|
||||||
|
await under.async_turn_aux_heat_on()
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def turn_aux_heat_off(self) -> None:
|
def turn_aux_heat_off(self) -> None:
|
||||||
"""Turn auxiliary heater off."""
|
"""Turn auxiliary heater off."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate:
|
||||||
return self._underlying_climate.turn_aux_heat_off()
|
for under in self._underlyings:
|
||||||
|
return under.turn_aux_heat_off()
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def async_turn_aux_heat_off(self) -> None:
|
async def async_turn_aux_heat_off(self) -> None:
|
||||||
"""Turn auxiliary heater off."""
|
"""Turn auxiliary heater off."""
|
||||||
if self._is_over_climate and self._underlying_climate:
|
if self._is_over_climate:
|
||||||
await self._underlying_climate.async_turn_aux_heat_off()
|
for under in self._underlyings:
|
||||||
|
await under.async_turn_aux_heat_off()
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -1624,7 +1627,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
await self._async_control_heating(True)
|
await self._async_control_heating()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _async_update_temp(self, state: State):
|
async def _async_update_temp(self, state: State):
|
||||||
@@ -1819,18 +1822,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._target_temp,
|
self._target_temp,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_heater_turn_on(self):
|
|
||||||
"""Turn heater toggleable device on."""
|
|
||||||
# TODO should be delegated
|
|
||||||
# data = {ATTR_ENTITY_ID: self._heater_entity_id}
|
|
||||||
# await self.hass.services.async_call(
|
|
||||||
# HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context
|
|
||||||
# )
|
|
||||||
for under in self._underlyings:
|
|
||||||
await under.turn_on()
|
|
||||||
|
|
||||||
async def _async_underlying_entity_turn_off(self):
|
async def _async_underlying_entity_turn_off(self):
|
||||||
"""Turn heater toggleable device off."""
|
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
await under.turn_off()
|
await under.turn_off()
|
||||||
@@ -2208,10 +2201,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
raise err
|
raise err
|
||||||
|
|
||||||
# Check overpowering condition
|
# Check overpowering condition
|
||||||
overpowering: bool = await self.check_overpowering()
|
# Not necessary for switch because each switch is checking at startup
|
||||||
if overpowering:
|
if self.is_over_climate:
|
||||||
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
overpowering: bool = await self.check_overpowering()
|
||||||
return
|
if overpowering:
|
||||||
|
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
||||||
|
return
|
||||||
|
|
||||||
security: bool = await self.check_security()
|
security: bool = await self.check_security()
|
||||||
if security and self._is_over_climate:
|
if security and self._is_over_climate:
|
||||||
|
|||||||
@@ -22,12 +22,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Linked entity",
|
"title": "Linked entities",
|
||||||
"description": "Linked entity attributes",
|
"description": "Linked entities attributes",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Heater entity id",
|
"heater_entity_id": "Heater switch",
|
||||||
|
"heater_entity2_id": "2nd Heater switch",
|
||||||
|
"heater_entity3_id": "3rd Heater switch",
|
||||||
|
"heater_entity4_id": "4th Heater switch",
|
||||||
|
"proportional_function": "Algorithm",
|
||||||
|
"climate_entity_id": "Underlying thermostat"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
|
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||||
"climate_entity_id": "Underlying thermostat entity id"
|
"climate_entity_id": "Underlying climate entity id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
@@ -142,12 +153,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Linked entity",
|
"title": "Linked entities",
|
||||||
"description": "Linked entity attributes",
|
"description": "Linked entities attributes",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Heater entity id",
|
"heater_entity_id": "Heater switch",
|
||||||
|
"heater_entity2_id": "2nd Heater switch",
|
||||||
|
"heater_entity3_id": "3rd Heater switch",
|
||||||
|
"heater_entity4_id": "4th Heater switch",
|
||||||
|
"proportional_function": "Algorithm",
|
||||||
|
"climate_entity_id": "Underlying thermostat"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
|
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||||
"climate_entity_id": "Underlying thermostat entity id"
|
"climate_entity_id": "Underlying climate entity id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ def skip_notifications_fixture():
|
|||||||
def skip_turn_on_off_heater():
|
def skip_turn_on_off_heater():
|
||||||
"""Skip turning on and off the heater"""
|
"""Skip turning on and off the heater"""
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_on"
|
||||||
), patch(
|
), patch(
|
||||||
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_off"
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ async def test_bug_56(
|
|||||||
assert entity
|
assert entity
|
||||||
# cause the underlying climate was not found
|
# cause the underlying climate was not found
|
||||||
assert entity.is_over_climate is True
|
assert entity.is_over_climate is True
|
||||||
assert entity._underlying_climate is None
|
assert entity.underlying_entity(0)._underlying_climate is None
|
||||||
|
|
||||||
# Should not failed
|
# Should not failed
|
||||||
entity.update_custom_attributes()
|
entity.update_custom_attributes()
|
||||||
@@ -260,7 +260,7 @@ async def test_bug_66(
|
|||||||
|
|
||||||
assert mock_send_event.call_count == 1
|
assert mock_send_event.call_count == 1
|
||||||
assert mock_heater_on.call_count == 1
|
assert mock_heater_on.call_count == 1
|
||||||
assert mock_heater_off.call_count == 1
|
assert mock_heater_off.call_count >= 1
|
||||||
assert mock_condition.call_count == 1
|
assert mock_condition.call_count == 1
|
||||||
|
|
||||||
assert entity.window_state == STATE_ON
|
assert entity.window_state == STATE_ON
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
""" Test the normal start of a Thermostat """
|
""" Test the normal start of a Thermostat """
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|||||||
@@ -22,12 +22,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Linked entity",
|
"title": "Linked entities",
|
||||||
"description": "Linked entity attributes",
|
"description": "Linked entities attributes",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Heater entity id",
|
"heater_entity_id": "Heater switch",
|
||||||
|
"heater_entity2_id": "2nd Heater switch",
|
||||||
|
"heater_entity3_id": "3rd Heater switch",
|
||||||
|
"heater_entity4_id": "4th Heater switch",
|
||||||
|
"proportional_function": "Algorithm",
|
||||||
|
"climate_entity_id": "Underlying thermostat"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
|
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||||
"climate_entity_id": "Underlying thermostat entity id"
|
"climate_entity_id": "Underlying climate entity id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
@@ -142,12 +153,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Linked entity",
|
"title": "Linked entities",
|
||||||
"description": "Linked entity attributes",
|
"description": "Linked entities attributes",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Heater entity id",
|
"heater_entity_id": "Heater switch",
|
||||||
|
"heater_entity2_id": "2nd Heater switch",
|
||||||
|
"heater_entity3_id": "3rd Heater switch",
|
||||||
|
"heater_entity4_id": "4th Heater switch",
|
||||||
|
"proportional_function": "Algorithm",
|
||||||
|
"climate_entity_id": "Underlying thermostat"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
|
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
|
||||||
|
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
|
||||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||||
"climate_entity_id": "Underlying thermostat entity id"
|
"climate_entity_id": "Underlying climate entity id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
|
|||||||
@@ -21,12 +21,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Entité liée",
|
"title": "Entité(s) liée(s)",
|
||||||
"description": "Attributs de l'entité liée",
|
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Radiateur entity id",
|
"heater_entity_id": "1er radiateur",
|
||||||
|
"heater_entity2_id": "2ème radiateur",
|
||||||
|
"heater_entity3_id": "3ème radiateur",
|
||||||
|
"heater_entity4_id": "4ème radiateur",
|
||||||
|
"proportional_function": "Algorithme",
|
||||||
|
"climate_entity_id": "Thermostat sous-jacent"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||||
|
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||||
|
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||||
|
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||||
"climate_entity_id": "Thermostat sous-jacent entity id"
|
"climate_entity_id": "Entity id du thermostat sous-jacent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
@@ -142,12 +153,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Entité liée",
|
"title": "Entité(s) liée(s)",
|
||||||
"description": "Attributs de l'entité liée",
|
"description": "Attributs de(s) l'entité(s) liée(s)",
|
||||||
"data": {
|
"data": {
|
||||||
"heater_entity_id": "Radiateur entity id",
|
"heater_entity_id": "1er radiateur",
|
||||||
|
"heater_entity2_id": "2ème radiateur",
|
||||||
|
"heater_entity3_id": "3ème radiateur",
|
||||||
|
"heater_entity4_id": "4ème radiateur",
|
||||||
|
"proportional_function": "Algorithme",
|
||||||
|
"climate_entity_id": "Thermostat sous-jacent"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||||
|
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
|
||||||
|
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
|
||||||
|
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
|
||||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||||
"climate_entity_id": "Thermostat sous-jacent entity id"
|
"climate_entity_id": "Entity id du thermostat sous-jacent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tpi": {
|
"tpi": {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
""" Underlying entities classes """
|
""" Underlying entities classes """
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||||
|
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ from homeassistant.backports.enum import StrEnum
|
|||||||
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
|
ClimateEntityFeature,
|
||||||
DOMAIN as CLIMATE_DOMAIN,
|
DOMAIN as CLIMATE_DOMAIN,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
HVACAction,
|
HVACAction,
|
||||||
@@ -27,8 +29,8 @@ from .const import UnknownEntity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# TODO remove this
|
# remove this
|
||||||
_LOGGER.setLevel(logging.DEBUG)
|
# _LOGGER.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingEntityType(StrEnum):
|
class UnderlyingEntityType(StrEnum):
|
||||||
@@ -45,25 +47,26 @@ class UnderlyingEntity:
|
|||||||
"""Represent a underlying device which could be a switch or a climate"""
|
"""Represent a underlying device which could be a switch or a climate"""
|
||||||
|
|
||||||
_hass: HomeAssistant
|
_hass: HomeAssistant
|
||||||
_thermostat_name: str
|
# Cannot import VersatileThermostat due to circular reference
|
||||||
|
_thermostat: Any
|
||||||
_entity_id: str
|
_entity_id: str
|
||||||
_type: UnderlyingEntityType
|
_type: UnderlyingEntityType
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
thermostat_name: str,
|
thermostat: Any,
|
||||||
entity_type: UnderlyingEntityType,
|
entity_type: UnderlyingEntityType,
|
||||||
entity_id: str,
|
entity_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the underlying entity"""
|
"""Initialize the underlying entity"""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._thermostat_name = thermostat_name
|
self._thermostat = thermostat
|
||||||
self._type = entity_type
|
self._type = entity_type
|
||||||
self._entity_id = entity_id
|
self._entity_id = entity_id
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self._thermostat_name
|
return str(self._thermostat) + "-" + self._entity_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_id(self):
|
def entity_id(self):
|
||||||
@@ -147,7 +150,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
thermostat_name: str,
|
thermostat: Any,
|
||||||
switch_entity_id: str,
|
switch_entity_id: str,
|
||||||
initial_delay_sec: int,
|
initial_delay_sec: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -155,7 +158,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
thermostat_name=thermostat_name,
|
thermostat=thermostat,
|
||||||
entity_type=UnderlyingEntityType.SWITCH,
|
entity_type=UnderlyingEntityType.SWITCH,
|
||||||
entity_id=switch_entity_id,
|
entity_id=switch_entity_id,
|
||||||
)
|
)
|
||||||
@@ -227,7 +230,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
_LOGGER.debug("%s - End of cycle (2)", self)
|
_LOGGER.debug("%s - End of cycle (2)", self)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If we should heat
|
# If we should heat, starts the cycle with delay
|
||||||
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
||||||
# Starts the cycle after the initial delay
|
# Starts the cycle after the initial delay
|
||||||
self._async_cancel_cycle = self.call_later(
|
self._async_cancel_cycle = self.call_later(
|
||||||
@@ -272,23 +275,23 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
await self.turn_off()
|
await self.turn_off()
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO if await self.check_overpowering():
|
if await self._thermostat.check_overpowering():
|
||||||
# _LOGGER.debug("%s - End of cycle (3)", self)
|
_LOGGER.debug("%s - End of cycle (3)", self)
|
||||||
# return
|
return
|
||||||
# Security mode could have change the on_time percent
|
# Security mode could have change the on_time percent
|
||||||
# TODO await self.check_security()
|
await self._thermostat.check_security()
|
||||||
time = self._on_time_sec
|
time = self._on_time_sec
|
||||||
|
|
||||||
action_label = "start"
|
action_label = "start"
|
||||||
if self._should_relaunch_control_heating:
|
# if self._should_relaunch_control_heating:
|
||||||
_LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
|
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
|
||||||
self._should_relaunch_control_heating = False
|
# self._should_relaunch_control_heating = False
|
||||||
# self.hass.create_task(self._async_control_heating())
|
# # self.hass.create_task(self._async_control_heating())
|
||||||
await self.start_cycle(
|
# await self.start_cycle(
|
||||||
self._hvac_mode, self._on_time_sec, self._off_time_sec
|
# self._hvac_mode, self._on_time_sec, self._off_time_sec
|
||||||
)
|
# )
|
||||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
# _LOGGER.debug("%s - End of cycle (3)", self)
|
||||||
return
|
# return
|
||||||
|
|
||||||
if time > 0:
|
if time > 0:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
@@ -325,15 +328,15 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
action_label = "stop"
|
action_label = "stop"
|
||||||
if self._should_relaunch_control_heating:
|
# if self._should_relaunch_control_heating:
|
||||||
_LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
|
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
|
||||||
self._should_relaunch_control_heating = False
|
# self._should_relaunch_control_heating = False
|
||||||
# self.hass.create_task(self._async_control_heating())
|
# # self.hass.create_task(self._async_control_heating())
|
||||||
await self.start_cycle(
|
# await self.start_cycle(
|
||||||
self._hvac_mode, self._on_time_sec, self._off_time_sec
|
# self._hvac_mode, self._on_time_sec, self._off_time_sec
|
||||||
)
|
# )
|
||||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
# _LOGGER.debug("%s - End of cycle (3)", self)
|
||||||
return
|
# return
|
||||||
|
|
||||||
time = self._off_time_sec
|
time = self._off_time_sec
|
||||||
|
|
||||||
@@ -355,7 +358,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# increment energy at the end of the cycle
|
# increment energy at the end of the cycle
|
||||||
# TODO self.incremente_energy()
|
self._thermostat.incremente_energy()
|
||||||
|
|
||||||
async def remove_entity(self):
|
async def remove_entity(self):
|
||||||
"""Remove the entity"""
|
"""Remove the entity"""
|
||||||
@@ -368,13 +371,16 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
_underlying_climate: ClimateEntity
|
_underlying_climate: ClimateEntity
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, thermostat_name: str, climate_entity_id: str
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
thermostat: Any,
|
||||||
|
climate_entity_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the underlying climate"""
|
"""Initialize the underlying climate"""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
thermostat_name=thermostat_name,
|
thermostat=thermostat,
|
||||||
entity_type=UnderlyingEntityType.CLIMATE,
|
entity_type=UnderlyingEntityType.CLIMATE,
|
||||||
entity_id=climate_entity_id,
|
entity_id=climate_entity_id,
|
||||||
)
|
)
|
||||||
@@ -423,7 +429,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_HVAC_MODE,
|
SERVICE_SET_HVAC_MODE,
|
||||||
data, # TODO Needed ?, context=self._context
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -449,7 +455,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_FAN_MODE,
|
SERVICE_SET_FAN_MODE,
|
||||||
data, # TODO needed ? context=self._context
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_humidity(self, humidity: int):
|
async def set_humidity(self, humidity: int):
|
||||||
@@ -465,7 +471,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_HUMIDITY,
|
SERVICE_SET_HUMIDITY,
|
||||||
data, # TODO needed ? context=self._context
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_swing_mode(self, swing_mode):
|
async def set_swing_mode(self, swing_mode):
|
||||||
@@ -481,7 +487,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_SWING_MODE,
|
SERVICE_SET_SWING_MODE,
|
||||||
data, # TODO needed ? context=self._context
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_temperature(self, temperature, max_temp, min_temp):
|
async def set_temperature(self, temperature, max_temp, min_temp):
|
||||||
@@ -498,19 +504,75 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
data, # TODO needed ? context=self._context
|
data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_action(self) -> HVACAction:
|
def hvac_action(self) -> HVACAction | None:
|
||||||
"""Get the hvac action of the underlying"""
|
"""Get the hvac action of the underlying"""
|
||||||
if not self.is_initialized:
|
if not self.is_initialized:
|
||||||
return None
|
return None
|
||||||
return self._underlying_climate.hvac_action
|
return self._underlying_climate.hvac_action
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode:
|
def hvac_mode(self) -> HVACMode | None:
|
||||||
"""Get the hvac mode of the underlying"""
|
"""Get the hvac mode of the underlying"""
|
||||||
if not self.is_initialized:
|
if not self.is_initialized:
|
||||||
return None
|
return None
|
||||||
return self._underlying_climate.hvac_mode
|
return self._underlying_climate.hvac_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
"""Get the fan_mode of the underlying"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return None
|
||||||
|
return self._underlying_climate.fan_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_mode(self) -> str | None:
|
||||||
|
"""Get the swing_mode of the underlying"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return None
|
||||||
|
return self._underlying_climate.swing_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> ClimateEntityFeature:
|
||||||
|
"""Get the supported features of the climate"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
return self._underlying_climate.supported_features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_modes(self) -> list[HVACMode]:
|
||||||
|
"""Get the hvac_modes"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return []
|
||||||
|
return self._underlying_climate.hvac_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str]:
|
||||||
|
"""Get the fan_modes"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return []
|
||||||
|
return self._underlying_climate.fan_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_modes(self) -> list[str]:
|
||||||
|
"""Get the swing_modes"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return []
|
||||||
|
return self._underlying_climate.swing_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self) -> str:
|
||||||
|
"""Get the temperature_unit"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return UnitOfTemperature.CELSIUS
|
||||||
|
return self._underlying_climate.temperature_unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_step(self) -> str:
|
||||||
|
"""Get the target_temperature_step"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return 1
|
||||||
|
return self._underlying_climate.target_temperature_step
|
||||||
|
|||||||
Reference in New Issue
Block a user