Merge branch 'dev' into dev-rc

This commit is contained in:
Bram Kragten 2025-07-30 17:07:48 +02:00
commit 02f87cba9b
18 changed files with 470 additions and 936 deletions

View File

@ -41,7 +41,6 @@ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.CLIMATE, Platform.CLIMATE,
Platform.DEVICE_TRACKER, Platform.DEVICE_TRACKER,
Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.WATER_HEATER, Platform.WATER_HEATER,

View File

@ -73,8 +73,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"weather": {}, "weather": {},
"geofence": {}, "geofence": {},
"zone": {}, "zone": {},
"zone_control": {},
"heating_circuits": {},
} }
@property @property
@ -101,14 +99,11 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
self.home_name = tado_home["name"] self.home_name = tado_home["name"]
devices = await self._async_update_devices() devices = await self._async_update_devices()
zones, zone_controls = await self._async_update_zones() zones = await self._async_update_zones()
home = await self._async_update_home() home = await self._async_update_home()
heating_circuits = await self._async_update_heating_circuits()
self.data["device"] = devices self.data["device"] = devices
self.data["zone"] = zones self.data["zone"] = zones
self.data["zone_control"] = zone_controls
self.data["heating_circuits"] = heating_circuits
self.data["weather"] = home["weather"] self.data["weather"] = home["weather"]
self.data["geofence"] = home["geofence"] self.data["geofence"] = home["geofence"]
@ -171,7 +166,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
return mapped_devices return mapped_devices
async def _async_update_zones(self) -> tuple[dict[int, dict], dict[int, dict]]: async def _async_update_zones(self) -> dict[int, dict]:
"""Update the zone data from Tado.""" """Update the zone data from Tado."""
try: try:
@ -184,12 +179,10 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
raise UpdateFailed(f"Error updating Tado zones: {err}") from err raise UpdateFailed(f"Error updating Tado zones: {err}") from err
mapped_zones: dict[int, dict] = {} mapped_zones: dict[int, dict] = {}
mapped_zone_controls: dict[int, dict] = {}
for zone in zone_states: for zone in zone_states:
mapped_zones[int(zone)] = await self._update_zone(int(zone)) mapped_zones[int(zone)] = await self._update_zone(int(zone))
mapped_zone_controls[int(zone)] = await self._update_zone_control(int(zone))
return mapped_zones, mapped_zone_controls return mapped_zones
async def _update_zone(self, zone_id: int) -> dict[str, str]: async def _update_zone(self, zone_id: int) -> dict[str, str]:
"""Update the internal data of a zone.""" """Update the internal data of a zone."""
@ -206,24 +199,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
_LOGGER.debug("Zone %s updated, with data: %s", zone_id, data) _LOGGER.debug("Zone %s updated, with data: %s", zone_id, data)
return data return data
async def _update_zone_control(self, zone_id: int) -> dict[str, Any]:
"""Update the internal zone control data of a zone."""
_LOGGER.debug("Updating zone control for zone %s", zone_id)
try:
zone_control_data = await self.hass.async_add_executor_job(
self._tado.get_zone_control, zone_id
)
except RequestException as err:
_LOGGER.error(
"Error updating Tado zone control for zone %s: %s", zone_id, err
)
raise UpdateFailed(
f"Error updating Tado zone control for zone {zone_id}: {err}"
) from err
return zone_control_data
async def _async_update_home(self) -> dict[str, dict]: async def _async_update_home(self) -> dict[str, dict]:
"""Update the home data from Tado.""" """Update the home data from Tado."""
@ -242,23 +217,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
return {"weather": weather, "geofence": geofence} return {"weather": weather, "geofence": geofence}
async def _async_update_heating_circuits(self) -> dict[str, dict]:
"""Update the heating circuits data from Tado."""
try:
heating_circuits = await self.hass.async_add_executor_job(
self._tado.get_heating_circuits
)
except RequestException as err:
_LOGGER.error("Error updating Tado heating circuits: %s", err)
raise UpdateFailed(f"Error updating Tado heating circuits: {err}") from err
mapped_heating_circuits: dict[str, dict] = {}
for circuit in heating_circuits:
mapped_heating_circuits[circuit["driverShortSerialNo"]] = circuit
return mapped_heating_circuits
async def get_capabilities(self, zone_id: int | str) -> dict: async def get_capabilities(self, zone_id: int | str) -> dict:
"""Fetch the capabilities from Tado.""" """Fetch the capabilities from Tado."""
@ -406,20 +364,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
except RequestException as exc: except RequestException as exc:
raise HomeAssistantError(f"Error setting Tado child lock: {exc}") from exc raise HomeAssistantError(f"Error setting Tado child lock: {exc}") from exc
async def set_heating_circuit(self, zone_id: int, circuit_id: int | None) -> None:
"""Set heating circuit for zone."""
try:
await self.hass.async_add_executor_job(
self._tado.set_zone_heating_circuit,
zone_id,
circuit_id,
)
except RequestException as exc:
raise HomeAssistantError(
f"Error setting Tado heating circuit: {exc}"
) from exc
await self._update_zone_control(zone_id)
class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"""Class to manage the mobile devices from Tado via PyTado.""" """Class to manage the mobile devices from Tado via PyTado."""

