Compare commits
2 Commits
3.0.0.beta
...
3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e40a15262 | ||
|
|
974e5d26db |
@@ -169,3 +169,12 @@ switch:
|
|||||||
option: comfort-2
|
option: comfort-2
|
||||||
target:
|
target:
|
||||||
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
|
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
themes:
|
||||||
|
versatile_thermostat_theme:
|
||||||
|
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-power-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-presence-on-color: "lightgreen"
|
||||||
|
|||||||
18
README-fr.md
18
README-fr.md
@@ -45,8 +45,6 @@
|
|||||||
- [Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements](#et-toujours-de-mieux-en-mieux-avec-laappdaemon-notifier-pour-notifier-les-évènements)
|
- [Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements](#et-toujours-de-mieux-en-mieux-avec-laappdaemon-notifier-pour-notifier-les-évènements)
|
||||||
- [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues)
|
- [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues)
|
||||||
|
|
||||||
_Composant développé à l'aide de l'incroyable modèle de développement [[blueprint](https://github.com/custom-components/integration_blueprint)]._
|
|
||||||
|
|
||||||
Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
|
Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
|
||||||
|
|
||||||
# Quand l'utiliser et ne pas l'utiliser
|
# Quand l'utiliser et ne pas l'utiliser
|
||||||
@@ -354,6 +352,22 @@ Dans l'ordre, il y a :
|
|||||||
12. l'état de l'ouverture (si la gestion des ouvertures est configurée),
|
12. l'état de l'ouverture (si la gestion des ouvertures est configurée),
|
||||||
13. l'état du mouvement (si la gestion du mouvements est configurée)
|
13. l'état du mouvement (si la gestion du mouvements est configurée)
|
||||||
|
|
||||||
|
Pour colorer les capteurs, ajouter ces lignes et personnalisez les au besoin, dans votre configuration.yaml :
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend:
|
||||||
|
themes:
|
||||||
|
versatile_thermostat_theme:
|
||||||
|
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-power-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-presence-on-color: "lightgreen"
|
||||||
|
```
|
||||||
|
et choisissez le thème ```versatile_thermostat_theme``` dans la configuration du panel. Vous obtiendrez quelque-chose qui va ressembler à ça :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
|
||||||
Cette implémentation personnalisée offre des services spécifiques pour faciliter l'intégration avec d'autres composants Home Assistant.
|
Cette implémentation personnalisée offre des services spécifiques pour faciliter l'intégration avec d'autres composants Home Assistant.
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -45,7 +45,6 @@
|
|||||||
- [And always better and better with the NOTIFIER daemon app to notify events](#and-always-better-and-better-with-the-notifier-daemon-app-to-notify-events)
|
- [And always better and better with the NOTIFIER daemon app to notify events](#and-always-better-and-better-with-the-notifier-daemon-app-to-notify-events)
|
||||||
- [Contributions are welcome!](#contributions-are-welcome)
|
- [Contributions are welcome!](#contributions-are-welcome)
|
||||||
|
|
||||||
_Component developed by using the amazing development template [[blueprint](https://github.com/custom-components/integration_blueprint)]._
|
|
||||||
|
|
||||||
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
||||||
|
|
||||||
@@ -340,6 +339,22 @@ In order, there are:
|
|||||||
12. opening status (if opening management is configured),
|
12. opening status (if opening management is configured),
|
||||||
13. motion status (if motion management is configured)
|
13. motion status (if motion management is configured)
|
||||||
|
|
||||||
|
To color the sensors, add these lines and customize them as needed, in your configuration.yaml:
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend:
|
||||||
|
themes:
|
||||||
|
versatile_thermostat_theme:
|
||||||
|
state-binary_sensor-safety-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-power-on-color: "#FF0B0B"
|
||||||
|
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
|
||||||
|
state-binary_sensor-presence-on-color: "lightgreen"
|
||||||
|
```
|
||||||
|
and choose the ```versatile_thermostat_theme``` theme in the panel configuration. You will get something that will look like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
|
||||||
This custom implementation offers some specific services to facilitate integration with others Home Assisstant components.
|
This custom implementation offers some specific services to facilitate integration with others Home Assisstant components.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
|
|
||||||
if config_entry.version == 1:
|
if config_entry.version == 1:
|
||||||
new = {**config_entry.data}
|
new = {**config_entry.data}
|
||||||
# TODO: modify Config Entry data
|
# TO DO: modify Config Entry data if there will be something to migrate
|
||||||
|
|
||||||
config_entry.version = 2
|
config_entry.version = 2
|
||||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ from homeassistant.core import HomeAssistant, callback, Event
|
|||||||
|
|
||||||
from homeassistant.const import STATE_ON
|
from homeassistant.const import STATE_ON
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorEntity,
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@@ -56,17 +59,25 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Security state"
|
self._attr_name = "Security state"
|
||||||
self._attr_unique_id = f"{self._device_name}_security_state"
|
self._attr_unique_id = f"{self._device_name}_security_state"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
self._attr_is_on = self.my_climate.security_state
|
self._attr_is_on = self.my_climate.security_state is True
|
||||||
if old_state != self._attr_is_on:
|
if old_state != self._attr_is_on:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||||
|
return BinarySensorDeviceClass.SAFETY
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
if self._attr_is_on:
|
if self._attr_is_on:
|
||||||
@@ -83,17 +94,25 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
|
|||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Overpowering state"
|
self._attr_name = "Overpowering state"
|
||||||
self._attr_unique_id = f"{self._device_name}_overpowering_state"
|
self._attr_unique_id = f"{self._device_name}_overpowering_state"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
self._attr_is_on = self.my_climate.overpowering_state
|
self._attr_is_on = self.my_climate.overpowering_state is True
|
||||||
if old_state != self._attr_is_on:
|
if old_state != self._attr_is_on:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||||
|
return BinarySensorDeviceClass.POWER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
if self._attr_is_on:
|
if self._attr_is_on:
|
||||||
@@ -110,17 +129,25 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Window state"
|
self._attr_name = "Window state"
|
||||||
self._attr_unique_id = f"{self._device_name}_window_state"
|
self._attr_unique_id = f"{self._device_name}_window_state"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
self._attr_is_on = self.my_climate.window_state == STATE_ON
|
self._attr_is_on = self.my_climate.window_state == STATE_ON
|
||||||
if old_state != self._attr_is_on:
|
if old_state != self._attr_is_on:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||||
|
return BinarySensorDeviceClass.WINDOW
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
if self._attr_is_on:
|
if self._attr_is_on:
|
||||||
@@ -137,17 +164,25 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Motion state"
|
self._attr_name = "Motion state"
|
||||||
self._attr_unique_id = f"{self._device_name}_motion_state"
|
self._attr_unique_id = f"{self._device_name}_motion_state"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
self._attr_is_on = self.my_climate.motion_state == STATE_ON
|
self._attr_is_on = self.my_climate.motion_state == STATE_ON
|
||||||
if old_state != self._attr_is_on:
|
if old_state != self._attr_is_on:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||||
|
return BinarySensorDeviceClass.MOTION
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
if self._attr_is_on:
|
if self._attr_is_on:
|
||||||
@@ -164,18 +199,26 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
|
|||||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||||
self._attr_name = "Presence state"
|
self._attr_name = "Presence state"
|
||||||
self._attr_unique_id = f"{self._device_name}_presence_state"
|
self._attr_unique_id = f"{self._device_name}_presence_state"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
|
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
old_state = self._attr_is_on
|
old_state = self._attr_is_on
|
||||||
self._attr_is_on = self.my_climate.presence_state == STATE_ON
|
self._attr_is_on = self.my_climate.presence_state == STATE_ON
|
||||||
if old_state != self._attr_is_on:
|
if old_state != self._attr_is_on:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||||
|
return BinarySensorDeviceClass.PRESENCE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
if self._attr_is_on:
|
if self._attr_is_on:
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self._presence_on = self._presence_sensor_entity_id is not None
|
self._presence_on = self._presence_sensor_entity_id is not None
|
||||||
|
|
||||||
# TODO if self.ac_mode:
|
# if self.ac_mode: -> MODE_COOL should be better to use thermostat_over_climate type
|
||||||
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
|
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
|
||||||
# else:
|
# else:
|
||||||
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
||||||
@@ -1118,6 +1118,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"""
|
"""
|
||||||
return self._attr_preset_modes
|
return self._attr_preset_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_over_climate(self) -> bool | None:
|
||||||
|
"""return True is the thermostat is over a climate
|
||||||
|
or False is over switch"""
|
||||||
|
return self._is_over_climate
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode):
|
async def async_set_hvac_mode(self, hvac_mode):
|
||||||
"""Set new target hvac mode."""
|
"""Set new target hvac mode."""
|
||||||
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
||||||
@@ -1488,6 +1494,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.hass, timedelta(seconds=self._motion_delay_sec), try_motion_condition
|
self.hass, timedelta(seconds=self._motion_delay_sec), try_motion_condition
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For testing purpose we need to access the inner function
|
||||||
|
return try_motion_condition
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def _check_switch_initial_state(self):
|
async def _check_switch_initial_state(self):
|
||||||
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
||||||
@@ -1551,7 +1560,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._hvac_mode = new_state.state
|
self._hvac_mode = new_state.state
|
||||||
|
|
||||||
# Interpretation of hvac
|
# Interpretation of hvac
|
||||||
HVAC_ACTION_ON = [
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
HVACAction.COOLING,
|
HVACAction.COOLING,
|
||||||
HVACAction.DRYING,
|
HVACAction.DRYING,
|
||||||
HVACAction.FAN,
|
HVACAction.FAN,
|
||||||
@@ -2126,7 +2135,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
|
||||||
|
|
||||||
async def _turn_on_off_later(
|
async def _turn_on_off_later(
|
||||||
on: bool, time, heater_action, next_cycle_action
|
on: bool, # pylint: disable=invalid-name
|
||||||
|
time,
|
||||||
|
heater_action,
|
||||||
|
next_cycle_action,
|
||||||
):
|
):
|
||||||
if self._async_cancel_cycle:
|
if self._async_cancel_cycle:
|
||||||
self._async_cancel_cycle()
|
self._async_cancel_cycle()
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_USER_DATA_SCHEMA = vol.Schema(
|
self.STEP_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(
|
vol.Required(
|
||||||
@@ -212,7 +212,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_THERMOSTAT_SWITCH = vol.Schema(
|
self.STEP_THERMOSTAT_SWITCH = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HEATER): selector.EntitySelector(
|
vol.Required(CONF_HEATER): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(
|
selector.EntitySelectorConfig(
|
||||||
@@ -229,7 +229,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_THERMOSTAT_CLIMATE = vol.Schema(
|
self.STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(CONF_CLIMATE): selector.EntitySelector(
|
vol.Required(CONF_CLIMATE): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||||
@@ -237,21 +237,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_TPI_DATA_SCHEMA = vol.Schema(
|
self.STEP_TPI_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
|
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float),
|
||||||
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
|
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_PRESETS_DATA_SCHEMA = vol.Schema(
|
self.STEP_PRESETS_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Optional(v, default=0.0): vol.Coerce(float)
|
vol.Optional(v, default=0.0): vol.Coerce(float)
|
||||||
for (k, v) in CONF_PRESETS.items()
|
for (k, v) in CONF_PRESETS.items()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema(
|
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
|
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(
|
selector.EntitySelectorConfig(
|
||||||
@@ -262,7 +262,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_MOTION_DATA_SCHEMA = vol.Schema(
|
self.STEP_MOTION_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
|
vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(
|
selector.EntitySelectorConfig(
|
||||||
@@ -279,7 +279,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_POWER_DATA_SCHEMA = vol.Schema(
|
self.STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
|
vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(
|
selector.EntitySelectorConfig(
|
||||||
@@ -295,7 +295,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema(
|
self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
|
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(
|
selector.EntitySelectorConfig(
|
||||||
@@ -314,7 +314,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema(
|
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY, default=10
|
CONF_MINIMAL_ACTIVATION_DELAY, default=10
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ from homeassistant.components.climate import (
|
|||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
DEVICE_MANUFACTURER = "JMCOLLIN"
|
|
||||||
DEVICE_MODEL = "Versatile Thermostat"
|
|
||||||
|
|
||||||
from .prop_algorithm import (
|
from .prop_algorithm import (
|
||||||
PROPORTIONAL_FUNCTION_TPI,
|
PROPORTIONAL_FUNCTION_TPI,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DEVICE_MANUFACTURER = "JMCOLLIN"
|
||||||
|
DEVICE_MODEL = "Versatile Thermostat"
|
||||||
|
|
||||||
PRESET_POWER = "power"
|
PRESET_POWER = "power"
|
||||||
PRESET_SECURITY = "security"
|
PRESET_SECURITY = "security"
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,12 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_energy"
|
self._attr_unique_id = f"{self._device_name}_energy"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
if math.isnan(self.my_climate.total_energy) or math.isinf(
|
||||||
self.my_climate.total_energy
|
self.my_climate.total_energy
|
||||||
@@ -125,9 +128,12 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_mean_power_cycle"
|
self._attr_unique_id = f"{self._device_name}_mean_power_cycle"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
|
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
|
||||||
self.my_climate.mean_cycle_power
|
self.my_climate.mean_cycle_power
|
||||||
@@ -182,9 +188,12 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_power_percent"
|
self._attr_unique_id = f"{self._device_name}_power_percent"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
on_percent = (
|
on_percent = (
|
||||||
float(self.my_climate.proportional_algorithm.on_percent)
|
float(self.my_climate.proportional_algorithm.on_percent)
|
||||||
@@ -234,9 +243,12 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_on_time"
|
self._attr_unique_id = f"{self._device_name}_on_time"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
on_time = (
|
on_time = (
|
||||||
float(self.my_climate.proportional_algorithm.on_time_sec)
|
float(self.my_climate.proportional_algorithm.on_time_sec)
|
||||||
@@ -279,9 +291,12 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_off_time"
|
self._attr_unique_id = f"{self._device_name}_off_time"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
off_time = (
|
off_time = (
|
||||||
float(self.my_climate.proportional_algorithm.off_time_sec)
|
float(self.my_climate.proportional_algorithm.off_time_sec)
|
||||||
@@ -324,9 +339,12 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_last_temp_datetime"
|
self._attr_unique_id = f"{self._device_name}_last_temp_datetime"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
old_state = self._attr_native_value
|
old_state = self._attr_native_value
|
||||||
self._attr_native_value = self.my_climate.last_temperature_mesure
|
self._attr_native_value = self.my_climate.last_temperature_mesure
|
||||||
@@ -353,9 +371,12 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{self._device_name}_last_ext_temp_datetime"
|
self._attr_unique_id = f"{self._device_name}_last_ext_temp_datetime"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
async def async_my_climate_changed(self, event: Event):
|
async def async_my_climate_changed(self, event: Event = None):
|
||||||
"""Called when my climate have change"""
|
"""Called when my climate have change"""
|
||||||
_LOGGER.debug("%s - climate state change", event.origin.name)
|
_LOGGER.debug(
|
||||||
|
"%s - climate state change",
|
||||||
|
event.origin.name if event and event.origin else None,
|
||||||
|
)
|
||||||
|
|
||||||
old_state = self._attr_native_value
|
old_state = self._attr_native_value
|
||||||
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
|
self._attr_native_value = self.my_climate.last_ext_temperature_mesure
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
""" Some common resources """
|
""" Some common resources """
|
||||||
from typing import Mapping
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||||
@@ -7,22 +6,21 @@ from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity import Entity
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
||||||
|
|
||||||
from ..climate import VersatileThermostat
|
|
||||||
from ..const import *
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
DOMAIN as CLIMATE_DOMAIN,
|
DOMAIN as CLIMATE_DOMAIN,
|
||||||
ATTR_PRESET_MODE,
|
|
||||||
HVACMode,
|
HVACMode,
|
||||||
HVACAction,
|
HVACAction,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from ..climate import VersatileThermostat
|
||||||
|
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
from .const import ( # pylint: disable=unused-import
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
|
||||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
|
||||||
@@ -81,60 +79,74 @@ class MockClimate(ClimateEntity):
|
|||||||
|
|
||||||
|
|
||||||
class MagicMockClimate(MagicMock):
|
class MagicMockClimate(MagicMock):
|
||||||
|
"""A Magic Mock class for a underlying climate entity"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self):
|
def temperature_unit(self): # pylint: disable=missing-function-docstring
|
||||||
return UnitOfTemperature.CELSIUS
|
return UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self):
|
def hvac_mode(self): # pylint: disable=missing-function-docstring
|
||||||
return HVACMode.HEAT
|
return HVACMode.HEAT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_action(self):
|
def hvac_action(self): # pylint: disable=missing-function-docstring
|
||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self): # pylint: disable=missing-function-docstring
|
||||||
return 15
|
return 15
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self): # pylint: disable=missing-function-docstring
|
||||||
return 14
|
return 14
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_step(self) -> float | None:
|
def target_temperature_step( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> float | None:
|
||||||
return 0.5
|
return 0.5
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_high(self) -> float | None:
|
def target_temperature_high( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> float | None:
|
||||||
return 35
|
return 35
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_low(self) -> float | None:
|
def target_temperature_low( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> float | None:
|
||||||
return 7
|
return 7
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self) -> list[str] | None:
|
def hvac_modes( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> list[str] | None:
|
||||||
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
|
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_modes(self) -> list[str] | None:
|
def fan_modes( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> list[str] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swing_modes(self) -> list[str] | None:
|
def swing_modes( # pylint: disable=missing-function-docstring
|
||||||
|
self,
|
||||||
|
) -> list[str] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swing_mode(self) -> str | None:
|
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
|
||||||
@@ -149,16 +161,23 @@ async def create_thermostat(
|
|||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
assert entry.state is ConfigEntryState.LOADED
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
def find_my_entity(entity_id) -> ClimateEntity:
|
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
"""Find my new entity"""
|
# """Find my new entity"""
|
||||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
for entity in component.entities:
|
# for entity in component.entities:
|
||||||
if entity.entity_id == entity_id:
|
# if entity.entity_id == entity_id:
|
||||||
return entity
|
# return entity
|
||||||
|
|
||||||
entity = find_my_entity(entity_id)
|
return search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||||
|
|
||||||
return entity
|
|
||||||
|
def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
|
||||||
|
"""Search and return the entity in the domain"""
|
||||||
|
component = hass.data[domain]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
|
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
|
||||||
@@ -177,6 +196,24 @@ async def send_temperature_change_event(entity: VersatileThermostat, new_temp, d
|
|||||||
return await entity._async_temperature_changed(temp_event)
|
return await entity._async_temperature_changed(temp_event)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_ext_temperature_change_event(
|
||||||
|
entity: VersatileThermostat, new_temp, date
|
||||||
|
):
|
||||||
|
"""Sending a new external temperature event simulating a change on temperature sensor"""
|
||||||
|
temp_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=new_temp,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return await entity._async_ext_temperature_changed(temp_event)
|
||||||
|
|
||||||
|
|
||||||
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
|
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
|
||||||
"""Sending a new power event simulating a change on power sensor"""
|
"""Sending a new power event simulating a change on power sensor"""
|
||||||
power_event = Event(
|
power_event = Event(
|
||||||
@@ -234,7 +271,57 @@ async def send_window_change_event(
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_tz(hass):
|
async def send_motion_change_event(
|
||||||
|
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||||
|
):
|
||||||
|
"""Sending a new motion event simulating a change on the window state"""
|
||||||
|
motion_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if new_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
"old_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if old_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ret = await entity._async_motion_changed(motion_event)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
async def send_presence_change_event(
|
||||||
|
entity: VersatileThermostat, new_state: bool, old_state: bool, date
|
||||||
|
):
|
||||||
|
"""Sending a new presence event simulating a change on the window state"""
|
||||||
|
presence_event = Event(
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
{
|
||||||
|
"new_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if new_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
"old_state": State(
|
||||||
|
entity_id=entity.entity_id,
|
||||||
|
state=STATE_ON if old_state else STATE_OFF,
|
||||||
|
last_changed=date,
|
||||||
|
last_updated=date,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ret = await entity._async_presence_changed(presence_event)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_tz(hass: HomeAssistant):
|
||||||
"""Get the current timezone"""
|
"""Get the current timezone"""
|
||||||
|
|
||||||
return dt_util.get_time_zone(hass.config.time_zone)
|
return dt_util.get_time_zone(hass.config.time_zone)
|
||||||
@@ -269,3 +356,4 @@ async def send_climate_change_event(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
ret = await entity._async_climate_changed(climate_event)
|
ret = await entity._async_climate_changed(climate_event)
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, StateMachine
|
from homeassistant.core import StateMachine
|
||||||
|
|
||||||
from custom_components.versatile_thermostat.config_flow import (
|
from custom_components.versatile_thermostat.config_flow import (
|
||||||
VersatileThermostatBaseConfigFlow,
|
VersatileThermostatBaseConfigFlow,
|
||||||
@@ -28,13 +28,14 @@ from custom_components.versatile_thermostat.climate import (
|
|||||||
VersatileThermostat,
|
VersatileThermostat,
|
||||||
)
|
)
|
||||||
|
|
||||||
pytest_plugins = "pytest_homeassistant_custom_component"
|
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# This fixture enables loading custom integrations in all tests.
|
# This fixture enables loading custom integrations in all tests.
|
||||||
# Remove to enable selective use of this fixture
|
# Remove to enable selective use of this fixture
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def auto_enable_custom_integrations(enable_custom_integrations):
|
def auto_enable_custom_integrations(enable_custom_integrations):
|
||||||
|
"""Enable all integration in tests"""
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +51,17 @@ def skip_notifications_fixture():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="skip_turn_on_off_heater")
|
||||||
|
def skip_turn_on_off_heater():
|
||||||
|
"""Skip turning on and off the heater"""
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
|
||||||
|
), patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
# This fixture is used to bypass the validate_input function in config_flow
|
# This fixture is used to bypass the validate_input function in config_flow
|
||||||
# NOT USED Now (keeped for memory)
|
# NOT USED Now (keeped for memory)
|
||||||
@pytest.fixture(name="skip_validate_input")
|
@pytest.fixture(name="skip_validate_input")
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
""" Test the normal start of a Thermostat """
|
||||||
|
from unittest.mock import patch
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
|
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from ..climate import VersatileThermostat
|
||||||
|
from ..binary_sensor import (
|
||||||
|
SecurityBinarySensor,
|
||||||
|
OverpoweringBinarySensor,
|
||||||
|
WindowBinarySensor,
|
||||||
|
MotionBinarySensor,
|
||||||
|
PresenceBinarySensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
|
||||||
|
async def test_security_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the security binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
security_binary_sensor: SecurityBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverswitchmockname_security_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert security_binary_sensor
|
||||||
|
|
||||||
|
now: datetime = datetime.now(tz=get_tz(hass))
|
||||||
|
|
||||||
|
# Security should be disabled
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
|
||||||
|
assert security_binary_sensor.state == STATE_OFF
|
||||||
|
assert security_binary_sensor.device_class == BinarySensorDeviceClass.SAFETY
|
||||||
|
|
||||||
|
# Set temperature in the past
|
||||||
|
event_timestamp = now - timedelta(minutes=6)
|
||||||
|
|
||||||
|
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||||
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
|
|
||||||
|
assert entity.security_state is True
|
||||||
|
# Simulate the event reception
|
||||||
|
await security_binary_sensor.async_my_climate_changed()
|
||||||
|
assert security_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
|
# set temperature now
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
assert entity.security_state is False
|
||||||
|
# Simulate the event reception
|
||||||
|
await security_binary_sensor.async_my_climate_changed()
|
||||||
|
assert security_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_overpowering_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the overpowering binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 100,
|
||||||
|
CONF_PRESET_POWER: 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverswitchmockname_overpowering_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert overpowering_binary_sensor
|
||||||
|
|
||||||
|
now: datetime = datetime.now(tz=get_tz(hass))
|
||||||
|
|
||||||
|
# Overpowering should be not set because poer have not been received
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
assert entity.overpowering_state is None
|
||||||
|
|
||||||
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
|
assert overpowering_binary_sensor.state is STATE_OFF
|
||||||
|
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
||||||
|
|
||||||
|
await send_power_change_event(entity, 100, now)
|
||||||
|
await send_max_power_change_event(entity, 150, now)
|
||||||
|
assert await entity.check_overpowering() is True
|
||||||
|
assert entity.overpowering_state is True
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
|
assert overpowering_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
|
# set max power to a low value
|
||||||
|
await send_max_power_change_event(entity, 201, now)
|
||||||
|
assert await entity.check_overpowering() is False
|
||||||
|
assert entity.overpowering_state is False
|
||||||
|
# Simulate the event reception
|
||||||
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
|
assert overpowering_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_window_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the window binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||||
|
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
window_binary_sensor: WindowBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverswitchmockname_window_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert window_binary_sensor
|
||||||
|
|
||||||
|
now: datetime = datetime.now(tz=get_tz(hass))
|
||||||
|
|
||||||
|
# Overpowering should be not set because poer have not been received
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
assert entity.window_state is None
|
||||||
|
|
||||||
|
await window_binary_sensor.async_my_climate_changed()
|
||||||
|
assert window_binary_sensor.state is STATE_OFF
|
||||||
|
assert window_binary_sensor.device_class == BinarySensorDeviceClass.WINDOW
|
||||||
|
|
||||||
|
# Open the window
|
||||||
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
|
try_window_condition = await send_window_change_event(entity, True, False, now)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
|
||||||
|
assert entity.window_state is STATE_ON
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await window_binary_sensor.async_my_climate_changed()
|
||||||
|
assert window_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
|
# close the window
|
||||||
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
|
try_window_condition = await send_window_change_event(entity, False, True, now)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_window_condition(None)
|
||||||
|
|
||||||
|
assert entity.window_state is STATE_OFF
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await window_binary_sensor.async_my_climate_changed()
|
||||||
|
assert window_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_motion_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the motion binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: True,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||||
|
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
motion_binary_sensor: MotionBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverswitchmockname_motion_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert motion_binary_sensor
|
||||||
|
|
||||||
|
now: datetime = datetime.now(tz=get_tz(hass))
|
||||||
|
|
||||||
|
# Overpowering should be not set because poer have not been received
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
assert entity.motion_state is None
|
||||||
|
|
||||||
|
await motion_binary_sensor.async_my_climate_changed()
|
||||||
|
assert motion_binary_sensor.state is STATE_OFF
|
||||||
|
assert motion_binary_sensor.device_class == BinarySensorDeviceClass.MOTION
|
||||||
|
|
||||||
|
# Detect motion
|
||||||
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
|
try_motion_condition = await send_motion_change_event(entity, True, False, now)
|
||||||
|
# simulate the call to try_window_condition
|
||||||
|
await try_motion_condition(None)
|
||||||
|
|
||||||
|
assert entity.motion_state is STATE_ON
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await motion_binary_sensor.async_my_climate_changed()
|
||||||
|
assert motion_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
|
# Undetect motion
|
||||||
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
|
try_motion_condition = await send_motion_change_event(entity, False, True, now)
|
||||||
|
# simulate the call to try_motion_condition
|
||||||
|
await try_motion_condition(None)
|
||||||
|
|
||||||
|
assert entity.motion_state is STATE_OFF
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await motion_binary_sensor.async_my_climate_changed()
|
||||||
|
assert motion_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_presence_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the presence binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
"eco_away_temp": 12,
|
||||||
|
"comfort_away_temp": 13,
|
||||||
|
"boost_away_temp": 14,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: True,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
presence_binary_sensor: PresenceBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverswitchmockname_presence_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert presence_binary_sensor
|
||||||
|
|
||||||
|
now: datetime = datetime.now(tz=get_tz(hass))
|
||||||
|
|
||||||
|
# Overpowering should be not set because poer have not been received
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
assert entity.presence_state is None
|
||||||
|
|
||||||
|
await presence_binary_sensor.async_my_climate_changed()
|
||||||
|
assert presence_binary_sensor.state is STATE_OFF
|
||||||
|
assert presence_binary_sensor.device_class == BinarySensorDeviceClass.PRESENCE
|
||||||
|
|
||||||
|
# Detect motion
|
||||||
|
await send_presence_change_event(entity, True, False, now)
|
||||||
|
|
||||||
|
assert entity.presence_state is STATE_ON
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await presence_binary_sensor.async_my_climate_changed()
|
||||||
|
assert presence_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
|
# Undetect motion
|
||||||
|
await send_presence_change_event(entity, False, True, now)
|
||||||
|
|
||||||
|
assert entity.presence_state is STATE_OFF
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await presence_binary_sensor.async_my_climate_changed()
|
||||||
|
assert presence_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensors_over_climate_minimal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the binary sensors with thermostat over climate type"""
|
||||||
|
|
||||||
|
the_mock_underlying = MagicMockClimate()
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||||
|
return_value=the_mock_underlying,
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverClimateMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.is_over_climate
|
||||||
|
|
||||||
|
security_binary_sensor: SecurityBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverclimatemockname_security_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert security_binary_sensor is not None
|
||||||
|
|
||||||
|
overpowering_binary_sensor: OverpoweringBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverclimatemockname_overpowering_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert overpowering_binary_sensor is None
|
||||||
|
|
||||||
|
window_binary_sensor: WindowBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverclimatemockname_window_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert window_binary_sensor is None
|
||||||
|
|
||||||
|
motion_binary_sensor: MotionBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverclimatemockname_motion_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert motion_binary_sensor is None
|
||||||
|
|
||||||
|
presence_binary_sensor: PresenceBinarySensor = search_entity(
|
||||||
|
hass, "binary_sensor.theoverclimatemockname_presence_state", "binary_sensor"
|
||||||
|
)
|
||||||
|
assert presence_binary_sensor is None
|
||||||
@@ -4,11 +4,7 @@ from homeassistant import data_entry_flow
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry, load_fixture
|
|
||||||
|
|
||||||
from custom_components.versatile_thermostat.const import DOMAIN
|
from custom_components.versatile_thermostat.const import DOMAIN
|
||||||
from custom_components.versatile_thermostat import VersatileThermostatAPI
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
@@ -41,7 +37,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
|||||||
assert result["step_id"] == SOURCE_USER
|
assert result["step_id"] == SOURCE_USER
|
||||||
|
|
||||||
|
|
||||||
async def test_user_config_flow_over_switch(hass, skip_hass_states_get):
|
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
|
||||||
"""Test the config flow with all thermostat_over_switch features"""
|
"""Test the config flow with all thermostat_over_switch features"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
@@ -138,7 +134,7 @@ async def test_user_config_flow_over_switch(hass, skip_hass_states_get):
|
|||||||
assert isinstance(result["result"], ConfigEntry)
|
assert isinstance(result["result"], ConfigEntry)
|
||||||
|
|
||||||
|
|
||||||
async def test_user_config_flow_over_climate(hass, skip_hass_states_get):
|
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
|
||||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Test the Power management """
|
""" Test the Power management """
|
||||||
from unittest.mock import patch, call, MagicMock
|
from unittest.mock import patch, call
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ async def test_power_management_hvac_off(
|
|||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: "eco",
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -394,7 +394,7 @@ async def test_power_management_energy_over_climate(
|
|||||||
hass, entry, "climate.theoverclimatemockname"
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
assert entity._is_over_climate
|
assert entity.is_over_climate
|
||||||
|
|
||||||
now = datetime.now(tz=get_tz(hass))
|
now = datetime.now(tz=get_tz(hass))
|
||||||
await send_temperature_change_event(entity, 15, now)
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
6. check that security is off and preset is changed to boost
|
6. check that security is off and preset is changed to boost
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tz = get_tz(hass)
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@@ -93,7 +93,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
|
|
||||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
assert entity._security_state is True
|
assert entity.security_state is True
|
||||||
assert entity.preset_mode == PRESET_SECURITY
|
assert entity.preset_mode == PRESET_SECURITY
|
||||||
assert entity._saved_preset_mode == PRESET_COMFORT
|
assert entity._saved_preset_mode == PRESET_COMFORT
|
||||||
assert entity._prop_algorithm.on_percent == 0.1
|
assert entity._prop_algorithm.on_percent == 0.1
|
||||||
|
|||||||
375
custom_components/versatile_thermostat/tests/test_sensors.py
Normal file
375
custom_components/versatile_thermostat/tests/test_sensors.py
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
""" Test the normal start of a Thermostat """
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfTime, UnitOfPower, UnitOfEnergy, PERCENTAGE
|
||||||
|
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from ..climate import VersatileThermostat
|
||||||
|
from ..sensor import (
|
||||||
|
EnergySensor,
|
||||||
|
MeanPowerSensor,
|
||||||
|
OnPercentSensor,
|
||||||
|
OnTimeSensor,
|
||||||
|
OffTimeSensor,
|
||||||
|
LastTemperatureSensor,
|
||||||
|
LastExtTemperatureSensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors_over_switch(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the sensors with a thermostat avec switch type"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_HEATER: "switch.mock_switch",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_DEVICE_POWER: 200,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
energy_sensor: EnergySensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_energy", "sensor"
|
||||||
|
)
|
||||||
|
assert energy_sensor
|
||||||
|
|
||||||
|
mean_power_sensor: MeanPowerSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_mean_power_cycle", "sensor"
|
||||||
|
)
|
||||||
|
assert mean_power_sensor
|
||||||
|
|
||||||
|
on_percent_sensor: OnPercentSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_power_percent", "sensor"
|
||||||
|
)
|
||||||
|
assert on_percent_sensor
|
||||||
|
|
||||||
|
on_time_sensor: OnTimeSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_on_time", "sensor"
|
||||||
|
)
|
||||||
|
assert on_time_sensor
|
||||||
|
|
||||||
|
off_time_sensor: OffTimeSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_off_time", "sensor"
|
||||||
|
)
|
||||||
|
assert off_time_sensor
|
||||||
|
|
||||||
|
last_temperature_sensor: LastTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_last_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_temperature_sensor
|
||||||
|
|
||||||
|
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverswitchmockname_last_external_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_ext_temperature_sensor
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await energy_sensor.async_my_climate_changed()
|
||||||
|
assert energy_sensor.state == 0.0
|
||||||
|
await mean_power_sensor.async_my_climate_changed()
|
||||||
|
assert mean_power_sensor.state == 0.0
|
||||||
|
await on_percent_sensor.async_my_climate_changed()
|
||||||
|
assert on_percent_sensor.state == 0.0
|
||||||
|
await on_time_sensor.async_my_climate_changed()
|
||||||
|
assert on_time_sensor.state == 0.0
|
||||||
|
await off_time_sensor.async_my_climate_changed()
|
||||||
|
assert off_time_sensor.state == 300.0
|
||||||
|
await last_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert last_temperature_sensor.state is not None
|
||||||
|
await last_ext_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert last_ext_temperature_sensor.state is not None
|
||||||
|
|
||||||
|
last_temp_date = last_temperature_sensor.state
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
event_timestamp = now - timedelta(minutes=1)
|
||||||
|
|
||||||
|
# Start the heater to get some values
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
|
||||||
|
await energy_sensor.async_my_climate_changed()
|
||||||
|
assert energy_sensor.state == 16.667
|
||||||
|
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
|
||||||
|
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
||||||
|
# because device_power is 200
|
||||||
|
assert energy_sensor.unit_of_measurement == UnitOfEnergy.WATT_HOUR
|
||||||
|
|
||||||
|
await mean_power_sensor.async_my_climate_changed()
|
||||||
|
assert mean_power_sensor.state == 200.0
|
||||||
|
assert mean_power_sensor.device_class == SensorDeviceClass.POWER
|
||||||
|
assert mean_power_sensor.state_class == SensorStateClass.MEASUREMENT
|
||||||
|
# because device_power is 200
|
||||||
|
assert mean_power_sensor.unit_of_measurement == UnitOfPower.WATT
|
||||||
|
|
||||||
|
await on_percent_sensor.async_my_climate_changed()
|
||||||
|
assert on_percent_sensor.state == 100.0
|
||||||
|
assert on_percent_sensor.unit_of_measurement == PERCENTAGE
|
||||||
|
|
||||||
|
await on_time_sensor.async_my_climate_changed()
|
||||||
|
assert on_time_sensor.state == 300.0
|
||||||
|
assert on_time_sensor.device_class == SensorDeviceClass.DURATION
|
||||||
|
assert on_time_sensor.state_class == SensorStateClass.MEASUREMENT
|
||||||
|
assert on_time_sensor.unit_of_measurement == UnitOfTime.SECONDS
|
||||||
|
|
||||||
|
await off_time_sensor.async_my_climate_changed()
|
||||||
|
assert off_time_sensor.state == 0.0
|
||||||
|
assert off_time_sensor.device_class == SensorDeviceClass.DURATION
|
||||||
|
assert off_time_sensor.state_class == SensorStateClass.MEASUREMENT
|
||||||
|
assert off_time_sensor.unit_of_measurement == UnitOfTime.SECONDS
|
||||||
|
|
||||||
|
await last_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert (
|
||||||
|
last_temperature_sensor.state is not None
|
||||||
|
and last_temperature_sensor.state != last_temp_date
|
||||||
|
)
|
||||||
|
assert last_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
await last_ext_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert (
|
||||||
|
last_ext_temperature_sensor.state is not None
|
||||||
|
and last_ext_temperature_sensor.state != last_temp_date
|
||||||
|
)
|
||||||
|
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors_over_climate(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the sensors with thermostat over climate type"""
|
||||||
|
|
||||||
|
the_mock_underlying = MagicMockClimate()
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||||
|
return_value=the_mock_underlying,
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverClimateMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||||
|
CONF_DEVICE_POWER: 1.5,
|
||||||
|
CONF_PRESET_POWER: 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.is_over_climate
|
||||||
|
|
||||||
|
energy_sensor: EnergySensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_energy", "sensor"
|
||||||
|
)
|
||||||
|
assert energy_sensor
|
||||||
|
|
||||||
|
last_temperature_sensor: LastTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_last_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_temperature_sensor
|
||||||
|
|
||||||
|
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_last_external_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_ext_temperature_sensor
|
||||||
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await energy_sensor.async_my_climate_changed()
|
||||||
|
assert energy_sensor.state == 0.0
|
||||||
|
await last_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert last_temperature_sensor.state is not None
|
||||||
|
await last_ext_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert last_ext_temperature_sensor.state is not None
|
||||||
|
|
||||||
|
last_temp_date = last_temperature_sensor.state
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
event_timestamp = now - timedelta(minutes=1)
|
||||||
|
|
||||||
|
# Start the heater to get some values
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 5, event_timestamp)
|
||||||
|
|
||||||
|
# to add energy we must have HVACAction underlying climate event
|
||||||
|
# Send a climate_change event with HVACAction=HEATING
|
||||||
|
event_timestamp = now - timedelta(minutes=60)
|
||||||
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
new_hvac_mode=HVACMode.HEAT,
|
||||||
|
old_hvac_mode=HVACMode.HEAT,
|
||||||
|
new_hvac_action=HVACAction.HEATING,
|
||||||
|
old_hvac_action=HVACAction.OFF,
|
||||||
|
date=event_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||||
|
await send_climate_change_event(
|
||||||
|
entity,
|
||||||
|
new_hvac_mode=HVACMode.HEAT,
|
||||||
|
old_hvac_mode=HVACMode.HEAT,
|
||||||
|
new_hvac_action=HVACAction.IDLE,
|
||||||
|
old_hvac_action=HVACAction.HEATING,
|
||||||
|
date=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh
|
||||||
|
await energy_sensor.async_my_climate_changed()
|
||||||
|
assert energy_sensor.state == 1.5
|
||||||
|
assert energy_sensor.device_class == SensorDeviceClass.ENERGY
|
||||||
|
assert energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
|
||||||
|
# because device_power is 1.5 kW
|
||||||
|
assert energy_sensor.unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR
|
||||||
|
|
||||||
|
entity.incremente_energy()
|
||||||
|
await energy_sensor.async_my_climate_changed()
|
||||||
|
assert energy_sensor.state == 3.0
|
||||||
|
|
||||||
|
await last_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert (
|
||||||
|
last_temperature_sensor.state is not None
|
||||||
|
and last_temperature_sensor.state != last_temp_date
|
||||||
|
)
|
||||||
|
assert last_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
await last_ext_temperature_sensor.async_my_climate_changed()
|
||||||
|
assert (
|
||||||
|
last_ext_temperature_sensor.state is not None
|
||||||
|
and last_ext_temperature_sensor.state != last_temp_date
|
||||||
|
)
|
||||||
|
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors_over_climate_minimal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test the sensors with thermostat over climate type"""
|
||||||
|
|
||||||
|
the_mock_underlying = MagicMockClimate()
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
|
||||||
|
return_value=the_mock_underlying,
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverClimateMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||||
|
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,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 19,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.is_over_climate
|
||||||
|
|
||||||
|
energy_sensor: EnergySensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_energy", "sensor"
|
||||||
|
)
|
||||||
|
assert energy_sensor is None
|
||||||
|
|
||||||
|
last_temperature_sensor: LastTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_last_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_temperature_sensor
|
||||||
|
|
||||||
|
last_ext_temperature_sensor: LastExtTemperatureSensor = search_entity(
|
||||||
|
hass, "sensor.theoverclimatemockname_last_external_temperature_date", "sensor"
|
||||||
|
)
|
||||||
|
assert last_ext_temperature_sensor
|
||||||
@@ -12,7 +12,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|||||||
|
|
||||||
from ..climate import VersatileThermostat
|
from ..climate import VersatileThermostat
|
||||||
|
|
||||||
from .commons import *
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
|
||||||
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
BIN
images/colored-thermostat-sensors.png
Normal file
BIN
images/colored-thermostat-sensors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Reference in New Issue
Block a user