View File

@ -1,108 +0,0 @@
"""Module for Tado select entities."""
import logging
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TadoConfigEntry
from .entity import TadoDataUpdateCoordinator, TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
NO_HEATING_CIRCUIT_OPTION = "no_heating_circuit"
async def async_setup_entry(
hass: HomeAssistant,
entry: TadoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Tado select platform."""
tado = entry.runtime_data.coordinator
entities: list[SelectEntity] = [
TadoHeatingCircuitSelectEntity(tado, zone["name"], zone["id"])
for zone in tado.zones
if zone["type"] == "HEATING"
]
async_add_entities(entities, True)
class TadoHeatingCircuitSelectEntity(TadoZoneEntity, SelectEntity):
"""Representation of a Tado heating circuit select entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
_attr_icon = "mdi:water-boiler"
_attr_translation_key = "heating_circuit"
def __init__(
self,
coordinator: TadoDataUpdateCoordinator,
zone_name: str,
zone_id: int,
) -> None:
"""Initialize the Tado heating circuit select entity."""
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
self._attr_unique_id = f"{zone_id} {coordinator.home_id} heating_circuit"
self._attr_options = []
self._attr_current_option = None
async def async_select_option(self, option: str) -> None:
"""Update the selected heating circuit."""
heating_circuit_id = (
None
if option == NO_HEATING_CIRCUIT_OPTION
else self.coordinator.data["heating_circuits"].get(option, {}).get("number")
)
await self.coordinator.set_heating_circuit(self.zone_id, heating_circuit_id)
await self.coordinator.async_request_refresh()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_callback()
super()._handle_coordinator_update()
@callback
def _async_update_callback(self) -> None:
"""Handle update callbacks."""
# Heating circuits list
heating_circuits = self.coordinator.data["heating_circuits"].values()
self._attr_options = [NO_HEATING_CIRCUIT_OPTION]
self._attr_options.extend(hc["driverShortSerialNo"] for hc in heating_circuits)
# Current heating circuit
zone_control = self.coordinator.data["zone_control"].get(self.zone_id)
if zone_control and "heatingCircuit" in zone_control:
heating_circuit_number = zone_control["heatingCircuit"]
if heating_circuit_number is None:
self._attr_current_option = NO_HEATING_CIRCUIT_OPTION
else:
# Find heating circuit by number
heating_circuit = next(
(
hc
for hc in heating_circuits
if hc.get("number") == heating_circuit_number
),
None,
)
if heating_circuit is None:
_LOGGER.error(
"Heating circuit with number %s not found for zone %s",
heating_circuit_number,
self.zone_name,
)
self._attr_current_option = NO_HEATING_CIRCUIT_OPTION
else:
self._attr_current_option = heating_circuit.get(
"driverShortSerialNo"
)

View File

@ -59,14 +59,6 @@
} }
} }
}, },
"select": {
"heating_circuit": {
"name": "Heating circuit",
"state": {
"no_heating_circuit": "No circuit"
}
}
},
"switch": { "switch": {
"child_lock": { "child_lock": {
"name": "Child lock" "name": "Child lock"

View File

@ -89,6 +89,7 @@ from .light import (
CONF_TEMPERATURE_ACTION, CONF_TEMPERATURE_ACTION,
async_create_preview_light, async_create_preview_light,
) )
from .lock import CONF_LOCK, CONF_OPEN, CONF_UNLOCK, async_create_preview_lock
from .number import ( from .number import (
CONF_MAX, CONF_MAX,
CONF_MIN, CONF_MIN,
@ -103,6 +104,18 @@ from .select import CONF_OPTIONS, CONF_SELECT_OPTION, async_create_preview_selec
from .sensor import async_create_preview_sensor from .sensor import async_create_preview_sensor
from .switch import async_create_preview_switch from .switch import async_create_preview_switch
from .template_entity import TemplateEntity from .template_entity import TemplateEntity
from .vacuum import (
CONF_FAN_SPEED,
CONF_FAN_SPEED_LIST,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
SERVICE_RETURN_TO_BASE,
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
async_create_preview_vacuum,
)
_SCHEMA_STATE: dict[vol.Marker, Any] = { _SCHEMA_STATE: dict[vol.Marker, Any] = {
vol.Required(CONF_STATE): selector.TemplateSelector(), vol.Required(CONF_STATE): selector.TemplateSelector(),
@ -221,6 +234,14 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(), vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(),
} }
if domain == Platform.LOCK:
schema |= _SCHEMA_STATE | {
vol.Required(CONF_LOCK): selector.ActionSelector(),
vol.Required(CONF_UNLOCK): selector.ActionSelector(),
vol.Optional(CONF_CODE_FORMAT): selector.TemplateSelector(),
vol.Optional(CONF_OPEN): selector.ActionSelector(),
}
if domain == Platform.NUMBER: if domain == Platform.NUMBER:
schema |= { schema |= {
vol.Required(CONF_STATE): selector.TemplateSelector(), vol.Required(CONF_STATE): selector.TemplateSelector(),
@ -294,6 +315,26 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
vol.Optional(CONF_TURN_OFF): selector.ActionSelector(), vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
} }
if domain == Platform.VACUUM:
schema |= _SCHEMA_STATE | {
vol.Required(SERVICE_START): selector.ActionSelector(),
vol.Optional(CONF_FAN_SPEED): selector.TemplateSelector(),
vol.Optional(CONF_FAN_SPEED_LIST): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[],
multiple=True,
custom_value=True,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(SERVICE_SET_FAN_SPEED): selector.ActionSelector(),
vol.Optional(SERVICE_STOP): selector.ActionSelector(),
vol.Optional(SERVICE_PAUSE): selector.ActionSelector(),
vol.Optional(SERVICE_RETURN_TO_BASE): selector.ActionSelector(),
vol.Optional(SERVICE_CLEAN_SPOT): selector.ActionSelector(),
vol.Optional(SERVICE_LOCATE): selector.ActionSelector(),
}
schema |= { schema |= {
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
vol.Optional(CONF_ADVANCED_OPTIONS): section( vol.Optional(CONF_ADVANCED_OPTIONS): section(
@ -403,10 +444,12 @@ TEMPLATE_TYPES = [
Platform.FAN, Platform.FAN,
Platform.IMAGE, Platform.IMAGE,
Platform.LIGHT, Platform.LIGHT,
Platform.LOCK,
Platform.NUMBER, Platform.NUMBER,
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.VACUUM,
] ]
CONFIG_FLOW = { CONFIG_FLOW = {
@ -445,6 +488,11 @@ CONFIG_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.LIGHT), validate_user_input=validate_user_input(Platform.LIGHT),
), ),
Platform.LOCK: SchemaFlowFormStep(
config_schema(Platform.LOCK),
preview="template",
validate_user_input=validate_user_input(Platform.LOCK),
),
Platform.NUMBER: SchemaFlowFormStep( Platform.NUMBER: SchemaFlowFormStep(
config_schema(Platform.NUMBER), config_schema(Platform.NUMBER),
preview="template", preview="template",
@ -465,6 +513,11 @@ CONFIG_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.SWITCH), validate_user_input=validate_user_input(Platform.SWITCH),
), ),
Platform.VACUUM: SchemaFlowFormStep(
config_schema(Platform.VACUUM),
preview="template",
validate_user_input=validate_user_input(Platform.VACUUM),
),
} }
@ -504,6 +557,11 @@ OPTIONS_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.LIGHT), validate_user_input=validate_user_input(Platform.LIGHT),
), ),
Platform.LOCK: SchemaFlowFormStep(
options_schema(Platform.LOCK),
preview="template",
validate_user_input=validate_user_input(Platform.LOCK),
),
Platform.NUMBER: SchemaFlowFormStep( Platform.NUMBER: SchemaFlowFormStep(
options_schema(Platform.NUMBER), options_schema(Platform.NUMBER),
preview="template", preview="template",
@ -524,6 +582,11 @@ OPTIONS_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.SWITCH), validate_user_input=validate_user_input(Platform.SWITCH),
), ),
Platform.VACUUM: SchemaFlowFormStep(
options_schema(Platform.VACUUM),
preview="template",
validate_user_input=validate_user_input(Platform.VACUUM),
),
} }
CREATE_PREVIEW_ENTITY: dict[ CREATE_PREVIEW_ENTITY: dict[
@ -535,10 +598,12 @@ CREATE_PREVIEW_ENTITY: dict[
Platform.COVER: async_create_preview_cover, Platform.COVER: async_create_preview_cover,
Platform.FAN: async_create_preview_fan, Platform.FAN: async_create_preview_fan,
Platform.LIGHT: async_create_preview_light, Platform.LIGHT: async_create_preview_light,
Platform.LOCK: async_create_preview_lock,
Platform.NUMBER: async_create_preview_number, Platform.NUMBER: async_create_preview_number,
Platform.SELECT: async_create_preview_select, Platform.SELECT: async_create_preview_select,
Platform.SENSOR: async_create_preview_sensor, Platform.SENSOR: async_create_preview_sensor,
Platform.SWITCH: async_create_preview_switch, Platform.SWITCH: async_create_preview_switch,
Platform.VACUUM: async_create_preview_vacuum,
} }

View File

@ -15,6 +15,7 @@ from homeassistant.components.lock import (
LockEntityFeature, LockEntityFeature,
LockState, LockState,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
CONF_NAME, CONF_NAME,
@ -26,15 +27,23 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.exceptions import ServiceValidationError, TemplateError
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
@ -82,6 +91,10 @@ PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend(
} }
).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema)
LOCK_CONFIG_ENTRY_SCHEMA = LOCK_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -102,6 +115,35 @@ async def async_setup_platform(
) )
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
)
@callback
def async_create_preview_lock(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateLockEntity:
"""Create a preview."""
return async_setup_template_preview(
hass,
name,
config,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
)
class AbstractTemplateLock(AbstractTemplateEntity, LockEntity): class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
"""Representation of a template lock features.""" """Representation of a template lock features."""

View File

@ -188,6 +188,29 @@
}, },
"title": "Template light" "title": "Template light"
}, },
"lock": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"state": "[%key:component::template::common::state%]",
"lock": "Actions on lock",
"unlock": "Actions on unlock",
"code_format": "[%key:component::template::common::code_format%]",
"open": "Actions on open"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "Template lock"
},
"number": { "number": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",
@ -265,10 +288,12 @@
"fan": "Template a fan", "fan": "Template a fan",
"image": "Template an image", "image": "Template an image",
"light": "Template a light", "light": "Template a light",
"lock": "Template a lock",
"number": "Template a number", "number": "Template a number",
"select": "Template a select", "select": "Template a select",
"sensor": "Template a sensor", "sensor": "Template a sensor",
"switch": "Template a switch" "switch": "Template a switch",
"vacuum": "Template a vacuum"
}, },
"title": "Template helper" "title": "Template helper"
}, },
@ -293,6 +318,34 @@
} }
}, },
"title": "Template switch" "title": "Template switch"
},
"vacuum": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"state": "[%key:component::template::common::state%]",
"start": "Actions on turn off",
"fan_speed": "Fan speed",
"fan_speeds": "Fan speeds",
"set_fan_speed": "Actions on set fan speed",
"stop": "Actions on stop",
"pause": "Actions on pause",
"return_to_base": "Actions on return to base",
"clean_spot": "Actions on clean spot",
"locate": "Actions on locate"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "Template vacuum"
} }
} }
}, },
@ -466,6 +519,28 @@
}, },
"title": "[%key:component::template::config::step::light::title%]" "title": "[%key:component::template::config::step::light::title%]"
}, },
"lock": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"state": "[%key:component::template::common::state%]",
"lock": "[%key:component::template::config::step::lock::data::lock%]",
"unlock": "[%key:component::template::config::step::lock::data::unlock%]",
"code_format": "[%key:component::template::common::code_format%]",
"open": "[%key:component::template::config::step::lock::data::open%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "[%key:component::template::config::step::lock::title%]"
},
"number": { "number": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",
@ -552,6 +627,34 @@
} }
}, },
"title": "[%key:component::template::config::step::switch::title%]" "title": "[%key:component::template::config::step::switch::title%]"
},
"vacuum": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"state": "[%key:component::template::common::state%]",
"start": "[%key:component::template::config::step::vacuum::data::start%]",
"fan_speed": "[%key:component::template::config::step::vacuum::data::fan_speed%]",
"fan_speeds": "[%key:component::template::config::step::vacuum::data::fan_speeds%]",
"set_fan_speed": "[%key:component::template::config::step::vacuum::data::set_fan_speed%]",
"stop": "[%key:component::template::config::step::vacuum::data::stop%]",
"pause": "[%key:component::template::config::step::vacuum::data::pause%]",
"return_to_base": "[%key:component::template::config::step::vacuum::data::return_to_base%]",
"clean_spot": "[%key:component::template::config::step::vacuum::data::clean_spot%]",
"locate": "[%key:component::template::config::step::vacuum::data::locate%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "Template vacuum"
} }
} }
}, },

View File

@ -22,6 +22,7 @@ from homeassistant.components.vacuum import (
VacuumActivity, VacuumActivity,
VacuumEntityFeature, VacuumEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
@ -34,16 +35,24 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY, TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_attributes_schema, make_template_entity_common_modern_attributes_schema,
@ -125,6 +134,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
{vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(VACUUM_LEGACY_YAML_SCHEMA)} {vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(VACUUM_LEGACY_YAML_SCHEMA)}
) )
VACUUM_CONFIG_ENTRY_SCHEMA = VACUUM_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -146,6 +159,35 @@ async def async_setup_platform(
) )
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
TemplateStateVacuumEntity,
VACUUM_CONFIG_ENTRY_SCHEMA,
)
@callback
def async_create_preview_vacuum(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> TemplateStateVacuumEntity:
"""Create a preview."""
return async_setup_template_preview(
hass,
name,
config,
TemplateStateVacuumEntity,
VACUUM_CONFIG_ENTRY_SCHEMA,
)
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity): class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
"""Representation of a template vacuum features.""" """Representation of a template vacuum features."""

View File

@ -1,7 +0,0 @@
[
{
"number": 1,
"driverSerialNo": "RU1234567890",
"driverShortSerialNo": "RU1234567890"
}
]

View File

@ -1,80 +0,0 @@
{
"type": "HEATING",
"earlyStartEnabled": false,
"heatingCircuit": 1,
"duties": {
"type": "HEATING",
"leader": {
"deviceType": "RU01",
"serialNo": "RU1234567890",
"shortSerialNo": "RU1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:53:40.710Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"batteryState": "NORMAL"
},
"drivers": [
{
"deviceType": "VA01",
"serialNo": "VA1234567890",
"shortSerialNo": "VA1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:54:15.166Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"mountingState": {
"value": "CALIBRATED",
"timestamp": "2025-06-09T23:25:12.678Z"
},
"mountingStateWithError": "CALIBRATED",
"batteryState": "LOW",
"childLockEnabled": false
}
],
"uis": [
{
"deviceType": "RU01",
"serialNo": "RU1234567890",
"shortSerialNo": "RU1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:53:40.710Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"batteryState": "NORMAL"
},
{
"deviceType": "VA01",
"serialNo": "VA1234567890",
"shortSerialNo": "VA1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:54:15.166Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"mountingState": {
"value": "CALIBRATED",
"timestamp": "2025-06-09T23:25:12.678Z"
},
"mountingStateWithError": "CALIBRATED",
"batteryState": "LOW",
"childLockEnabled": false
}
]
}
}

View File

@ -62,13 +62,6 @@
'presence': 'HOME', 'presence': 'HOME',
'presenceLocked': False, 'presenceLocked': False,
}), }),
'heating_circuits': dict({
'RU1234567890': dict({
'driverSerialNo': 'RU1234567890',
'driverShortSerialNo': 'RU1234567890',
'number': 1,
}),
}),
'weather': dict({ 'weather': dict({
'outsideTemperature': dict({ 'outsideTemperature': dict({
'celsius': 7.46, 'celsius': 7.46,
@ -117,560 +110,6 @@
'repr': "TadoZone(zone_id=6, current_temp=24.3, connection=None, current_temp_timestamp='2024-06-28T22: 23: 15.679Z', current_humidity=70.9, current_humidity_timestamp='2024-06-28T22: 23: 15.679Z', is_away=False, current_hvac_action='HEATING', current_fan_speed='AUTO', current_fan_level='LEVEL3', current_hvac_mode='HEAT', current_swing_mode='OFF', current_vertical_swing_mode='ON', current_horizontal_swing_mode='ON', target_temp=25.0, available=True, power='ON', link='ONLINE', ac_power_timestamp='2022-07-13T18: 06: 58.183Z', heating_power_timestamp=None, ac_power='ON', heating_power=None, heating_power_percentage=None, tado_mode='HOME', overlay_termination_type='MANUAL', overlay_termination_timestamp=None, default_overlay_termination_type='MANUAL', default_overlay_termination_duration=None, preparation=False, open_window=False, open_window_detected=False, open_window_attr={}, precision=0.1)", 'repr': "TadoZone(zone_id=6, current_temp=24.3, connection=None, current_temp_timestamp='2024-06-28T22: 23: 15.679Z', current_humidity=70.9, current_humidity_timestamp='2024-06-28T22: 23: 15.679Z', is_away=False, current_hvac_action='HEATING', current_fan_speed='AUTO', current_fan_level='LEVEL3', current_hvac_mode='HEAT', current_swing_mode='OFF', current_vertical_swing_mode='ON', current_horizontal_swing_mode='ON', target_temp=25.0, available=True, power='ON', link='ONLINE', ac_power_timestamp='2022-07-13T18: 06: 58.183Z', heating_power_timestamp=None, ac_power='ON', heating_power=None, heating_power_percentage=None, tado_mode='HOME', overlay_termination_type='MANUAL', overlay_termination_timestamp=None, default_overlay_termination_type='MANUAL', default_overlay_termination_duration=None, preparation=False, open_window=False, open_window_detected=False, open_window_attr={}, precision=0.1)",
}), }),
}), }),
'zone_control': dict({
'1': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'2': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'3': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'4': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'5': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'6': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
}),
}), }),
'mobile_devices': dict({ 'mobile_devices': dict({
'mobile_device': dict({ 'mobile_device': dict({

View File

@ -1,91 +0,0 @@
"""The select tests for the tado platform."""
from unittest.mock import patch
import pytest
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION
from homeassistant.core import HomeAssistant
from .util import async_init_integration
HEATING_CIRCUIT_SELECT_ENTITY = "select.baseboard_heater_heating_circuit"
NO_HEATING_CIRCUIT = "no_heating_circuit"
HEATING_CIRCUIT_OPTION = "RU1234567890"
ZONE_ID = 1
HEATING_CIRCUIT_ID = 1
async def test_heating_circuit_select(hass: HomeAssistant) -> None:
"""Test creation of heating circuit select entity."""
await async_init_integration(hass)
state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY)
assert state is not None
assert state.state == HEATING_CIRCUIT_OPTION
assert NO_HEATING_CIRCUIT in state.attributes["options"]
assert HEATING_CIRCUIT_OPTION in state.attributes["options"]
@pytest.mark.parametrize(
("option", "expected_circuit_id"),
[(HEATING_CIRCUIT_OPTION, HEATING_CIRCUIT_ID), (NO_HEATING_CIRCUIT, None)],
)
async def test_heating_circuit_select_action(
hass: HomeAssistant, option, expected_circuit_id
) -> None:
"""Test selecting heating circuit option."""
await async_init_integration(hass)
# Test selecting a specific heating circuit
with (
patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_heating_circuit"
) as mock_set_zone_heating_circuit,
patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.get_zone_control"
) as mock_get_zone_control,
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: HEATING_CIRCUIT_SELECT_ENTITY,
ATTR_OPTION: option,
},
blocking=True,
)
mock_set_zone_heating_circuit.assert_called_with(ZONE_ID, expected_circuit_id)
assert mock_get_zone_control.called
@pytest.mark.usefixtures("caplog")
async def test_heating_circuit_not_found(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test when a heating circuit with a specific number is not found."""
circuit_not_matching_zone_control = 999
heating_circuits = [
{
"number": circuit_not_matching_zone_control,
"driverSerialNo": "RU1234567890",
"driverShortSerialNo": "RU1234567890",
}
]
with patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.get_heating_circuits",
return_value=heating_circuits,
):
await async_init_integration(hass)
state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY)
assert state.state == NO_HEATING_CIRCUIT
assert "Heating circuit with number 1 not found for zone" in caplog.text

View File

@ -20,10 +20,8 @@ async def async_init_integration(
me_fixture = "me.json" me_fixture = "me.json"
weather_fixture = "weather.json" weather_fixture = "weather.json"
home_fixture = "home.json" home_fixture = "home.json"
home_heating_circuits_fixture = "heating_circuits.json"
home_state_fixture = "home_state.json" home_state_fixture = "home_state.json"
zones_fixture = "zones.json" zones_fixture = "zones.json"
zone_control_fixture = "zone_control.json"
zone_states_fixture = "zone_states.json" zone_states_fixture = "zone_states.json"
# WR1 Device # WR1 Device
@ -72,10 +70,6 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/", "https://my.tado.com/api/v2/homes/1/",
text=await async_load_fixture(hass, home_fixture, DOMAIN), text=await async_load_fixture(hass, home_fixture, DOMAIN),
) )
m.get(
"https://my.tado.com/api/v2/homes/1/heatingCircuits",
text=await async_load_fixture(hass, home_heating_circuits_fixture, DOMAIN),
)
m.get( m.get(
"https://my.tado.com/api/v2/homes/1/weather", "https://my.tado.com/api/v2/homes/1/weather",
text=await async_load_fixture(hass, weather_fixture, DOMAIN), text=await async_load_fixture(hass, weather_fixture, DOMAIN),
@ -184,12 +178,6 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/zones/1/state", "https://my.tado.com/api/v2/homes/1/zones/1/state",
text=await async_load_fixture(hass, zone_1_state_fixture, DOMAIN), text=await async_load_fixture(hass, zone_1_state_fixture, DOMAIN),
) )
zone_ids = [1, 2, 3, 4, 5, 6]
for zone_id in zone_ids:
m.get(
f"https://my.tado.com/api/v2/homes/1/zones/{zone_id}/control",
text=await async_load_fixture(hass, zone_control_fixture, DOMAIN),
)
m.post( m.post(
"https://login.tado.com/oauth2/token", "https://login.tado.com/oauth2/token",
text=await async_load_fixture(hass, token_fixture, DOMAIN), text=await async_load_fixture(hass, token_fixture, DOMAIN),

View File

@ -0,0 +1,15 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My template',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.my_template',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---

View File

@ -0,0 +1,15 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My template',
'supported_features': <VacuumEntityFeature: 12288>,
}),
'context': <ANY>,
'entity_id': 'vacuum.my_template',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'docked',
})
# ---

View File

@ -179,6 +179,16 @@ BINARY_SENSOR_OPTIONS = {
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{}, {},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
"locked",
{"one": "locked", "two": "unlocked"},
{},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
{},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -229,6 +239,16 @@ BINARY_SENSOR_OPTIONS = {
{}, {},
{}, {},
), ),
(
"vacuum",
{"state": "{{ states('vacuum.one') }}"},
"docked",
{"one": "docked", "two": "cleaning"},
{},
{"start": []},
{"start": []},
{},
),
], ],
) )
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
@ -362,6 +382,12 @@ async def test_config_flow(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -398,6 +424,12 @@ async def test_config_flow(
{"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"},
{"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"},
), ),
(
"vacuum",
{"state": "{{ states('vacuum.one') }}"},
{"start": []},
{"start": []},
),
], ],
) )
async def test_config_flow_device( async def test_config_flow_device(
@ -587,6 +619,16 @@ async def test_config_flow_device(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
"state", "state",
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"state": "{{ states('lock.two') }}"},
["locked", "unlocked"],
{"one": "locked", "two": "unlocked"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
"state",
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -647,6 +689,16 @@ async def test_config_flow_device(
{}, {},
"value_template", "value_template",
), ),
(
"vacuum",
{"state": "{{ states('vacuum.one') }}"},
{"state": "{{ states('vacuum.two') }}"},
["docked", "cleaning"],
{"one": "docked", "two": "cleaning"},
{"start": []},
{"start": []},
"state",
),
], ],
) )
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
@ -1438,6 +1490,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -1480,6 +1538,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{}, {},
{}, {},
), ),
(
"vacuum",
{"state": "{{ states('vacuum.one') }}"},
{"start": []},
{"start": []},
),
], ],
) )
async def test_options_flow_change_device( async def test_options_flow_change_device(

View File

@ -3,6 +3,7 @@
from typing import Any from typing import Any
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant import setup from homeassistant import setup
from homeassistant.components import lock, template from homeassistant.components import lock, template
@ -19,9 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import ConfigurationStyle from .conftest import ConfigurationStyle, async_get_flow_preview_state
from tests.common import assert_setup_component from tests.common import MockConfigEntry, assert_setup_component
from tests.typing import WebSocketGenerator
TEST_OBJECT_ID = "test_template_lock" TEST_OBJECT_ID = "test_template_lock"
TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}" TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}"
@ -1186,3 +1188,58 @@ async def test_optimistic(hass: HomeAssistant) -> None:
state = hass.states.get(TEST_ENTITY_ID) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED assert state.state == LockState.UNLOCKED
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a lock from a config entry."""
hass.states.async_set(
"sensor.test_state",
LockState.LOCKED,
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"state": "{{ states('sensor.test_state') }}",
"lock": [],
"unlock": [],
"template_type": lock.DOMAIN,
},
title="My template",
)
template_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("lock.my_template")
assert state is not None
assert state == snapshot
async def test_flow_preview(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the config flow preview."""
state = await async_get_flow_preview_state(
hass,
hass_ws_client,
lock.DOMAIN,
{
"name": "My template",
"state": "{{ 'locked' }}",
"lock": [],
"unlock": [],
},
)
assert state["state"] == LockState.LOCKED

View File

@ -3,6 +3,7 @@
from typing import Any from typing import Any
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import template, vacuum from homeassistant.components import template, vacuum
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
@ -18,10 +19,11 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import ConfigurationStyle from .conftest import ConfigurationStyle, async_get_flow_preview_state
from tests.common import assert_setup_component from tests.common import MockConfigEntry, assert_setup_component
from tests.components.vacuum import common from tests.components.vacuum import common
from tests.typing import WebSocketGenerator
TEST_OBJECT_ID = "test_vacuum" TEST_OBJECT_ID = "test_vacuum"
TEST_ENTITY_ID = f"vacuum.{TEST_OBJECT_ID}" TEST_ENTITY_ID = f"vacuum.{TEST_OBJECT_ID}"
@ -1261,3 +1263,56 @@ async def test_optimistic_option(
state = hass.states.get(TEST_ENTITY_ID) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == VacuumActivity.DOCKED assert state.state == VacuumActivity.DOCKED
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a vacuum from a config entry."""
hass.states.async_set(
"sensor.test_sensor",
"docked",
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"state": "{{ states('sensor.test_sensor') }}",
"start": [],
"template_type": vacuum.DOMAIN,
},
title="My template",
)
template_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("vacuum.my_template")
assert state is not None
assert state == snapshot
async def test_flow_preview(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the config flow preview."""
state = await async_get_flow_preview_state(
hass,
hass_ws_client,
vacuum.DOMAIN,
{
"name": "My template",
"state": "{{ 'cleaning' }}",
"start": [],
},
)
assert state["state"] == VacuumActivity.CLEANING