diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce7cf1ac124..e96de66ac76 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ env: CACHE_VERSION: 4 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 - HA_SHORT_VERSION: "2025.8" + HA_SHORT_VERSION: "2025.9" DEFAULT_PYTHON: "3.13" ALL_PYTHON_VERSIONS: "['3.13']" # 10.3 is the oldest supported version diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cc6014b38b0..c5dcf19ce6e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.29.4 + uses: github/codeql-action/init@v3.29.5 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.29.4 + uses: github/codeql-action/analyze@v3.29.5 with: category: "/language:python" diff --git a/.github/workflows/detect-duplicate-issues.yml b/.github/workflows/detect-duplicate-issues.yml index 0facf6fdf77..fd7ed1a38a9 100644 --- a/.github/workflows/detect-duplicate-issues.yml +++ b/.github/workflows/detect-duplicate-issues.yml @@ -231,7 +231,7 @@ jobs: - name: Detect duplicates using AI id: ai_detection if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/ai-inference@v1.2.3 + uses: actions/ai-inference@v1.2.4 with: model: openai/gpt-4o system-prompt: | diff --git a/.github/workflows/detect-non-english-issues.yml b/.github/workflows/detect-non-english-issues.yml index b1ce58c4b41..eefc896bfcb 100644 --- a/.github/workflows/detect-non-english-issues.yml +++ b/.github/workflows/detect-non-english-issues.yml @@ -57,7 +57,7 @@ jobs: - name: Detect language using AI id: ai_language_detection if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/ai-inference@v1.2.3 + uses: actions/ai-inference@v1.2.4 with: model: openai/gpt-4o-mini system-prompt: | diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ea02b249dc9..8d9fca093de 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -159,7 +159,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.03.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -219,7 +219,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.03.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 diff --git a/.strict-typing b/.strict-typing index c6e27a011f1..c125e85bbfc 100644 --- a/.strict-typing +++ b/.strict-typing @@ -53,6 +53,7 @@ homeassistant.components.air_quality.* homeassistant.components.airgradient.* homeassistant.components.airly.* homeassistant.components.airnow.* +homeassistant.components.airos.* homeassistant.components.airq.* homeassistant.components.airthings.* homeassistant.components.airthings_ble.* diff --git a/CODEOWNERS b/CODEOWNERS index 4e7c1b9175a..5ef8479d4d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -67,6 +67,8 @@ build.json @home-assistant/supervisor /tests/components/airly/ @bieniu /homeassistant/components/airnow/ @asymworks /tests/components/airnow/ @asymworks +/homeassistant/components/airos/ @CoMPaTech +/tests/components/airos/ @CoMPaTech /homeassistant/components/airq/ @Sibgatulin @dl2080 /tests/components/airq/ @Sibgatulin @dl2080 /homeassistant/components/airthings/ @danielhiversen @LaStrada diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 7dcccbb1a1e..f92ed38ad85 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False): redirect_uri: str -AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]] +class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False): + """Typed result dict for auth flow.""" + + result: Credentials # Only present if type is CREATE_ENTRY @attr.s(slots=True) diff --git a/homeassistant/brands/frient.json b/homeassistant/brands/frient.json new file mode 100644 index 00000000000..e6b4374576f --- /dev/null +++ b/homeassistant/brands/frient.json @@ -0,0 +1,5 @@ +{ + "domain": "frient", + "name": "Frient", + "iot_standards": ["zigbee"] +} diff --git a/homeassistant/brands/third_reality.json b/homeassistant/brands/third_reality.json index 172b74c42fc..7a4304dad9f 100644 --- a/homeassistant/brands/third_reality.json +++ b/homeassistant/brands/third_reality.json @@ -1,5 +1,5 @@ { "domain": "third_reality", "name": "Third Reality", - "iot_standards": ["zigbee"] + "iot_standards": ["matter", "zigbee"] } diff --git a/homeassistant/brands/ubiquiti.json b/homeassistant/brands/ubiquiti.json index 8b64cffaa7e..bb345775a60 100644 --- a/homeassistant/brands/ubiquiti.json +++ b/homeassistant/brands/ubiquiti.json @@ -1,5 +1,5 @@ { "domain": "ubiquiti", "name": "Ubiquiti", - "integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"] + "integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"] } diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py new file mode 100644 index 00000000000..54f0db205a9 --- /dev/null +++ b/homeassistant/components/airos/__init__.py @@ -0,0 +1,42 @@ +"""The Ubiquiti airOS integration.""" + +from __future__ import annotations + +from airos.airos8 import AirOS + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator + +_PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: + """Set up Ubiquiti airOS from a config entry.""" + + # By default airOS 8 comes with self-signed SSL certificates, + # with no option in the web UI to change or upload a custom certificate. + session = async_get_clientsession(hass, verify_ssl=False) + + airos_device = AirOS( + host=entry.data[CONF_HOST], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + session=session, + ) + + coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/homeassistant/components/airos/config_flow.py b/homeassistant/components/airos/config_flow.py new file mode 100644 index 00000000000..287f54101c8 --- /dev/null +++ b/homeassistant/components/airos/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for the Ubiquiti airOS integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from airos.exceptions import ( + ConnectionAuthenticationError, + ConnectionSetupError, + DataMissingError, + DeviceConnectionError, + KeyDataMissingError, +) +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .coordinator import AirOS + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME, default="ubnt"): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ubiquiti airOS.""" + + VERSION = 1 + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + # By default airOS 8 comes with self-signed SSL certificates, + # with no option in the web UI to change or upload a custom certificate. + session = async_get_clientsession(self.hass, verify_ssl=False) + + airos_device = AirOS( + host=user_input[CONF_HOST], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + session=session, + ) + try: + await airos_device.login() + airos_data = await airos_device.status() + + except ( + ConnectionSetupError, + DeviceConnectionError, + ): + errors["base"] = "cannot_connect" + except (ConnectionAuthenticationError, DataMissingError): + errors["base"] = "invalid_auth" + except KeyDataMissingError: + errors["base"] = "key_data_missing" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(airos_data.derived.mac) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=airos_data.host.hostname, data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/airos/const.py b/homeassistant/components/airos/const.py new file mode 100644 index 00000000000..f4be2594613 --- /dev/null +++ b/homeassistant/components/airos/const.py @@ -0,0 +1,9 @@ +"""Constants for the Ubiquiti airOS integration.""" + +from datetime import timedelta + +DOMAIN = "airos" + +SCAN_INTERVAL = timedelta(minutes=1) + +MANUFACTURER = "Ubiquiti" diff --git a/homeassistant/components/airos/coordinator.py b/homeassistant/components/airos/coordinator.py new file mode 100644 index 00000000000..3f0f1a12380 --- /dev/null +++ b/homeassistant/components/airos/coordinator.py @@ -0,0 +1,66 @@ +"""DataUpdateCoordinator for AirOS.""" + +from __future__ import annotations + +import logging + +from airos.airos8 import AirOS, AirOSData +from airos.exceptions import ( + ConnectionAuthenticationError, + ConnectionSetupError, + DataMissingError, + DeviceConnectionError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator] + + +class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]): + """Class to manage fetching AirOS data from single endpoint.""" + + config_entry: AirOSConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS + ) -> None: + """Initialize the coordinator.""" + self.airos_device = airos_device + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> AirOSData: + """Fetch data from AirOS.""" + try: + await self.airos_device.login() + return await self.airos_device.status() + except (ConnectionAuthenticationError,) as err: + _LOGGER.exception("Error authenticating with airOS device") + raise ConfigEntryError( + translation_domain=DOMAIN, translation_key="invalid_auth" + ) from err + except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err: + _LOGGER.error("Error connecting to airOS device: %s", err) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except (DataMissingError,) as err: + _LOGGER.error("Expected data not returned by airOS device: %s", err) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="error_data_missing", + ) from err diff --git a/homeassistant/components/airos/diagnostics.py b/homeassistant/components/airos/diagnostics.py new file mode 100644 index 00000000000..70fef685c86 --- /dev/null +++ b/homeassistant/components/airos/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics support for airOS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .coordinator import AirOSConfigEntry + +IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related +HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address +TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD] +TO_REDACT_AIROS = [ + "hostname", # Prevent leaking device naming + "essid", # Network SSID + "lat", # GPS latitude to prevent exposing location data. + "lon", # GPS longitude to prevent exposing location data. + *HW_REDACT, + *IP_REDACT, +] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AirOSConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return { + "entry_data": async_redact_data(entry.data, TO_REDACT_HA), + "data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS), + } diff --git a/homeassistant/components/airos/entity.py b/homeassistant/components/airos/entity.py new file mode 100644 index 00000000000..e54962110fc --- /dev/null +++ b/homeassistant/components/airos/entity.py @@ -0,0 +1,36 @@ +"""Generic AirOS Entity Class.""" + +from __future__ import annotations + +from homeassistant.const import CONF_HOST +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import AirOSDataUpdateCoordinator + + +class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]): + """Represent a AirOS Entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None: + """Initialise the gateway.""" + super().__init__(coordinator) + + airos_data = self.coordinator.data + + configuration_url: str | None = ( + f"https://{coordinator.config_entry.data[CONF_HOST]}" + ) + + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)}, + configuration_url=configuration_url, + identifiers={(DOMAIN, str(airos_data.host.device_id))}, + manufacturer=MANUFACTURER, + model=airos_data.host.devmodel, + name=airos_data.host.hostname, + sw_version=airos_data.host.fwversion, + ) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json new file mode 100644 index 00000000000..cb6119a6fa9 --- /dev/null +++ b/homeassistant/components/airos/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "airos", + "name": "Ubiquiti airOS", + "codeowners": ["@CoMPaTech"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airos", + "iot_class": "local_polling", + "quality_scale": "bronze", + "requirements": ["airos==0.2.1"] +} diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml new file mode 100644 index 00000000000..c8c5d209af5 --- /dev/null +++ b/homeassistant/components/airos/quality_scale.yaml @@ -0,0 +1,72 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: airOS does not have actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: airOS does not have actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: local_polling without events + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: airOS does not have actions + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: todo + integration-owner: done + log-when-unavailable: todo + parallel-updates: todo + reauthentication-flow: todo + test-coverage: done + + # Gold + devices: done + diagnostics: done + discovery-update-info: todo + discovery: todo + docs-data-update: done + docs-examples: todo + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: todo + docs-troubleshooting: done + docs-use-cases: todo + dynamic-devices: todo + entity-category: done + entity-device-class: done + entity-disabled-by-default: + status: todo + comment: prepared binary_sensors will provide this + entity-translations: done + exception-translations: done + icon-translations: + status: exempt + comment: no (custom) icons used or envisioned + reconfiguration-flow: todo + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: done diff --git a/homeassistant/components/airos/sensor.py b/homeassistant/components/airos/sensor.py new file mode 100644 index 00000000000..690bf21fc8e --- /dev/null +++ b/homeassistant/components/airos/sensor.py @@ -0,0 +1,152 @@ +"""AirOS Sensor component for Home Assistant.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from airos.data import NetRole, WirelessMode + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, + UnitOfDataRate, + UnitOfFrequency, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator +from .entity import AirOSEntity + +_LOGGER = logging.getLogger(__name__) + +WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode] +NETROLE_OPTIONS = [mode.value for mode in NetRole] + + +@dataclass(frozen=True, kw_only=True) +class AirOSSensorEntityDescription(SensorEntityDescription): + """Describe an AirOS sensor.""" + + value_fn: Callable[[AirOSData], StateType] + + +SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( + AirOSSensorEntityDescription( + key="host_cpuload", + translation_key="host_cpuload", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.host.cpuload, + entity_registry_enabled_default=False, + ), + AirOSSensorEntityDescription( + key="host_netrole", + translation_key="host_netrole", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.host.netrole.value, + options=NETROLE_OPTIONS, + ), + AirOSSensorEntityDescription( + key="wireless_frequency", + translation_key="wireless_frequency", + native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.frequency, + ), + AirOSSensorEntityDescription( + key="wireless_essid", + translation_key="wireless_essid", + value_fn=lambda data: data.wireless.essid, + ), + AirOSSensorEntityDescription( + key="wireless_mode", + translation_key="wireless_mode", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(), + options=WIRELESS_MODE_OPTIONS, + ), + AirOSSensorEntityDescription( + key="wireless_antenna_gain", + translation_key="wireless_antenna_gain", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.antenna_gain, + ), + AirOSSensorEntityDescription( + key="wireless_throughput_tx", + translation_key="wireless_throughput_tx", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.throughput.tx, + ), + AirOSSensorEntityDescription( + key="wireless_throughput_rx", + translation_key="wireless_throughput_rx", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.throughput.rx, + ), + AirOSSensorEntityDescription( + key="wireless_polling_dl_capacity", + translation_key="wireless_polling_dl_capacity", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.polling.dl_capacity, + ), + AirOSSensorEntityDescription( + key="wireless_polling_ul_capacity", + translation_key="wireless_polling_ul_capacity", + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.wireless.polling.ul_capacity, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirOSConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the AirOS sensors from a config entry.""" + coordinator = config_entry.runtime_data + + async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS) + + +class AirOSSensor(AirOSEntity, SensorEntity): + """Representation of a Sensor.""" + + entity_description: AirOSSensorEntityDescription + + def __init__( + self, + coordinator: AirOSDataUpdateCoordinator, + description: AirOSSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}" + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json new file mode 100644 index 00000000000..6823ba8520b --- /dev/null +++ b/homeassistant/components/airos/strings.json @@ -0,0 +1,87 @@ +{ + "config": { + "flow_title": "Ubiquiti airOS device", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "host": "IP address or hostname of the airOS device", + "username": "Administrator username for the airOS device, normally 'ubnt'", + "password": "Password configured through the UISP app or web interface" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "key_data_missing": "Expected data not returned from the device, check the documentation for supported devices", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "host_cpuload": { + "name": "CPU load" + }, + "host_netrole": { + "name": "Network role", + "state": { + "bridge": "Bridge", + "router": "Router" + } + }, + "wireless_frequency": { + "name": "Wireless frequency" + }, + "wireless_essid": { + "name": "Wireless SSID" + }, + "wireless_mode": { + "name": "Wireless mode", + "state": { + "ap_ptp": "Access point", + "sta_ptp": "Station" + } + }, + "wireless_antenna_gain": { + "name": "Antenna gain" + }, + "wireless_throughput_tx": { + "name": "Throughput transmit (actual)" + }, + "wireless_throughput_rx": { + "name": "Throughput receive (actual)" + }, + "wireless_polling_dl_capacity": { + "name": "Download capacity" + }, + "wireless_polling_ul_capacity": { + "name": "Upload capacity" + }, + "wireless_remote_hostname": { + "name": "Remote hostname" + } + } + }, + "exceptions": { + "invalid_auth": { + "message": "[%key:common::config_flow::error::invalid_auth%]" + }, + "cannot_connect": { + "message": "[%key:common::config_flow::error::cannot_connect%]" + }, + "key_data_missing": { + "message": "Key data not returned from device" + }, + "error_data_missing": { + "message": "Data incomplete or missing" + } + } +} diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 175fd320062..04c666dc5bc 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -7,21 +7,18 @@ import logging from airthings import Airthings -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SECRET -from .coordinator import AirthingsDataUpdateCoordinator +from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.SENSOR] SCAN_INTERVAL = timedelta(minutes=6) -type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] - async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: """Set up Airthings from a config entry.""" @@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> async_get_clientsession(hass), ) - coordinator = AirthingsDataUpdateCoordinator(hass, airthings) + coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/airthings/coordinator.py b/homeassistant/components/airthings/coordinator.py index 6172dc0b6ef..9e15e4a0c5d 100644 --- a/homeassistant/components/airthings/coordinator.py +++ b/homeassistant/components/airthings/coordinator.py @@ -5,6 +5,7 @@ import logging from airthings import Airthings, AirthingsDevice, AirthingsError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -13,15 +14,23 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=6) +type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator] + class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]): """Coordinator for Airthings data updates.""" - def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None: + def __init__( + self, + hass: HomeAssistant, + airthings: Airthings, + config_entry: AirthingsConfigEntry, + ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_method=self._update_method, update_interval=SCAN_INTERVAL, diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index fe623c10b33..9df0e60850e 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -2,8 +2,12 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType +from .const import DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator +from .services import async_setup_services PLATFORMS = [ Platform.BINARY_SENSOR, @@ -12,11 +16,20 @@ PLATFORMS = [ Platform.SWITCH, ] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Alexa Devices component.""" + async_setup_services(hass) + return True + async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Set up Alexa Devices platform.""" - coordinator = AmazonDevicesCoordinator(hass, entry) + session = aiohttp_client.async_create_clientsession(hass) + coordinator = AmazonDevicesCoordinator(hass, entry, session) await coordinator.async_config_entry_first_refresh() @@ -29,8 +42,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Unload a config entry.""" - coordinator = entry.runtime_data - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await coordinator.api.close() - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 5ee3bc2e5f0..3e705d73ade 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -17,6 +17,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import CountrySelector @@ -33,18 +34,15 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema( async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" + session = aiohttp_client.async_create_clientsession(hass) api = AmazonEchoApi( + session, data[CONF_COUNTRY], data[CONF_USERNAME], data[CONF_PASSWORD], ) - try: - data = await api.login_mode_interactive(data[CONF_CODE]) - finally: - await api.close() - - return data + return await api.login_mode_interactive(data[CONF_CODE]) class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 7af66f4bb8b..f4a1faa4f81 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -8,6 +8,7 @@ from aioamazondevices.exceptions import ( CannotConnect, CannotRetrieveData, ) +from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME @@ -31,6 +32,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): self, hass: HomeAssistant, entry: AmazonConfigEntry, + session: ClientSession, ) -> None: """Initialize the scanner.""" super().__init__( @@ -41,6 +43,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): update_interval=timedelta(seconds=SCAN_INTERVAL), ) self.api = AmazonEchoApi( + session, entry.data[CONF_COUNTRY], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/alexa_devices/icons.json b/homeassistant/components/alexa_devices/icons.json index 492f89b8fe4..bedd4af1734 100644 --- a/homeassistant/components/alexa_devices/icons.json +++ b/homeassistant/components/alexa_devices/icons.json @@ -38,5 +38,13 @@ } } } + }, + "services": { + "send_sound": { + "service": "mdi:cast-audio" + }, + "send_text_command": { + "service": "mdi:microphone-message" + } } } diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 74187ba7ed4..90410412dfa 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==3.5.1"] + "requirements": ["aioamazondevices==4.0.0"] } diff --git a/homeassistant/components/alexa_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml index 47ff53dd04e..5a2ff55b9b2 100644 --- a/homeassistant/components/alexa_devices/quality_scale.yaml +++ b/homeassistant/components/alexa_devices/quality_scale.yaml @@ -48,7 +48,7 @@ rules: comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration docs-data-update: done docs-examples: done - docs-known-limitations: todo + docs-known-limitations: done docs-supported-devices: done docs-supported-functions: done docs-troubleshooting: done @@ -70,5 +70,5 @@ rules: # Platinum async-dependency: done - inject-websession: todo + inject-websession: done strict-typing: done diff --git a/homeassistant/components/alexa_devices/services.py b/homeassistant/components/alexa_devices/services.py new file mode 100644 index 00000000000..5463c7a4319 --- /dev/null +++ b/homeassistant/components/alexa_devices/services.py @@ -0,0 +1,121 @@ +"""Support for services.""" + +from aioamazondevices.sounds import SOUNDS_LIST +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import DOMAIN +from .coordinator import AmazonConfigEntry + +ATTR_TEXT_COMMAND = "text_command" +ATTR_SOUND = "sound" +ATTR_SOUND_VARIANT = "sound_variant" +SERVICE_TEXT_COMMAND = "send_text_command" +SERVICE_SOUND_NOTIFICATION = "send_sound" + +SCHEMA_SOUND_SERVICE = vol.Schema( + { + vol.Required(ATTR_SOUND): cv.string, + vol.Required(ATTR_SOUND_VARIANT): cv.positive_int, + vol.Required(ATTR_DEVICE_ID): cv.string, + }, +) +SCHEMA_CUSTOM_COMMAND = vol.Schema( + { + vol.Required(ATTR_TEXT_COMMAND): cv.string, + vol.Required(ATTR_DEVICE_ID): cv.string, + } +) + + +@callback +def async_get_entry_id_for_service_call( + call: ServiceCall, +) -> tuple[dr.DeviceEntry, AmazonConfigEntry]: + """Get the entry ID related to a service call (by device ID).""" + device_registry = dr.async_get(call.hass) + device_id = call.data[ATTR_DEVICE_ID] + if (device_entry := device_registry.async_get(device_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device_id", + translation_placeholders={"device_id": device_id}, + ) + + for entry_id in device_entry.config_entries: + if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="entry_not_loaded", + translation_placeholders={"entry": entry.title}, + ) + return (device_entry, entry) + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="config_entry_not_found", + translation_placeholders={"device_id": device_id}, + ) + + +async def _async_execute_action(call: ServiceCall, attribute: str) -> None: + """Execute action on the device.""" + device, config_entry = async_get_entry_id_for_service_call(call) + assert device.serial_number + value: str = call.data[attribute] + + coordinator = config_entry.runtime_data + + if attribute == ATTR_SOUND: + variant: int = call.data[ATTR_SOUND_VARIANT] + pad = "_" if variant > 10 else "_0" + file = f"{value}{pad}{variant!s}" + if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_sound_value", + translation_placeholders={"sound": value, "variant": str(variant)}, + ) + await coordinator.api.call_alexa_sound( + coordinator.data[device.serial_number], file + ) + elif attribute == ATTR_TEXT_COMMAND: + await coordinator.api.call_alexa_text_command( + coordinator.data[device.serial_number], value + ) + + +async def async_send_sound_notification(call: ServiceCall) -> None: + """Send a sound notification to a AmazonDevice.""" + await _async_execute_action(call, ATTR_SOUND) + + +async def async_send_text_command(call: ServiceCall) -> None: + """Send a custom command to a AmazonDevice.""" + await _async_execute_action(call, ATTR_TEXT_COMMAND) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up the services for the Amazon Devices integration.""" + for service_name, method, schema in ( + ( + SERVICE_SOUND_NOTIFICATION, + async_send_sound_notification, + SCHEMA_SOUND_SERVICE, + ), + ( + SERVICE_TEXT_COMMAND, + async_send_text_command, + SCHEMA_CUSTOM_COMMAND, + ), + ): + hass.services.async_register(DOMAIN, service_name, method, schema=schema) diff --git a/homeassistant/components/alexa_devices/services.yaml b/homeassistant/components/alexa_devices/services.yaml new file mode 100644 index 00000000000..d9eef28aea2 --- /dev/null +++ b/homeassistant/components/alexa_devices/services.yaml @@ -0,0 +1,504 @@ +send_text_command: + fields: + device_id: + required: true + selector: + device: + integration: alexa_devices + text_command: + required: true + example: "Play B.B.C. on TuneIn" + selector: + text: + +send_sound: + fields: + device_id: + required: true + selector: + device: + integration: alexa_devices + sound_variant: + required: true + example: 1 + default: 1 + selector: + number: + min: 1 + max: 50 + sound: + required: true + example: amzn_sfx_doorbell_chime + default: amzn_sfx_doorbell_chime + selector: + select: + options: + - air_horn + - air_horns + - airboat + - airport + - aliens + - amzn_sfx_airplane_takeoff_whoosh + - amzn_sfx_army_march_clank_7x + - amzn_sfx_army_march_large_8x + - amzn_sfx_army_march_small_8x + - amzn_sfx_baby_big_cry + - amzn_sfx_baby_cry + - amzn_sfx_baby_fuss + - amzn_sfx_battle_group_clanks + - amzn_sfx_battle_man_grunts + - amzn_sfx_battle_men_grunts + - amzn_sfx_battle_men_horses + - amzn_sfx_battle_noisy_clanks + - amzn_sfx_battle_yells_men + - amzn_sfx_battle_yells_men_run + - amzn_sfx_bear_groan_roar + - amzn_sfx_bear_roar_grumble + - amzn_sfx_bear_roar_small + - amzn_sfx_beep_1x + - amzn_sfx_bell_med_chime + - amzn_sfx_bell_short_chime + - amzn_sfx_bell_timer + - amzn_sfx_bicycle_bell_ring + - amzn_sfx_bird_chickadee_chirp_1x + - amzn_sfx_bird_chickadee_chirps + - amzn_sfx_bird_forest + - amzn_sfx_bird_forest_short + - amzn_sfx_bird_robin_chirp_1x + - amzn_sfx_boing_long_1x + - amzn_sfx_boing_med_1x + - amzn_sfx_boing_short_1x + - amzn_sfx_bus_drive_past + - amzn_sfx_buzz_electronic + - amzn_sfx_buzzer_loud_alarm + - amzn_sfx_buzzer_small + - amzn_sfx_car_accelerate + - amzn_sfx_car_accelerate_noisy + - amzn_sfx_car_click_seatbelt + - amzn_sfx_car_close_door_1x + - amzn_sfx_car_drive_past + - amzn_sfx_car_honk_1x + - amzn_sfx_car_honk_2x + - amzn_sfx_car_honk_3x + - amzn_sfx_car_honk_long_1x + - amzn_sfx_car_into_driveway + - amzn_sfx_car_into_driveway_fast + - amzn_sfx_car_slam_door_1x + - amzn_sfx_car_undo_seatbelt + - amzn_sfx_cat_angry_meow_1x + - amzn_sfx_cat_angry_screech_1x + - amzn_sfx_cat_long_meow_1x + - amzn_sfx_cat_meow_1x + - amzn_sfx_cat_purr + - amzn_sfx_cat_purr_meow + - amzn_sfx_chicken_cluck + - amzn_sfx_church_bell_1x + - amzn_sfx_church_bells_ringing + - amzn_sfx_clear_throat_ahem + - amzn_sfx_clock_ticking + - amzn_sfx_clock_ticking_long + - amzn_sfx_copy_machine + - amzn_sfx_cough + - amzn_sfx_crow_caw_1x + - amzn_sfx_crowd_applause + - amzn_sfx_crowd_bar + - amzn_sfx_crowd_bar_rowdy + - amzn_sfx_crowd_boo + - amzn_sfx_crowd_cheer_med + - amzn_sfx_crowd_excited_cheer + - amzn_sfx_dog_med_bark_1x + - amzn_sfx_dog_med_bark_2x + - amzn_sfx_dog_med_bark_growl + - amzn_sfx_dog_med_growl_1x + - amzn_sfx_dog_med_woof_1x + - amzn_sfx_dog_small_bark_2x + - amzn_sfx_door_open + - amzn_sfx_door_shut + - amzn_sfx_doorbell + - amzn_sfx_doorbell_buzz + - amzn_sfx_doorbell_chime + - amzn_sfx_drinking_slurp + - amzn_sfx_drum_and_cymbal + - amzn_sfx_drum_comedy + - amzn_sfx_earthquake_rumble + - amzn_sfx_electric_guitar + - amzn_sfx_electronic_beep + - amzn_sfx_electronic_major_chord + - amzn_sfx_elephant + - amzn_sfx_elevator_bell_1x + - amzn_sfx_elevator_open_bell + - amzn_sfx_fairy_melodic_chimes + - amzn_sfx_fairy_sparkle_chimes + - amzn_sfx_faucet_drip + - amzn_sfx_faucet_running + - amzn_sfx_fireplace_crackle + - amzn_sfx_fireworks + - amzn_sfx_fireworks_firecrackers + - amzn_sfx_fireworks_launch + - amzn_sfx_fireworks_whistles + - amzn_sfx_food_frying + - amzn_sfx_footsteps + - amzn_sfx_footsteps_muffled + - amzn_sfx_ghost_spooky + - amzn_sfx_glass_on_table + - amzn_sfx_glasses_clink + - amzn_sfx_horse_gallop_4x + - amzn_sfx_horse_huff_whinny + - amzn_sfx_horse_neigh + - amzn_sfx_horse_neigh_low + - amzn_sfx_horse_whinny + - amzn_sfx_human_walking + - amzn_sfx_jar_on_table_1x + - amzn_sfx_kitchen_ambience + - amzn_sfx_large_crowd_cheer + - amzn_sfx_large_fire_crackling + - amzn_sfx_laughter + - amzn_sfx_laughter_giggle + - amzn_sfx_lightning_strike + - amzn_sfx_lion_roar + - amzn_sfx_magic_blast_1x + - amzn_sfx_monkey_calls_3x + - amzn_sfx_monkey_chimp + - amzn_sfx_monkeys_chatter + - amzn_sfx_motorcycle_accelerate + - amzn_sfx_motorcycle_engine_idle + - amzn_sfx_motorcycle_engine_rev + - amzn_sfx_musical_drone_intro + - amzn_sfx_oars_splashing_rowboat + - amzn_sfx_object_on_table_2x + - amzn_sfx_ocean_wave_1x + - amzn_sfx_ocean_wave_on_rocks_1x + - amzn_sfx_ocean_wave_surf + - amzn_sfx_people_walking + - amzn_sfx_person_running + - amzn_sfx_piano_note_1x + - amzn_sfx_punch + - amzn_sfx_rain + - amzn_sfx_rain_on_roof + - amzn_sfx_rain_thunder + - amzn_sfx_rat_squeak_2x + - amzn_sfx_rat_squeaks + - amzn_sfx_raven_caw_1x + - amzn_sfx_raven_caw_2x + - amzn_sfx_restaurant_ambience + - amzn_sfx_rooster_crow + - amzn_sfx_scifi_air_escaping + - amzn_sfx_scifi_alarm + - amzn_sfx_scifi_alien_voice + - amzn_sfx_scifi_boots_walking + - amzn_sfx_scifi_close_large_explosion + - amzn_sfx_scifi_door_open + - amzn_sfx_scifi_engines_on + - amzn_sfx_scifi_engines_on_large + - amzn_sfx_scifi_engines_on_short_burst + - amzn_sfx_scifi_explosion + - amzn_sfx_scifi_explosion_2x + - amzn_sfx_scifi_incoming_explosion + - amzn_sfx_scifi_laser_gun_battle + - amzn_sfx_scifi_laser_gun_fires + - amzn_sfx_scifi_laser_gun_fires_large + - amzn_sfx_scifi_long_explosion_1x + - amzn_sfx_scifi_missile + - amzn_sfx_scifi_motor_short_1x + - amzn_sfx_scifi_open_airlock + - amzn_sfx_scifi_radar_high_ping + - amzn_sfx_scifi_radar_low + - amzn_sfx_scifi_radar_medium + - amzn_sfx_scifi_run_away + - amzn_sfx_scifi_sheilds_up + - amzn_sfx_scifi_short_low_explosion + - amzn_sfx_scifi_small_whoosh_flyby + - amzn_sfx_scifi_small_zoom_flyby + - amzn_sfx_scifi_sonar_ping_3x + - amzn_sfx_scifi_sonar_ping_4x + - amzn_sfx_scifi_spaceship_flyby + - amzn_sfx_scifi_timer_beep + - amzn_sfx_scifi_zap_backwards + - amzn_sfx_scifi_zap_electric + - amzn_sfx_sheep_baa + - amzn_sfx_sheep_bleat + - amzn_sfx_silverware_clank + - amzn_sfx_sirens + - amzn_sfx_sleigh_bells + - amzn_sfx_small_stream + - amzn_sfx_sneeze + - amzn_sfx_stream + - amzn_sfx_strong_wind_desert + - amzn_sfx_strong_wind_whistling + - amzn_sfx_subway_leaving + - amzn_sfx_subway_passing + - amzn_sfx_subway_stopping + - amzn_sfx_swoosh_cartoon_fast + - amzn_sfx_swoosh_fast_1x + - amzn_sfx_swoosh_fast_6x + - amzn_sfx_test_tone + - amzn_sfx_thunder_rumble + - amzn_sfx_toilet_flush + - amzn_sfx_trumpet_bugle + - amzn_sfx_turkey_gobbling + - amzn_sfx_typing_medium + - amzn_sfx_typing_short + - amzn_sfx_typing_typewriter + - amzn_sfx_vacuum_off + - amzn_sfx_vacuum_on + - amzn_sfx_walking_in_mud + - amzn_sfx_walking_in_snow + - amzn_sfx_walking_on_grass + - amzn_sfx_water_dripping + - amzn_sfx_water_droplets + - amzn_sfx_wind_strong_gusting + - amzn_sfx_wind_whistling_desert + - amzn_sfx_wings_flap_4x + - amzn_sfx_wings_flap_fast + - amzn_sfx_wolf_howl + - amzn_sfx_wolf_young_howl + - amzn_sfx_wooden_door + - amzn_sfx_wooden_door_creaks_long + - amzn_sfx_wooden_door_creaks_multiple + - amzn_sfx_wooden_door_creaks_open + - amzn_ui_sfx_gameshow_bridge + - amzn_ui_sfx_gameshow_countdown_loop_32s_full + - amzn_ui_sfx_gameshow_countdown_loop_64s_full + - amzn_ui_sfx_gameshow_countdown_loop_64s_minimal + - amzn_ui_sfx_gameshow_intro + - amzn_ui_sfx_gameshow_negative_response + - amzn_ui_sfx_gameshow_neutral_response + - amzn_ui_sfx_gameshow_outro + - amzn_ui_sfx_gameshow_player1 + - amzn_ui_sfx_gameshow_player2 + - amzn_ui_sfx_gameshow_player3 + - amzn_ui_sfx_gameshow_player4 + - amzn_ui_sfx_gameshow_positive_response + - amzn_ui_sfx_gameshow_tally_negative + - amzn_ui_sfx_gameshow_tally_positive + - amzn_ui_sfx_gameshow_waiting_loop_30s + - anchor + - answering_machines + - arcs_sparks + - arrows_bows + - baby + - back_up_beeps + - bars_restaurants + - baseball + - basketball + - battles + - beeps_tones + - bell + - bikes + - billiards + - board_games + - body + - boing + - books + - bow_wash + - box + - break_shatter_smash + - breaks + - brooms_mops + - bullets + - buses + - buzz + - buzz_hums + - buzzers + - buzzers_pistols + - cables_metal + - camera + - cannons + - car_alarm + - car_alarms + - car_cell_phones + - carnivals_fairs + - cars + - casino + - casinos + - cellar + - chimes + - chimes_bells + - chorus + - christmas + - church_bells + - clock + - cloth + - concrete + - construction + - construction_factory + - crashes + - crowds + - debris + - dining_kitchens + - dinosaurs + - dripping + - drops + - electric + - electrical + - elevator + - evolution_monsters + - explosions + - factory + - falls + - fax_scanner_copier + - feedback_mics + - fight + - fire + - fire_extinguisher + - fireballs + - fireworks + - fishing_pole + - flags + - football + - footsteps + - futuristic + - futuristic_ship + - gameshow + - gear + - ghosts_demons + - giant_monster + - glass + - glasses_clink + - golf + - gorilla + - grenade_lanucher + - griffen + - gyms_locker_rooms + - handgun_loading + - handgun_shot + - handle + - hands + - heartbeats_ekg + - helicopter + - high_tech + - hit_punch_slap + - hits + - horns + - horror + - hot_tub_filling_up + - human + - human_vocals + - hygene # codespell:ignore + - ice_skating + - ignitions + - infantry + - intro + - jet + - juggling + - key_lock + - kids + - knocks + - lab_equip + - lacrosse + - lamps_lanterns + - leather + - liquid_suction + - locker_doors + - machine_gun + - magic_spells + - medium_large_explosions + - metal + - modern_rings + - money_coins + - motorcycles + - movement + - moves + - nature + - oar_boat + - pagers + - paintball + - paper + - parachute + - pay_phones + - phone_beeps + - pigmy_bats + - pills + - pour_water + - power_up_down + - printers + - prison + - public_space + - racquetball + - radios_static + - rain + - rc_airplane + - rc_car + - refrigerators_freezers + - regular + - respirator + - rifle + - roller_coaster + - rollerskates_rollerblades + - room_tones + - ropes_climbing + - rotary_rings + - rowboat_canoe + - rubber + - running + - sails + - sand_gravel + - screen_doors + - screens + - seats_stools + - servos + - shoes_boots + - shotgun + - shower + - sink_faucet + - sink_filling_water + - sink_run_and_off + - sink_water_splatter + - sirens + - skateboards + - ski + - skids_tires + - sled + - slides + - small_explosions + - snow + - snowmobile + - soldiers + - splash_water + - splashes_sprays + - sports_whistles + - squeaks + - squeaky + - stairs + - steam + - submarine_diesel + - swing_doors + - switches_levers + - swords + - tape + - tape_machine + - televisions_shows + - tennis_pingpong + - textile + - throw + - thunder + - ticks + - timer + - toilet_flush + - tone + - tones_noises + - toys + - tractors + - traffic + - train + - trucks_vans + - turnstiles + - typing + - umbrella + - underwater + - vampires + - various + - video_tunes + - volcano_earthquake + - watches + - water + - water_running + - werewolves + - winches_gears + - wind + - wood + - wood_boat + - woosh + - zap + - zippers + translation_key: sound diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 19cc39cab42..1b1150d5649 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -4,7 +4,8 @@ "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", - "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported." + "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", + "device_id_description": "The ID of the device to send the command to." }, "config": { "flow_title": "{username}", @@ -84,12 +85,532 @@ } } }, + "services": { + "send_sound": { + "name": "Send sound", + "description": "Sends a sound to a device", + "fields": { + "device_id": { + "name": "Device", + "description": "[%key:component::alexa_devices::common::device_id_description%]" + }, + "sound": { + "name": "Alexa Skill sound file", + "description": "The sound file to play." + }, + "sound_variant": { + "name": "Sound variant", + "description": "The variant of the sound to play." + } + } + }, + "send_text_command": { + "name": "Send text command", + "description": "Sends a text command to a device", + "fields": { + "text_command": { + "name": "Alexa text command", + "description": "The text command to send." + }, + "device_id": { + "name": "Device", + "description": "[%key:component::alexa_devices::common::device_id_description%]" + } + } + } + }, + "selector": { + "sound": { + "options": { + "air_horn": "Air Horn", + "air_horns": "Air Horns", + "airboat": "Airboat", + "airport": "Airport", + "aliens": "Aliens", + "amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh", + "amzn_sfx_army_march_clank_7x": "Army March Clank 7x", + "amzn_sfx_army_march_large_8x": "Army March Large 8x", + "amzn_sfx_army_march_small_8x": "Army March Small 8x", + "amzn_sfx_baby_big_cry": "Baby Big Cry", + "amzn_sfx_baby_cry": "Baby Cry", + "amzn_sfx_baby_fuss": "Baby Fuss", + "amzn_sfx_battle_group_clanks": "Battle Group Clanks", + "amzn_sfx_battle_man_grunts": "Battle Man Grunts", + "amzn_sfx_battle_men_grunts": "Battle Men Grunts", + "amzn_sfx_battle_men_horses": "Battle Men Horses", + "amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks", + "amzn_sfx_battle_yells_men": "Battle Yells Men", + "amzn_sfx_battle_yells_men_run": "Battle Yells Men Run", + "amzn_sfx_bear_groan_roar": "Bear Groan Roar", + "amzn_sfx_bear_roar_grumble": "Bear Roar Grumble", + "amzn_sfx_bear_roar_small": "Bear Roar Small", + "amzn_sfx_beep_1x": "Beep 1x", + "amzn_sfx_bell_med_chime": "Bell Med Chime", + "amzn_sfx_bell_short_chime": "Bell Short Chime", + "amzn_sfx_bell_timer": "Bell Timer", + "amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring", + "amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x", + "amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps", + "amzn_sfx_bird_forest": "Bird Forest", + "amzn_sfx_bird_forest_short": "Bird Forest Short", + "amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x", + "amzn_sfx_boing_long_1x": "Boing Long 1x", + "amzn_sfx_boing_med_1x": "Boing Med 1x", + "amzn_sfx_boing_short_1x": "Boing Short 1x", + "amzn_sfx_bus_drive_past": "Bus Drive Past", + "amzn_sfx_buzz_electronic": "Buzz Electronic", + "amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm", + "amzn_sfx_buzzer_small": "Buzzer Small", + "amzn_sfx_car_accelerate": "Car Accelerate", + "amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy", + "amzn_sfx_car_click_seatbelt": "Car Click Seatbelt", + "amzn_sfx_car_close_door_1x": "Car Close Door 1x", + "amzn_sfx_car_drive_past": "Car Drive Past", + "amzn_sfx_car_honk_1x": "Car Honk 1x", + "amzn_sfx_car_honk_2x": "Car Honk 2x", + "amzn_sfx_car_honk_3x": "Car Honk 3x", + "amzn_sfx_car_honk_long_1x": "Car Honk Long 1x", + "amzn_sfx_car_into_driveway": "Car Into Driveway", + "amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast", + "amzn_sfx_car_slam_door_1x": "Car Slam Door 1x", + "amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt", + "amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x", + "amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x", + "amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x", + "amzn_sfx_cat_meow_1x": "Cat Meow 1x", + "amzn_sfx_cat_purr": "Cat Purr", + "amzn_sfx_cat_purr_meow": "Cat Purr Meow", + "amzn_sfx_chicken_cluck": "Chicken Cluck", + "amzn_sfx_church_bell_1x": "Church Bell 1x", + "amzn_sfx_church_bells_ringing": "Church Bells Ringing", + "amzn_sfx_clear_throat_ahem": "Clear Throat Ahem", + "amzn_sfx_clock_ticking": "Clock Ticking", + "amzn_sfx_clock_ticking_long": "Clock Ticking Long", + "amzn_sfx_copy_machine": "Copy Machine", + "amzn_sfx_cough": "Cough", + "amzn_sfx_crow_caw_1x": "Crow Caw 1x", + "amzn_sfx_crowd_applause": "Crowd Applause", + "amzn_sfx_crowd_bar": "Crowd Bar", + "amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy", + "amzn_sfx_crowd_boo": "Crowd Boo", + "amzn_sfx_crowd_cheer_med": "Crowd Cheer Med", + "amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer", + "amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x", + "amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x", + "amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl", + "amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x", + "amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x", + "amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x", + "amzn_sfx_door_open": "Door Open", + "amzn_sfx_door_shut": "Door Shut", + "amzn_sfx_doorbell": "Doorbell", + "amzn_sfx_doorbell_buzz": "Doorbell Buzz", + "amzn_sfx_doorbell_chime": "Doorbell Chime", + "amzn_sfx_drinking_slurp": "Drinking Slurp", + "amzn_sfx_drum_and_cymbal": "Drum And Cymbal", + "amzn_sfx_drum_comedy": "Drum Comedy", + "amzn_sfx_earthquake_rumble": "Earthquake Rumble", + "amzn_sfx_electric_guitar": "Electric Guitar", + "amzn_sfx_electronic_beep": "Electronic Beep", + "amzn_sfx_electronic_major_chord": "Electronic Major Chord", + "amzn_sfx_elephant": "Elephant", + "amzn_sfx_elevator_bell_1x": "Elevator Bell 1x", + "amzn_sfx_elevator_open_bell": "Elevator Open Bell", + "amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes", + "amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes", + "amzn_sfx_faucet_drip": "Faucet Drip", + "amzn_sfx_faucet_running": "Faucet Running", + "amzn_sfx_fireplace_crackle": "Fireplace Crackle", + "amzn_sfx_fireworks": "Fireworks", + "amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers", + "amzn_sfx_fireworks_launch": "Fireworks Launch", + "amzn_sfx_fireworks_whistles": "Fireworks Whistles", + "amzn_sfx_food_frying": "Food Frying", + "amzn_sfx_footsteps": "Footsteps", + "amzn_sfx_footsteps_muffled": "Footsteps Muffled", + "amzn_sfx_ghost_spooky": "Ghost Spooky", + "amzn_sfx_glass_on_table": "Glass On Table", + "amzn_sfx_glasses_clink": "Glasses Clink", + "amzn_sfx_horse_gallop_4x": "Horse Gallop 4x", + "amzn_sfx_horse_huff_whinny": "Horse Huff Whinny", + "amzn_sfx_horse_neigh": "Horse Neigh", + "amzn_sfx_horse_neigh_low": "Horse Neigh Low", + "amzn_sfx_horse_whinny": "Horse Whinny", + "amzn_sfx_human_walking": "Human Walking", + "amzn_sfx_jar_on_table_1x": "Jar On Table 1x", + "amzn_sfx_kitchen_ambience": "Kitchen Ambience", + "amzn_sfx_large_crowd_cheer": "Large Crowd Cheer", + "amzn_sfx_large_fire_crackling": "Large Fire Crackling", + "amzn_sfx_laughter": "Laughter", + "amzn_sfx_laughter_giggle": "Laughter Giggle", + "amzn_sfx_lightning_strike": "Lightning Strike", + "amzn_sfx_lion_roar": "Lion Roar", + "amzn_sfx_magic_blast_1x": "Magic Blast 1x", + "amzn_sfx_monkey_calls_3x": "Monkey Calls 3x", + "amzn_sfx_monkey_chimp": "Monkey Chimp", + "amzn_sfx_monkeys_chatter": "Monkeys Chatter", + "amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate", + "amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle", + "amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev", + "amzn_sfx_musical_drone_intro": "Musical Drone Intro", + "amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat", + "amzn_sfx_object_on_table_2x": "Object On Table 2x", + "amzn_sfx_ocean_wave_1x": "Ocean Wave 1x", + "amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x", + "amzn_sfx_ocean_wave_surf": "Ocean Wave Surf", + "amzn_sfx_people_walking": "People Walking", + "amzn_sfx_person_running": "Person Running", + "amzn_sfx_piano_note_1x": "Piano Note 1x", + "amzn_sfx_punch": "Punch", + "amzn_sfx_rain": "Rain", + "amzn_sfx_rain_on_roof": "Rain On Roof", + "amzn_sfx_rain_thunder": "Rain Thunder", + "amzn_sfx_rat_squeak_2x": "Rat Squeak 2x", + "amzn_sfx_rat_squeaks": "Rat Squeaks", + "amzn_sfx_raven_caw_1x": "Raven Caw 1x", + "amzn_sfx_raven_caw_2x": "Raven Caw 2x", + "amzn_sfx_restaurant_ambience": "Restaurant Ambience", + "amzn_sfx_rooster_crow": "Rooster Crow", + "amzn_sfx_scifi_air_escaping": "Scifi Air Escaping", + "amzn_sfx_scifi_alarm": "Scifi Alarm", + "amzn_sfx_scifi_alien_voice": "Scifi Alien Voice", + "amzn_sfx_scifi_boots_walking": "Scifi Boots Walking", + "amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion", + "amzn_sfx_scifi_door_open": "Scifi Door Open", + "amzn_sfx_scifi_engines_on": "Scifi Engines On", + "amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large", + "amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst", + "amzn_sfx_scifi_explosion": "Scifi Explosion", + "amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x", + "amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion", + "amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle", + "amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires", + "amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large", + "amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x", + "amzn_sfx_scifi_missile": "Scifi Missile", + "amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x", + "amzn_sfx_scifi_open_airlock": "Scifi Open Airlock", + "amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping", + "amzn_sfx_scifi_radar_low": "Scifi Radar Low", + "amzn_sfx_scifi_radar_medium": "Scifi Radar Medium", + "amzn_sfx_scifi_run_away": "Scifi Run Away", + "amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up", + "amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion", + "amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby", + "amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby", + "amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x", + "amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x", + "amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby", + "amzn_sfx_scifi_timer_beep": "Scifi Timer Beep", + "amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards", + "amzn_sfx_scifi_zap_electric": "Scifi Zap Electric", + "amzn_sfx_sheep_baa": "Sheep Baa", + "amzn_sfx_sheep_bleat": "Sheep Bleat", + "amzn_sfx_silverware_clank": "Silverware Clank", + "amzn_sfx_sirens": "Sirens", + "amzn_sfx_sleigh_bells": "Sleigh Bells", + "amzn_sfx_small_stream": "Small Stream", + "amzn_sfx_sneeze": "Sneeze", + "amzn_sfx_stream": "Stream", + "amzn_sfx_strong_wind_desert": "Strong Wind Desert", + "amzn_sfx_strong_wind_whistling": "Strong Wind Whistling", + "amzn_sfx_subway_leaving": "Subway Leaving", + "amzn_sfx_subway_passing": "Subway Passing", + "amzn_sfx_subway_stopping": "Subway Stopping", + "amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast", + "amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x", + "amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x", + "amzn_sfx_test_tone": "Test Tone", + "amzn_sfx_thunder_rumble": "Thunder Rumble", + "amzn_sfx_toilet_flush": "Toilet Flush", + "amzn_sfx_trumpet_bugle": "Trumpet Bugle", + "amzn_sfx_turkey_gobbling": "Turkey Gobbling", + "amzn_sfx_typing_medium": "Typing Medium", + "amzn_sfx_typing_short": "Typing Short", + "amzn_sfx_typing_typewriter": "Typing Typewriter", + "amzn_sfx_vacuum_off": "Vacuum Off", + "amzn_sfx_vacuum_on": "Vacuum On", + "amzn_sfx_walking_in_mud": "Walking In Mud", + "amzn_sfx_walking_in_snow": "Walking In Snow", + "amzn_sfx_walking_on_grass": "Walking On Grass", + "amzn_sfx_water_dripping": "Water Dripping", + "amzn_sfx_water_droplets": "Water Droplets", + "amzn_sfx_wind_strong_gusting": "Wind Strong Gusting", + "amzn_sfx_wind_whistling_desert": "Wind Whistling Desert", + "amzn_sfx_wings_flap_4x": "Wings Flap 4x", + "amzn_sfx_wings_flap_fast": "Wings Flap Fast", + "amzn_sfx_wolf_howl": "Wolf Howl", + "amzn_sfx_wolf_young_howl": "Wolf Young Howl", + "amzn_sfx_wooden_door": "Wooden Door", + "amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long", + "amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple", + "amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open", + "amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge", + "amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full", + "amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full", + "amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal", + "amzn_ui_sfx_gameshow_intro": "Gameshow Intro", + "amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response", + "amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response", + "amzn_ui_sfx_gameshow_outro": "Gameshow Outro", + "amzn_ui_sfx_gameshow_player1": "Gameshow Player1", + "amzn_ui_sfx_gameshow_player2": "Gameshow Player2", + "amzn_ui_sfx_gameshow_player3": "Gameshow Player3", + "amzn_ui_sfx_gameshow_player4": "Gameshow Player4", + "amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response", + "amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative", + "amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive", + "amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s", + "anchor": "Anchor", + "answering_machines": "Answering Machines", + "arcs_sparks": "Arcs Sparks", + "arrows_bows": "Arrows Bows", + "baby": "Baby", + "back_up_beeps": "Back Up Beeps", + "bars_restaurants": "Bars Restaurants", + "baseball": "Baseball", + "basketball": "Basketball", + "battles": "Battles", + "beeps_tones": "Beeps Tones", + "bell": "Bell", + "bikes": "Bikes", + "billiards": "Billiards", + "board_games": "Board Games", + "body": "Body", + "boing": "Boing", + "books": "Books", + "bow_wash": "Bow Wash", + "box": "Box", + "break_shatter_smash": "Break Shatter Smash", + "breaks": "Breaks", + "brooms_mops": "Brooms Mops", + "bullets": "Bullets", + "buses": "Buses", + "buzz": "Buzz", + "buzz_hums": "Buzz Hums", + "buzzers": "Buzzers", + "buzzers_pistols": "Buzzers Pistols", + "cables_metal": "Cables Metal", + "camera": "Camera", + "cannons": "Cannons", + "car_alarm": "Car Alarm", + "car_alarms": "Car Alarms", + "car_cell_phones": "Car Cell Phones", + "carnivals_fairs": "Carnivals Fairs", + "cars": "Cars", + "casino": "Casino", + "casinos": "Casinos", + "cellar": "Cellar", + "chimes": "Chimes", + "chimes_bells": "Chimes Bells", + "chorus": "Chorus", + "christmas": "Christmas", + "church_bells": "Church Bells", + "clock": "Clock", + "cloth": "Cloth", + "concrete": "Concrete", + "construction": "Construction", + "construction_factory": "Construction Factory", + "crashes": "Crashes", + "crowds": "Crowds", + "debris": "Debris", + "dining_kitchens": "Dining Kitchens", + "dinosaurs": "Dinosaurs", + "dripping": "Dripping", + "drops": "Drops", + "electric": "Electric", + "electrical": "Electrical", + "elevator": "Elevator", + "evolution_monsters": "Evolution Monsters", + "explosions": "Explosions", + "factory": "Factory", + "falls": "Falls", + "fax_scanner_copier": "Fax Scanner Copier", + "feedback_mics": "Feedback Mics", + "fight": "Fight", + "fire": "Fire", + "fire_extinguisher": "Fire Extinguisher", + "fireballs": "Fireballs", + "fireworks": "Fireworks", + "fishing_pole": "Fishing Pole", + "flags": "Flags", + "football": "Football", + "footsteps": "Footsteps", + "futuristic": "Futuristic", + "futuristic_ship": "Futuristic Ship", + "gameshow": "Gameshow", + "gear": "Gear", + "ghosts_demons": "Ghosts Demons", + "giant_monster": "Giant Monster", + "glass": "Glass", + "glasses_clink": "Glasses Clink", + "golf": "Golf", + "gorilla": "Gorilla", + "grenade_lanucher": "Grenade Lanucher", + "griffen": "Griffen", + "gyms_locker_rooms": "Gyms Locker Rooms", + "handgun_loading": "Handgun Loading", + "handgun_shot": "Handgun Shot", + "handle": "Handle", + "hands": "Hands", + "heartbeats_ekg": "Heartbeats EKG", + "helicopter": "Helicopter", + "high_tech": "High Tech", + "hit_punch_slap": "Hit Punch Slap", + "hits": "Hits", + "horns": "Horns", + "horror": "Horror", + "hot_tub_filling_up": "Hot Tub Filling Up", + "human": "Human", + "human_vocals": "Human Vocals", + "hygene": "Hygene", + "ice_skating": "Ice Skating", + "ignitions": "Ignitions", + "infantry": "Infantry", + "intro": "Intro", + "jet": "Jet", + "juggling": "Juggling", + "key_lock": "Key Lock", + "kids": "Kids", + "knocks": "Knocks", + "lab_equip": "Lab Equip", + "lacrosse": "Lacrosse", + "lamps_lanterns": "Lamps Lanterns", + "leather": "Leather", + "liquid_suction": "Liquid Suction", + "locker_doors": "Locker Doors", + "machine_gun": "Machine Gun", + "magic_spells": "Magic Spells", + "medium_large_explosions": "Medium Large Explosions", + "metal": "Metal", + "modern_rings": "Modern Rings", + "money_coins": "Money Coins", + "motorcycles": "Motorcycles", + "movement": "Movement", + "moves": "Moves", + "nature": "Nature", + "oar_boat": "Oar Boat", + "pagers": "Pagers", + "paintball": "Paintball", + "paper": "Paper", + "parachute": "Parachute", + "pay_phones": "Pay Phones", + "phone_beeps": "Phone Beeps", + "pigmy_bats": "Pigmy Bats", + "pills": "Pills", + "pour_water": "Pour Water", + "power_up_down": "Power Up Down", + "printers": "Printers", + "prison": "Prison", + "public_space": "Public Space", + "racquetball": "Racquetball", + "radios_static": "Radios Static", + "rain": "Rain", + "rc_airplane": "RC Airplane", + "rc_car": "RC Car", + "refrigerators_freezers": "Refrigerators Freezers", + "regular": "Regular", + "respirator": "Respirator", + "rifle": "Rifle", + "roller_coaster": "Roller Coaster", + "rollerskates_rollerblades": "RollerSkates RollerBlades", + "room_tones": "Room Tones", + "ropes_climbing": "Ropes Climbing", + "rotary_rings": "Rotary Rings", + "rowboat_canoe": "Rowboat Canoe", + "rubber": "Rubber", + "running": "Running", + "sails": "Sails", + "sand_gravel": "Sand Gravel", + "screen_doors": "Screen Doors", + "screens": "Screens", + "seats_stools": "Seats Stools", + "servos": "Servos", + "shoes_boots": "Shoes Boots", + "shotgun": "Shotgun", + "shower": "Shower", + "sink_faucet": "Sink Faucet", + "sink_filling_water": "Sink Filling Water", + "sink_run_and_off": "Sink Run And Off", + "sink_water_splatter": "Sink Water Splatter", + "sirens": "Sirens", + "skateboards": "Skateboards", + "ski": "Ski", + "skids_tires": "Skids Tires", + "sled": "Sled", + "slides": "Slides", + "small_explosions": "Small Explosions", + "snow": "Snow", + "snowmobile": "Snowmobile", + "soldiers": "Soldiers", + "splash_water": "Splash Water", + "splashes_sprays": "Splashes Sprays", + "sports_whistles": "Sports Whistles", + "squeaks": "Squeaks", + "squeaky": "Squeaky", + "stairs": "Stairs", + "steam": "Steam", + "submarine_diesel": "Submarine Diesel", + "swing_doors": "Swing Doors", + "switches_levers": "Switches Levers", + "swords": "Swords", + "tape": "Tape", + "tape_machine": "Tape Machine", + "televisions_shows": "Televisions Shows", + "tennis_pingpong": "Tennis PingPong", + "textile": "Textile", + "throw": "Throw", + "thunder": "Thunder", + "ticks": "Ticks", + "timer": "Timer", + "toilet_flush": "Toilet Flush", + "tone": "Tone", + "tones_noises": "Tones Noises", + "toys": "Toys", + "tractors": "Tractors", + "traffic": "Traffic", + "train": "Train", + "trucks_vans": "Trucks Vans", + "turnstiles": "Turnstiles", + "typing": "Typing", + "umbrella": "Umbrella", + "underwater": "Underwater", + "vampires": "Vampires", + "various": "Various", + "video_tunes": "Video Tunes", + "volcano_earthquake": "Volcano Earthquake", + "watches": "Watches", + "water": "Water", + "water_running": "Water Running", + "werewolves": "Werewolves", + "winches_gears": "Winches Gears", + "wind": "Wind", + "wood": "Wood", + "wood_boat": "Wood Boat", + "woosh": "Woosh", + "zap": "Zap", + "zippers": "Zippers" + } + } + }, "exceptions": { "cannot_connect_with_error": { "message": "Error connecting: {error}" }, "cannot_retrieve_data_with_error": { "message": "Error retrieving data: {error}" + }, + "device_serial_number_missing": { + "message": "Device serial number missing: {device_id}" + }, + "invalid_device_id": { + "message": "Invalid device ID specified: {device_id}" + }, + "invalid_sound_value": { + "message": "Invalid sound {sound} with variant {variant} specified" + }, + "entry_not_loaded": { + "message": "Entry not loaded: {entry}" } } } diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 8a2a182c796..0d0f5183566 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: "model": device.model, "sw_version": device.sw_version, "hw_version": device.hw_version, - "has_suggested_area": device.suggested_area is not None, "has_configuration_url": device.configuration_url is not None, "via_device": None, } diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index a34f191b7a7..3cf8d2e863d 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from pyasuswrt import AsusWrtError @@ -40,6 +40,9 @@ from .const import ( SENSORS_CONNECTED_DEVICE, ) +if TYPE_CHECKING: + from . import AsusWrtConfigEntry + CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] SCAN_INTERVAL = timedelta(seconds=30) @@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__) class AsusWrtSensorDataHandler: """Data handler for AsusWrt sensor.""" - def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None: + def __init__( + self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry + ) -> None: """Initialize a AsusWrt sensor data handler.""" self._hass = hass self._api = api + self._entry = entry self._connected_devices = 0 async def _get_connected_devices(self) -> dict[str, int]: @@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler: update_method=method, # Polling interval. Will only be polled if there are subscribers. update_interval=SCAN_INTERVAL if should_poll else None, + config_entry=self._entry, ) await coordinator.async_refresh() @@ -321,7 +328,9 @@ class AsusWrtRouter: if self._sensors_data_handler: return - self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) + self._sensors_data_handler = AsusWrtSensorDataHandler( + self.hass, self._api, self._entry + ) self._sensors_data_handler.update_device_count(self._connected_devices) sensors_types = await self._api.async_get_available_sensors() diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 2368c848eea..e7af7d84942 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index d27235123b9..fe7ccededf2 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -268,7 +268,7 @@ class LoginFlowBaseView(HomeAssistantView): result.pop("data") result.pop("context") - result_obj: Credentials = result.pop("result") + result_obj = result.pop("result") # Result can be None if credential was never linked to a user before. user = await hass.auth.async_get_user_by_credentials(result_obj) @@ -281,7 +281,8 @@ class LoginFlowBaseView(HomeAssistantView): ) process_success_login(request) - result["result"] = self._store_result(client_id, result_obj) + # We overwrite the Credentials object with the string code to retrieve it. + result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item] return self.json(result) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index e7fc1262f6d..f1b2f7d5b97 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1119,7 +1119,7 @@ class BackupManager: ) if unavailable_agents: LOGGER.warning( - "Backup agents %s are not available, will backupp to %s", + "Backup agents %s are not available, will backup to %s", unavailable_agents, available_agents, ) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index caf5cc7541d..54fb061676d 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["pyblu==2.0.1"], + "requirements": ["pyblu==2.0.4"], "zeroconf": [ { "type": "_musc._tcp.local." diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3b1e6e70ff6..cd6aae91259 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,7 +20,7 @@ "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", - "dbus-fast==2.44.2", + "dbus-fast==2.44.3", "habluetooth==4.0.1" ] } diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 7c1644fff54..8fdbb5054a8 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]): device.hass, _LOGGER, name=f"{device.name} ({device.api.model} at {device.api.host[0]})", + config_entry=device.config, update_method=self.async_update, update_interval=self.SCAN_INTERVAL, ) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 6abfe57a4ae..5f4f67a114a 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any -from bsblan import BSBLAN, BSBLANConfig, BSBLANError +from bsblan import BSBLAN, BSBLANAuthError, BSBLANConfig, BSBLANError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -45,7 +46,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): self.username = user_input.get(CONF_USERNAME) self.password = user_input.get(CONF_PASSWORD) - return await self._validate_and_create() + return await self._validate_and_create(user_input) async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo @@ -128,14 +129,29 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): self.username = user_input.get(CONF_USERNAME) self.password = user_input.get(CONF_PASSWORD) - return await self._validate_and_create(is_discovery=True) + return await self._validate_and_create(user_input, is_discovery=True) async def _validate_and_create( - self, is_discovery: bool = False + self, user_input: dict[str, Any], is_discovery: bool = False ) -> ConfigFlowResult: """Validate device connection and create entry.""" try: - await self._get_bsblan_info(is_discovery=is_discovery) + await self._get_bsblan_info() + except BSBLANAuthError: + if is_discovery: + return self.async_show_form( + step_id="discovery_confirm", + data_schema=vol.Schema( + { + vol.Optional(CONF_PASSKEY): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + } + ), + errors={"base": "invalid_auth"}, + description_placeholders={"host": str(self.host)}, + ) + return self._show_setup_form({"base": "invalid_auth"}, user_input) except BSBLANError: if is_discovery: return self.async_show_form( @@ -154,18 +170,137 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth flow.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth confirmation flow.""" + existing_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert existing_entry + + if user_input is None: + # Preserve existing values as defaults + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=existing_entry.data.get( + CONF_PASSKEY, vol.UNDEFINED + ), + ): str, + vol.Optional( + CONF_USERNAME, + default=existing_entry.data.get( + CONF_USERNAME, vol.UNDEFINED + ), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + ) + + # Combine existing data with the user's new input for validation. + # This correctly handles adding, changing, and clearing credentials. + config_data = existing_entry.data.copy() + config_data.update(user_input) + + self.host = config_data[CONF_HOST] + self.port = config_data[CONF_PORT] + self.passkey = config_data.get(CONF_PASSKEY) + self.username = config_data.get(CONF_USERNAME) + self.password = config_data.get(CONF_PASSWORD) + + try: + await self._get_bsblan_info(raise_on_progress=False, is_reauth=True) + except BSBLANAuthError: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=user_input.get(CONF_PASSKEY, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + errors={"base": "invalid_auth"}, + ) + except BSBLANError: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PASSKEY, + default=user_input.get(CONF_PASSKEY, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=vol.UNDEFINED, + ): str, + } + ), + errors={"base": "cannot_connect"}, + ) + + # Update only the fields that were provided by the user + return self.async_update_reload_and_abort( + existing_entry, data_updates=user_input, reason="reauth_successful" + ) + @callback - def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: + def _show_setup_form( + self, errors: dict | None = None, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Show the setup form to the user.""" + # Preserve user input if provided, otherwise use defaults + defaults = user_input or {} + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional(CONF_PASSKEY): str, - vol.Optional(CONF_USERNAME): str, - vol.Optional(CONF_PASSWORD): str, + vol.Required( + CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional( + CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_USERNAME, + default=defaults.get(CONF_USERNAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_PASSWORD, + default=defaults.get(CONF_PASSWORD, vol.UNDEFINED), + ): str, } ), errors=errors or {}, @@ -186,7 +321,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): ) async def _get_bsblan_info( - self, raise_on_progress: bool = True, is_discovery: bool = False + self, + raise_on_progress: bool = True, + is_reauth: bool = False, ) -> None: """Get device information from a BSBLAN device.""" config = BSBLANConfig( @@ -209,11 +346,13 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): format_mac(self.mac), raise_on_progress=raise_on_progress ) - # Always allow updating host/port for both user and discovery flows - # This ensures connectivity is maintained when devices change IP addresses - self._abort_if_unique_id_configured( - updates={ - CONF_HOST: self.host, - CONF_PORT: self.port, - } - ) + # Skip unique_id configuration check during reauth to prevent "already_configured" abort + if not is_reauth: + # Always allow updating host/port for both user and discovery flows + # This ensures connectivity is maintained when devices change IP addresses + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.host, + CONF_PORT: self.port, + } + ) diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py index 5c5e23efa8a..38a19dba8ea 100644 --- a/homeassistant/components/bsblan/coordinator.py +++ b/homeassistant/components/bsblan/coordinator.py @@ -4,11 +4,19 @@ from dataclasses import dataclass from datetime import timedelta from random import randint -from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State +from bsblan import ( + BSBLAN, + BSBLANAuthError, + BSBLANConnectionError, + HotWaterState, + Sensor, + State, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SCAN_INTERVAL @@ -62,6 +70,10 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]): state = await self.client.state() sensor = await self.client.sensor() dhw = await self.client.hot_water_state() + except BSBLANAuthError as err: + raise ConfigEntryAuthFailed( + "Authentication failed for BSB-Lan device" + ) from err except BSBLANConnectionError as err: host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown" raise UpdateFailed( diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index cd4633dfb86..86e52e76f41 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -33,14 +33,25 @@ "username": "[%key:component::bsblan::config::step::user::data_description::username%]", "password": "[%key:component::bsblan::config::step::user::data_description::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The BSB-Lan integration needs to re-authenticate with {name}", + "data": { + "passkey": "[%key:component::bsblan::config::step::user::data::passkey%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "exceptions": { diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index a819203e549..63eae6261d4 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.110.0"], + "requirements": ["hass-nabucasa==0.110.1"], "single_config_entry": true } diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 9ee154dbff4..c1b8fc095c3 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -4,11 +4,13 @@ from __future__ import annotations import asyncio import logging -from typing import Any -from aiohttp.client_exceptions import ClientError -from hass_nabucasa import Cloud, cloud_api -from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo +from hass_nabucasa import ( + Cloud, + MigratePaypalAgreementInfo, + PaymentsApiError, + SubscriptionInfo, +) from .client import CloudClient from .const import REQUEST_TIMEOUT @@ -29,17 +31,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo async def async_migrate_paypal_agreement( cloud: Cloud[CloudClient], -) -> dict[str, Any] | None: +) -> MigratePaypalAgreementInfo | None: """Migrate a paypal agreement from legacy.""" try: async with asyncio.timeout(REQUEST_TIMEOUT): - return await cloud_api.async_migrate_paypal_agreement(cloud) + return await cloud.payments.migrate_paypal_agreement() except TimeoutError: _LOGGER.error( "A timeout of %s was reached while trying to start agreement migration", REQUEST_TIMEOUT, ) - except ClientError as exception: + except PaymentsApiError as exception: _LOGGER.error("Failed to start agreement migration - %s", exception) return None diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d20d4de881f..a9aafcfaa5e 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -146,8 +146,9 @@ def _prepare_config_flow_result_json( return prepare_result_json(result) data = result.copy() - entry: config_entries.ConfigEntry = data["result"] - data["result"] = entry.as_json_fragment + entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item] + # We overwrite the ConfigEntry object with its json representation. + data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key] data.pop("data") data.pop("context") return data diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ad0a4c96102..31adffad064 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"] } diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 606f34c9ae0..219f3afe4e2 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -75,7 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> b prefix = options[CONF_PREFIX] sample_rate = options[CONF_RATE] - statsd_client = DogStatsd(host=host, port=port, namespace=prefix) + statsd_client = DogStatsd( + host=host, port=port, namespace=prefix, disable_telemetry=True + ) entry.runtime_data = statsd_client initialize(statsd_host=host, statsd_port=port) diff --git a/homeassistant/components/datadog/config_flow.py b/homeassistant/components/datadog/config_flow.py index 876b79b6019..a2ad74e2c57 100644 --- a/homeassistant/components/datadog/config_flow.py +++ b/homeassistant/components/datadog/config_flow.py @@ -58,7 +58,6 @@ class DatadogConfigFlow(ConfigFlow, domain=DOMAIN): CONF_RATE: user_input[CONF_RATE], }, ) - return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -107,7 +106,26 @@ class DatadogOptionsFlowHandler(OptionsFlow): options = self.config_entry.options if user_input is None: - user_input = {} + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_PREFIX, + default=options.get( + CONF_PREFIX, data.get(CONF_PREFIX, DEFAULT_PREFIX) + ), + ): str, + vol.Required( + CONF_RATE, + default=options.get( + CONF_RATE, data.get(CONF_RATE, DEFAULT_RATE) + ), + ): int, + } + ), + errors={}, + ) success = await validate_datadog_connection( self.hass, diff --git a/homeassistant/components/datadog/const.py b/homeassistant/components/datadog/const.py index e9e5d80eeba..7c9a0311228 100644 --- a/homeassistant/components/datadog/const.py +++ b/homeassistant/components/datadog/const.py @@ -4,7 +4,7 @@ DOMAIN = "datadog" CONF_RATE = "rate" -DEFAULT_HOST = "localhost" +DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 8125 DEFAULT_PREFIX = "hass" DEFAULT_RATE = 1 diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 815446b9ab4..798a314e307 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["datadog"], "quality_scale": "legacy", - "requirements": ["datadog==0.15.0"] + "requirements": ["datadog==0.52.0"] } diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c5a1b9aeb63..8fea21b707e 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.1.1"], + "requirements": ["denonavr==1.1.2"], "ssdp": [ { "manufacturer": "Denon", diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index ea2a4f4f820..599e5ecae5b 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.2.0", - "aiodiscover==2.7.0", + "aiodiscover==2.7.1", "cached-ipaddress==0.10.0" ] } diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index d432410c8c5..86a30558375 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any from deebot_client.capabilities import Capabilities, DeviceType from deebot_client.device import Device -from deebot_client.events import BatteryEvent, FanSpeedEvent, RoomsEvent, StateEvent +from deebot_client.events import FanSpeedEvent, RoomsEvent, StateEvent from deebot_client.models import CleanAction, CleanMode, Room, State import sucks @@ -216,7 +216,6 @@ class EcovacsVacuum( VacuumEntityFeature.PAUSE | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.BATTERY | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.LOCATE | VacuumEntityFeature.STATE @@ -243,10 +242,6 @@ class EcovacsVacuum( """Set up the event listeners now that hass is ready.""" await super().async_added_to_hass() - async def on_battery(event: BatteryEvent) -> None: - self._attr_battery_level = event.value - self.async_write_ha_state() - async def on_rooms(event: RoomsEvent) -> None: self._rooms = event.rooms self.async_write_ha_state() @@ -255,7 +250,6 @@ class EcovacsVacuum( self._attr_activity = _STATE_TO_VACUUM_STATE[event.state] self.async_write_ha_state() - self._subscribe(self._capability.battery.event, on_battery) self._subscribe(self._capability.state.event, on_status) if self._capability.fan_speed: diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index c7f18cb205e..bc86e6e9bab 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emoncms", "iot_class": "local_polling", - "requirements": ["pyemoncms==0.1.1"] + "requirements": ["pyemoncms==0.1.2"] } diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json index 900e8dd0474..f68ea92d26c 100644 --- a/homeassistant/components/emoncms/strings.json +++ b/homeassistant/components/emoncms/strings.json @@ -12,12 +12,26 @@ }, "data_description": { "url": "Server URL starting with the protocol (http or https)", - "api_key": "Your 32 bits API key" + "api_key": "Your 32 bits API key", + "sync_mode": "Pick your feeds manually (default) or synchronize them at once" } }, "choose_feeds": { "data": { "include_only_feed_id": "Choose feeds to include" + }, + "data_description": { + "include_only_feed_id": "Pick the feeds you want to synchronize" + } + }, + "reconfigure": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "url": "[%key:component::emoncms::config::step::user::data_description::url%]", + "api_key": "[%key:component::emoncms::config::step::user::data_description::api_key%]" } } }, @@ -30,8 +44,8 @@ "selector": { "sync_mode": { "options": { - "auto": "Synchronize all available Feeds", - "manual": "Select which Feeds to synchronize" + "auto": "Synchronize all available feeds", + "manual": "Select which feeds to synchronize" } } }, @@ -89,6 +103,9 @@ "init": { "data": { "include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data::include_only_feed_id%]" + }, + "data_description": { + "include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data_description::include_only_feed_id%]" } } } diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index a1a9d4ed6b4..93244068feb 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -116,6 +116,9 @@ async def async_get_config_entry_diagnostics( entities.append({"entity": entity_dict, "state": state_dict}) device_dict = asdict(device) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") + device_dict.pop("is_new", None) device_entities.append({"device": device_dict, "entities": entities}) # remove envoy serial diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 320179bf2df..e337dac74e0 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.2.2"], + "requirements": ["pyenphase==2.2.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 75408246e78..4efb0e494ef 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -51,6 +51,7 @@ from .const import ( DOMAIN, ) from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info +from .encryption_key_storage import async_get_encryption_key_storage from .entry_data import ESPHomeConfigEntry from .manager import async_replace_device @@ -159,7 +160,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle reauthorization flow.""" errors = {} - if await self._retrieve_encryption_key_from_dashboard(): + if ( + await self._retrieve_encryption_key_from_storage() + or await self._retrieve_encryption_key_from_dashboard() + ): error = await self.fetch_device_info() if error is None: return await self._async_authenticate_or_add() @@ -226,9 +230,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): response = await self.fetch_device_info() self._noise_psk = None + # Try to retrieve an existing key from dashboard or storage. if ( self._device_name and await self._retrieve_encryption_key_from_dashboard() + ) or ( + self._device_mac and await self._retrieve_encryption_key_from_storage() ): response = await self.fetch_device_info() @@ -284,6 +291,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = discovery_info.properties.get("friendly_name", device_name) self._host = discovery_info.host self._port = discovery_info.port + self._device_mac = mac_address self._noise_required = bool(discovery_info.properties.get("api_encryption")) # Check if already configured @@ -308,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # Don't call _fetch_device_info() for ignored entries raise AbortFlow("already_configured") configured_host: str | None = entry.data.get(CONF_HOST) - configured_port: int | None = entry.data.get(CONF_PORT) - if configured_host == host and configured_port == port: + configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT) + # When port is None (from DHCP discovery), only compare hosts + if configured_host == host and (port is None or configured_port == port): # Don't probe to verify the mac is correct since - # the host and port matches. + # the host matches (and port matches if provided). raise AbortFlow("already_configured") configured_psk: str | None = entry.data.get(CONF_NOISE_PSK) await self._fetch_device_info(host, port or configured_port, configured_psk) @@ -772,6 +781,26 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._noise_psk = noise_psk return True + async def _retrieve_encryption_key_from_storage(self) -> bool: + """Try to retrieve the encryption key from storage. + + Return boolean if a key was retrieved. + """ + # Try to get MAC address from current flow state or reauth entry + mac_address = self._device_mac + if mac_address is None and self._reauth_entry is not None: + # In reauth flow, get MAC from the existing entry's unique_id + mac_address = self._reauth_entry.unique_id + + assert mac_address is not None + + storage = await async_get_encryption_key_storage(self.hass) + if stored_key := await storage.async_get_key(mac_address): + self._noise_psk = stored_key + return True + + return False + @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/esphome/encryption_key_storage.py b/homeassistant/components/esphome/encryption_key_storage.py new file mode 100644 index 00000000000..e4b5ef41c2e --- /dev/null +++ b/homeassistant/components/esphome/encryption_key_storage.py @@ -0,0 +1,94 @@ +"""Encryption key storage for ESPHome devices.""" + +from __future__ import annotations + +import logging +from typing import TypedDict + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.singleton import singleton +from homeassistant.helpers.storage import Store +from homeassistant.util.hass_dict import HassKey + +_LOGGER = logging.getLogger(__name__) + +ENCRYPTION_KEY_STORAGE_VERSION = 1 +ENCRYPTION_KEY_STORAGE_KEY = "esphome.encryption_keys" + + +class EncryptionKeyData(TypedDict): + """Encryption key storage data.""" + + keys: dict[str, str] # MAC address -> base64 encoded key + + +KEY_ENCRYPTION_STORAGE: HassKey[ESPHomeEncryptionKeyStorage] = HassKey( + "esphome_encryption_key_storage" +) + + +class ESPHomeEncryptionKeyStorage: + """Storage for ESPHome encryption keys.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the encryption key storage.""" + self.hass = hass + self._store = Store[EncryptionKeyData]( + hass, + ENCRYPTION_KEY_STORAGE_VERSION, + ENCRYPTION_KEY_STORAGE_KEY, + encoder=JSONEncoder, + ) + self._data: EncryptionKeyData | None = None + + async def async_load(self) -> None: + """Load encryption keys from storage.""" + if self._data is None: + data = await self._store.async_load() + self._data = data or {"keys": {}} + + async def async_save(self) -> None: + """Save encryption keys to storage.""" + if self._data is not None: + await self._store.async_save(self._data) + + async def async_get_key(self, mac_address: str) -> str | None: + """Get encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + return self._data["keys"].get(mac_address.lower()) + + async def async_store_key(self, mac_address: str, key: str) -> None: + """Store encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + self._data["keys"][mac_address.lower()] = key + await self.async_save() + _LOGGER.debug( + "Stored encryption key for device with MAC %s", + mac_address, + ) + + async def async_remove_key(self, mac_address: str) -> None: + """Remove encryption key for a MAC address.""" + await self.async_load() + assert self._data is not None + lower_mac_address = mac_address.lower() + if lower_mac_address in self._data["keys"]: + del self._data["keys"][lower_mac_address] + await self.async_save() + _LOGGER.debug( + "Removed encryption key for device with MAC %s", + mac_address, + ) + + +@singleton(KEY_ENCRYPTION_STORAGE, async_=True) +async def async_get_encryption_key_storage( + hass: HomeAssistant, +) -> ESPHomeEncryptionKeyStorage: + """Get the encryption key storage instance.""" + storage = ESPHomeEncryptionKeyStorage(hass) + await storage.async_load() + return storage diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 5e9e11171af..4d5de77b1e0 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -3,8 +3,10 @@ from __future__ import annotations import asyncio +import base64 from functools import partial import logging +import secrets from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( @@ -68,6 +70,7 @@ from .const import ( CONF_ALLOW_SERVICE_CALLS, CONF_BLUETOOTH_MAC_ADDRESS, CONF_DEVICE_NAME, + CONF_NOISE_PSK, CONF_SUBSCRIBE_LOGS, DEFAULT_ALLOW_SERVICE_CALLS, DEFAULT_URL, @@ -78,6 +81,7 @@ from .const import ( ) from .dashboard import async_get_dashboard from .domain_data import DomainData +from .encryption_key_storage import async_get_encryption_key_storage # Import config flow so that it's added to the registry from .entry_data import ESPHomeConfigEntry, RuntimeEntryData @@ -85,9 +89,7 @@ from .entry_data import ESPHomeConfigEntry, RuntimeEntryData DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}" if TYPE_CHECKING: - from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined] - SubscribeLogsResponse, - ) + from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001 _LOGGER = logging.getLogger(__name__) @@ -515,6 +517,8 @@ class ESPHomeManager: assert api_version is not None, "API version must be set" entry_data.async_on_connect(device_info, api_version) + await self._handle_dynamic_encryption_key(device_info) + if device_info.name: reconnect_logic.name = device_info.name @@ -618,6 +622,7 @@ class ESPHomeManager: ), ): return + if isinstance(err, InvalidEncryptionKeyAPIError): if ( (received_name := err.received_name) @@ -648,6 +653,93 @@ class ESPHomeManager: return self.entry.async_start_reauth(self.hass) + async def _handle_dynamic_encryption_key( + self, device_info: EsphomeDeviceInfo + ) -> None: + """Handle dynamic encryption keys. + + If a device reports it supports encryption, but we connected without a key, + we need to generate and store one. + """ + noise_psk: str | None = self.entry.data.get(CONF_NOISE_PSK) + if noise_psk: + # we're already connected with a noise PSK - nothing to do + return + + if not device_info.api_encryption_supported: + # device does not support encryption - nothing to do + return + + # Connected to device without key and the device supports encryption + storage = await async_get_encryption_key_storage(self.hass) + + # First check if we have a key in storage for this device + from_storage: bool = False + if self.entry.unique_id and ( + stored_key := await storage.async_get_key(self.entry.unique_id) + ): + _LOGGER.debug( + "Retrieved encryption key from storage for device %s", + self.entry.unique_id, + ) + # Use the stored key + new_key = stored_key.encode() + new_key_str = stored_key + from_storage = True + else: + # No stored key found, generate a new one + _LOGGER.debug( + "Generating new encryption key for device %s", self.entry.unique_id + ) + new_key = base64.b64encode(secrets.token_bytes(32)) + new_key_str = new_key.decode() + + try: + # Store the key on the device using the existing connection + result = await self.cli.noise_encryption_set_key(new_key) + except APIConnectionError as ex: + _LOGGER.error( + "Connection error while storing encryption key for device %s (%s): %s", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ex, + ) + return + else: + if not result: + _LOGGER.error( + "Failed to set dynamic encryption key on device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + return + + # Key stored successfully on device + assert self.entry.unique_id is not None + + # Only store in storage if it was newly generated + if not from_storage: + await storage.async_store_key(self.entry.unique_id, new_key_str) + + # Always update config entry + self.hass.config_entries.async_update_entry( + self.entry, + data={**self.entry.data, CONF_NOISE_PSK: new_key_str}, + ) + + if from_storage: + _LOGGER.info( + "Set encryption key from storage on device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + else: + _LOGGER.info( + "Generated and stored encryption key for device %s (%s)", + self.entry.data.get(CONF_DEVICE_NAME, self.host), + self.entry.unique_id, + ) + @callback def _async_handle_logging_changed(self, _event: Event) -> None: """Handle when the logging level changes.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 17fd72fc939..6bf164aa9bc 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.1.2", + "aioesphomeapi==37.2.2", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index c441b34b42d..ec631e8e5c1 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -66,6 +66,26 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="last_alarm_type_name", translation_key="last_alarm_type_name", ), + "Record_Mode": SensorEntityDescription( + key="Record_Mode", + translation_key="record_mode", + entity_registry_enabled_default=False, + ), + "battery_camera_work_mode": SensorEntityDescription( + key="battery_camera_work_mode", + translation_key="battery_camera_work_mode", + entity_registry_enabled_default=False, + ), + "powerStatus": SensorEntityDescription( + key="powerStatus", + translation_key="power_status", + entity_registry_enabled_default=False, + ), + "OnlineStatus": SensorEntityDescription( + key="OnlineStatus", + translation_key="online_status", + entity_registry_enabled_default=False, + ), } @@ -76,16 +96,26 @@ async def async_setup_entry( ) -> None: """Set up EZVIZ sensors based on a config entry.""" coordinator = entry.runtime_data + entities: list[EzvizSensor] = [] - async_add_entities( - [ + for camera, sensors in coordinator.data.items(): + entities.extend( EzvizSensor(coordinator, camera, sensor) - for camera in coordinator.data - for sensor, value in coordinator.data[camera].items() - if sensor in SENSOR_TYPES - if value is not None - ] - ) + for sensor, value in sensors.items() + if sensor in SENSOR_TYPES and value is not None + ) + + optionals = sensors.get("optionals", {}) + entities.extend( + EzvizSensor(coordinator, camera, optional_key) + for optional_key in ("powerStatus", "OnlineStatus") + if optional_key in optionals + ) + + if "mode" in optionals.get("Record_Mode", {}): + entities.append(EzvizSensor(coordinator, camera, "mode")) + + async_add_entities(entities) class EzvizSensor(EzvizEntity, SensorEntity): diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index b03a5dbc61a..ad8f7114407 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -147,6 +147,18 @@ }, "last_alarm_type_name": { "name": "Last alarm type name" + }, + "record_mode": { + "name": "Record mode" + }, + "battery_camera_work_mode": { + "name": "Battery work mode" + }, + "power_status": { + "name": "Power status" + }, + "online_status": { + "name": "Online status" } }, "switch": { diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 8a3d1ebf04c..cfbdfbcb424 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -106,6 +106,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_logger_{self.host}", + config_entry=self.config_entry, ) await self.logger_coordinator.async_config_entry_first_refresh() @@ -120,6 +121,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_meters_{self.host}", + config_entry=self.config_entry, ) ) @@ -129,6 +131,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_ohmpilot_{self.host}", + config_entry=self.config_entry, ) ) @@ -138,6 +141,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_power_flow_{self.host}", + config_entry=self.config_entry, ) ) @@ -147,6 +151,7 @@ class FroniusSolarNet: solar_net=self, logger=_LOGGER, name=f"{DOMAIN}_storages_{self.host}", + config_entry=self.config_entry, ) ) @@ -206,6 +211,7 @@ class FroniusSolarNet: logger=_LOGGER, name=_inverter_name, inverter_info=_inverter_info, + config_entry=self.config_entry, ) if self.config_entry.state == ConfigEntryState.LOADED: await _coordinator.async_refresh() diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 791acf8a39c..706940f5da7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250702.3"] + "requirements": ["home-assistant-frontend==20250731.0"] } diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 5e36087e9e4..152e629be2e 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -141,9 +141,7 @@ async def light_switch_options_schema( """Generate options schema.""" return (await basic_group_options_schema(domain, handler)).extend( { - vol.Required( - CONF_ALL, default=False, description={"advanced": True} - ): selector.BooleanSelector(), + vol.Required(CONF_ALL, default=False): selector.BooleanSelector(), } ) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index b80b78027bf..bb9ab4b25d8 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -21,12 +21,14 @@ }, "binary_sensor": { "title": "[%key:component::group::config::step::user::title%]", - "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "data": { "all": "All entities", "entities": "Members", "hide_members": "Hide members", "name": "[%key:common::config_flow::data::name%]" + }, + "data_description": { + "all": "If enabled, the group's state is on only if all members are on. If disabled, the group's state is on if any member is on." } }, "button": { @@ -105,6 +107,9 @@ "device_class": "Device class", "state_class": "State class", "unit_of_measurement": "Unit of measurement" + }, + "data_description": { + "ignore_non_numeric": "If enabled, the group's state is calculated if at least one member has a numerical value. If disabled, the group's state is calculated only if all group members have numerical values." } }, "switch": { @@ -120,11 +125,13 @@ "options": { "step": { "binary_sensor": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } }, "button": { @@ -146,11 +153,13 @@ } }, "light": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } }, "lock": { @@ -172,7 +181,6 @@ } }, "sensor": { - "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "data": { "ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", @@ -182,14 +190,19 @@ "device_class": "[%key:component::group::config::step::sensor::data::device_class%]", "state_class": "[%key:component::group::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::group::config::step::sensor::data::unit_of_measurement%]" + }, + "data_description": { + "ignore_non_numeric": "[%key:component::group::config::step::sensor::data_description::ignore_non_numeric%]" } }, "switch": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } } } diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 7b3e67228b1..b6a730835bb 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.6.0"] + "requirements": ["growattServer==1.7.1"] } diff --git a/homeassistant/components/habitica/button.py b/homeassistant/components/habitica/button.py index c57ba39fb6a..de8920deb77 100644 --- a/homeassistant/components/habitica/button.py +++ b/homeassistant/components/habitica/button.py @@ -7,15 +7,7 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from aiohttp import ClientError -from habiticalib import ( - HabiticaClass, - HabiticaException, - NotAuthorizedError, - Skill, - TaskType, - TooManyRequestsError, -) +from habiticalib import Habitica, HabiticaClass, Skill, TaskType from homeassistant.components.button import ( DOMAIN as BUTTON_DOMAIN, @@ -23,16 +15,11 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ASSETS_URL, DOMAIN -from .coordinator import ( - HabiticaConfigEntry, - HabiticaData, - HabiticaDataUpdateCoordinator, -) +from .coordinator import HabiticaConfigEntry, HabiticaData from .entity import HabiticaBase PARALLEL_UPDATES = 1 @@ -42,7 +29,7 @@ PARALLEL_UPDATES = 1 class HabiticaButtonEntityDescription(ButtonEntityDescription): """Describes Habitica button entity.""" - press_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + press_fn: Callable[[Habitica], Any] available_fn: Callable[[HabiticaData], bool] class_needed: HabiticaClass | None = None entity_picture: str | None = None @@ -73,13 +60,13 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.RUN_CRON, translation_key=HabiticaButtonEntity.RUN_CRON, - press_fn=lambda coordinator: coordinator.habitica.run_cron(), + press_fn=lambda habitica: habitica.run_cron(), available_fn=lambda data: data.user.needsCron is True, ), HabiticaButtonEntityDescription( key=HabiticaButtonEntity.BUY_HEALTH_POTION, translation_key=HabiticaButtonEntity.BUY_HEALTH_POTION, - press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(), + press_fn=lambda habitica: habitica.buy_health_potion(), available_fn=( lambda data: (data.user.stats.gp or 0) >= 25 and (data.user.stats.hp or 0) < 50 @@ -89,7 +76,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS, translation_key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS, - press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(), + press_fn=lambda habitica: habitica.allocate_stat_points(), available_fn=( lambda data: data.user.preferences.automaticAllocation is True and (data.user.stats.points or 0) > 0 @@ -98,7 +85,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.REVIVE, translation_key=HabiticaButtonEntity.REVIVE, - press_fn=lambda coordinator: coordinator.habitica.revive(), + press_fn=lambda habitica: habitica.revive(), available_fn=lambda data: data.user.stats.hp == 0, ), ) @@ -108,9 +95,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.MPHEAL, translation_key=HabiticaButtonEntity.MPHEAL, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.ETHEREAL_SURGE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 30 @@ -121,7 +106,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.EARTH, translation_key=HabiticaButtonEntity.EARTH, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE), + press_fn=lambda habitica: habitica.cast_skill(Skill.EARTHQUAKE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 35 @@ -132,9 +117,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.FROST, translation_key=HabiticaButtonEntity.FROST, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.CHILLING_FROST), # chilling frost can only be cast once per day (streaks buff is false) available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 @@ -147,9 +130,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.DEFENSIVE_STANCE, translation_key=HabiticaButtonEntity.DEFENSIVE_STANCE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.DEFENSIVE_STANCE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 25 @@ -160,9 +141,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.VALOROUS_PRESENCE, translation_key=HabiticaButtonEntity.VALOROUS_PRESENCE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.VALOROUS_PRESENCE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 20 @@ -173,9 +152,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.INTIMIDATE, translation_key=HabiticaButtonEntity.INTIMIDATE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.INTIMIDATING_GAZE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 and (data.user.stats.mp or 0) >= 15 @@ -186,11 +163,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.TOOLS_OF_TRADE, translation_key=HabiticaButtonEntity.TOOLS_OF_TRADE, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill( - Skill.TOOLS_OF_THE_TRADE - ) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.TOOLS_OF_THE_TRADE), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 25 @@ -201,7 +174,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.STEALTH, translation_key=HabiticaButtonEntity.STEALTH, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH), + press_fn=lambda habitica: habitica.cast_skill(Skill.STEALTH), # Stealth buffs stack and it can only be cast if the amount of # buffs is smaller than the amount of unfinished dailies available_fn=( @@ -224,9 +197,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.HEAL, translation_key=HabiticaButtonEntity.HEAL, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.HEALING_LIGHT), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 11 and (data.user.stats.mp or 0) >= 15 @@ -238,11 +209,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.BRIGHTNESS, translation_key=HabiticaButtonEntity.BRIGHTNESS, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill( - Skill.SEARING_BRIGHTNESS - ) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.SEARING_BRIGHTNESS), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 12 and (data.user.stats.mp or 0) >= 15 @@ -253,9 +220,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.PROTECT_AURA, translation_key=HabiticaButtonEntity.PROTECT_AURA, - press_fn=( - lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA) - ), + press_fn=lambda habitica: habitica.cast_skill(Skill.PROTECTIVE_AURA), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 13 and (data.user.stats.mp or 0) >= 30 @@ -266,7 +231,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = ( HabiticaButtonEntityDescription( key=HabiticaButtonEntity.HEAL_ALL, translation_key=HabiticaButtonEntity.HEAL_ALL, - press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING), + press_fn=lambda habitica: habitica.cast_skill(Skill.BLESSING), available_fn=( lambda data: (data.user.stats.lvl or 0) >= 14 and (data.user.stats.mp or 0) >= 25 @@ -332,33 +297,9 @@ class HabiticaButton(HabiticaBase, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - try: - await self.entity_description.press_fn(self.coordinator) - except TooManyRequestsError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="setup_rate_limit_exception", - translation_placeholders={"retry_after": str(e.retry_after)}, - ) from e - except NotAuthorizedError as e: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="service_call_unallowed", - ) from e - except HabiticaException as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": e.error.message}, - ) from e - except ClientError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": str(e)}, - ) from e - else: - await self.coordinator.async_request_refresh() + + await self.coordinator.execute(self.entity_description.press_fn) + await self.coordinator.async_request_refresh() @property def available(self) -> bool: diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index b25edc7ceaf..0e0a2db8d58 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -28,6 +28,7 @@ from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError, + ServiceValidationError, ) from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -130,19 +131,22 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): else: return HabiticaData(user=user, tasks=tasks + completed_todos) - async def execute( - self, func: Callable[[HabiticaDataUpdateCoordinator], Any] - ) -> None: + async def execute(self, func: Callable[[Habitica], Any]) -> None: """Execute an API call.""" try: - await func(self) + await func(self.habitica) except TooManyRequestsError as e: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="setup_rate_limit_exception", translation_placeholders={"retry_after": str(e.retry_after)}, ) from e + except NotAuthorizedError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_unallowed", + ) from e except HabiticaException as e: raise HomeAssistantError( translation_domain=DOMAIN, diff --git a/homeassistant/components/habitica/switch.py b/homeassistant/components/habitica/switch.py index fb98460f7e5..826cd341bba 100644 --- a/homeassistant/components/habitica/switch.py +++ b/homeassistant/components/habitica/switch.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any +from habiticalib import Habitica + from homeassistant.components.switch import ( SwitchDeviceClass, SwitchEntity, @@ -15,11 +17,7 @@ from homeassistant.components.switch import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .coordinator import ( - HabiticaConfigEntry, - HabiticaData, - HabiticaDataUpdateCoordinator, -) +from .coordinator import HabiticaConfigEntry, HabiticaData from .entity import HabiticaBase PARALLEL_UPDATES = 1 @@ -29,8 +27,8 @@ PARALLEL_UPDATES = 1 class HabiticaSwitchEntityDescription(SwitchEntityDescription): """Describes Habitica switch entity.""" - turn_on_fn: Callable[[HabiticaDataUpdateCoordinator], Any] - turn_off_fn: Callable[[HabiticaDataUpdateCoordinator], Any] + turn_on_fn: Callable[[Habitica], Any] + turn_off_fn: Callable[[Habitica], Any] is_on_fn: Callable[[HabiticaData], bool | None] @@ -45,8 +43,8 @@ SWTICH_DESCRIPTIONS: tuple[HabiticaSwitchEntityDescription, ...] = ( key=HabiticaSwitchEntity.SLEEP, translation_key=HabiticaSwitchEntity.SLEEP, device_class=SwitchDeviceClass.SWITCH, - turn_on_fn=lambda coordinator: coordinator.habitica.toggle_sleep(), - turn_off_fn=lambda coordinator: coordinator.habitica.toggle_sleep(), + turn_on_fn=lambda habitica: habitica.toggle_sleep(), + turn_off_fn=lambda habitica: habitica.toggle_sleep(), is_on_fn=lambda data: data.user.preferences.sleep, ), ) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 6d67b4b79c0..1e312ee34d9 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -9,6 +9,7 @@ "healthy": "Healthy", "host_os": "Host operating system", "installed_addons": "Installed add-ons", + "nameservers": "Nameservers", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor version", "supported": "Supported", diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index bc8da2a2a92..0a7e9b51e97 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -54,6 +54,15 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: "error": "Unsupported", } + nameservers = set() + for interface in network_info.get("interfaces", []): + if not interface.get("primary"): + continue + if ipv4 := interface.get("ipv4"): + nameservers.update(ipv4.get("nameservers", [])) + if ipv6 := interface.get("ipv6"): + nameservers.update(ipv6.get("nameservers", [])) + information = { "host_os": host_info.get("operating_system"), "update_channel": info.get("channel"), @@ -62,6 +71,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: "docker_version": info.get("docker"), "disk_total": f"{host_info.get('disk_total')} GB", "disk_used": f"{host_info.get('disk_used')} GB", + "nameservers": ", ".join(nameservers), "healthy": healthy, "supported": supported, "host_connectivity": network_info.get("host_internet"), diff --git a/homeassistant/components/homeassistant_hardware/coordinator.py b/homeassistant/components/homeassistant_hardware/coordinator.py index c9a5c891328..36a2f407282 100644 --- a/homeassistant/components/homeassistant_hardware/coordinator.py +++ b/homeassistant/components/homeassistant_hardware/coordinator.py @@ -12,6 +12,7 @@ from ha_silabs_firmware_client import ( ManifestMissing, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8) class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]): """Coordinator to manage firmware updates.""" - def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + session: ClientSession, + url: str, + ) -> None: """Initialize the firmware update coordinator.""" super().__init__( hass, _LOGGER, name="firmware update coordinator", update_interval=FIRMWARE_REFRESH_INTERVAL, + config_entry=config_entry, ) self.hass = hass self.session = session diff --git a/homeassistant/components/homeassistant_sky_connect/update.py b/homeassistant/components/homeassistant_sky_connect/update.py index 74c28b37eaf..df69b6d40a2 100644 --- a/homeassistant/components/homeassistant_sky_connect/update.py +++ b/homeassistant/components/homeassistant_sky_connect/update.py @@ -124,6 +124,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/homeassistant/components/homeassistant_yellow/update.py b/homeassistant/components/homeassistant_yellow/update.py index 9531bd456cb..7a6e2f19b1f 100644 --- a/homeassistant/components/homeassistant_yellow/update.py +++ b/homeassistant/components/homeassistant_yellow/update.py @@ -129,6 +129,7 @@ def _async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, NABU_CASA_FIRMWARE_RELEASES_URL, ), diff --git a/homeassistant/components/homee/config_flow.py b/homeassistant/components/homee/config_flow.py index 7030752f4c3..44c9b70953b 100644 --- a/homeassistant/components/homee/config_flow.py +++ b/homeassistant/components/homee/config_flow.py @@ -11,10 +11,16 @@ from pyHomee import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .const import DOMAIN +from .const import ( + DOMAIN, + RESULT_CANNOT_CONNECT, + RESULT_INVALID_AUTH, + RESULT_UNKNOWN_ERROR, +) _LOGGER = logging.getLogger(__name__) @@ -33,60 +39,137 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 homee: Homee + _host: str + _name: str _reauth_host: str _reauth_username: str + async def _connect_homee(self) -> dict[str, str]: + errors: dict[str, str] = {} + try: + await self.homee.get_access_token() + except HomeeConnectionFailedException: + errors["base"] = RESULT_CANNOT_CONNECT + except HomeeAuthenticationFailedException: + errors["base"] = RESULT_INVALID_AUTH + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = RESULT_UNKNOWN_ERROR + else: + _LOGGER.info("Got access token for homee") + self.hass.loop.create_task(self.homee.run()) + _LOGGER.debug("Homee task created") + await self.homee.wait_until_connected() + _LOGGER.info("Homee connected") + self.homee.disconnect() + _LOGGER.debug("Homee disconnecting") + await self.homee.wait_until_disconnected() + _LOGGER.info("Homee config successfully tested") + + await self.async_set_unique_id( + self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER + ) + + self._abort_if_unique_id_configured() + + _LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid) + + return errors + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial user step.""" + errors: dict[str, str] = {} - errors = {} if user_input is not None: self.homee = Homee( user_input[CONF_HOST], user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) + errors = await self._connect_homee() - try: - await self.homee.get_access_token() - except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" - except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - _LOGGER.info("Got access token for homee") - self.hass.loop.create_task(self.homee.run()) - _LOGGER.debug("Homee task created") - await self.homee.wait_until_connected() - _LOGGER.info("Homee connected") - self.homee.disconnect() - _LOGGER.debug("Homee disconnecting") - await self.homee.wait_until_disconnected() - _LOGGER.info("Homee config successfully tested") - - await self.async_set_unique_id(self.homee.settings.uid) - - self._abort_if_unique_id_configured() - - _LOGGER.info( - "Created new homee entry with ID %s", self.homee.settings.uid - ) - + if not errors: return self.async_create_entry( title=f"{self.homee.settings.homee_name} ({self.homee.host})", data=user_input, ) + return self.async_show_form( step_id="user", data_schema=AUTH_SCHEMA, errors=errors, ) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + + # Ensure that an IPv4 address is received + self._host = discovery_info.host + self._name = discovery_info.hostname[6:18] + if discovery_info.ip_address.version == 6: + return self.async_abort(reason="ipv6_address") + + await self.async_set_unique_id(self._name) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) + + # Cause an auth-error to see if homee is reachable. + self.homee = Homee( + self._host, + "dummy_username", + "dummy_password", + ) + errors = await self._connect_homee() + if errors["base"] != RESULT_INVALID_AUTH: + return self.async_abort(reason=RESULT_CANNOT_CONNECT) + + self.context["title_placeholders"] = {"name": self._name, "host": self._host} + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm the configuration of the device.""" + + errors: dict[str, str] = {} + if user_input is not None: + self.homee = Homee( + self._host, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + errors = await self._connect_homee() + + if not errors: + return self.async_create_entry( + title=f"{self.homee.settings.homee_name} ({self.homee.host})", + data={ + CONF_HOST: self._host, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + + return self.async_show_form( + step_id="zeroconf_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={ + CONF_HOST: self._name, + }, + last_step=True, + ) + async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: @@ -108,12 +191,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.homee.get_access_token() except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" + errors["base"] = RESULT_CANNOT_CONNECT except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" + errors["base"] = RESULT_INVALID_AUTH except Exception: _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + errors["base"] = RESULT_UNKNOWN_ERROR else: self.hass.loop.create_task(self.homee.run()) await self.homee.wait_until_connected() @@ -161,12 +244,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.homee.get_access_token() except HomeeConnectionFailedException: - errors["base"] = "cannot_connect" + errors["base"] = RESULT_CANNOT_CONNECT except HomeeAuthenticationFailedException: - errors["base"] = "invalid_auth" + errors["base"] = RESULT_INVALID_AUTH except Exception: _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + errors["base"] = RESULT_UNKNOWN_ERROR else: self.hass.loop.create_task(self.homee.run()) await self.homee.wait_until_connected() diff --git a/homeassistant/components/homee/const.py b/homeassistant/components/homee/const.py index 7bc3de189d6..718baf346ae 100644 --- a/homeassistant/components/homee/const.py +++ b/homeassistant/components/homee/const.py @@ -20,6 +20,11 @@ from homeassistant.const import ( # General DOMAIN = "homee" +# Error strings +RESULT_CANNOT_CONNECT = "cannot_connect" +RESULT_INVALID_AUTH = "invalid_auth" +RESULT_UNKNOWN_ERROR = "unknown" + # Sensor mappings HOMEE_UNIT_TO_HA_UNIT = { "": None, diff --git a/homeassistant/components/homee/manifest.json b/homeassistant/components/homee/manifest.json index 9cac876f325..35e89ec645a 100644 --- a/homeassistant/components/homee/manifest.json +++ b/homeassistant/components/homee/manifest.json @@ -8,5 +8,11 @@ "iot_class": "local_push", "loggers": ["homee"], "quality_scale": "silver", - "requirements": ["pyHomee==1.2.10"] + "requirements": ["pyHomee==1.2.10"], + "zeroconf": [ + { + "type": "_ssh._tcp.local.", + "name": "homee-*" + } + ] } diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index 267d5553a8c..26fa335d147 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -46,6 +46,17 @@ "data_description": { "host": "[%key:component::homee::config::step::user::data_description::host%]" } + }, + "zeroconf_confirm": { + "title": "Configure discovered homee {host}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::homee::config::step::user::data_description::username%]", + "password": "[%key:component::homee::config::step::user::data_description::password%]" + } } } }, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 95842d56094..681ebcbbef7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -628,12 +628,12 @@ class HomeAccessory(Accessory): # type: ignore[misc] self, domain: str, service: str, - service_data: dict[str, Any] | None, + service_data: dict[str, Any], value: Any | None = None, ) -> None: """Fire event and call service for changes from HomeKit.""" event_data = { - ATTR_ENTITY_ID: self.entity_id, + ATTR_ENTITY_ID: service_data.get(ATTR_ENTITY_ID, self.entity_id), ATTR_DISPLAY_NAME: self.display_name, ATTR_SERVICE: service, ATTR_VALUE: value, diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 44f18c30099..2d4e2b03079 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -57,6 +57,8 @@ CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor" CONF_LINKED_OBSTRUCTION_SENSOR = "linked_obstruction_sensor" CONF_LINKED_PM25_SENSOR = "linked_pm25_sensor" CONF_LINKED_TEMPERATURE_SENSOR = "linked_temperature_sensor" +CONF_LINKED_VALVE_DURATION = "linked_valve_duration" +CONF_LINKED_VALVE_END_TIME = "linked_valve_end_time" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" @@ -229,10 +231,12 @@ CHAR_ON = "On" CHAR_OUTLET_IN_USE = "OutletInUse" CHAR_POSITION_STATE = "PositionState" CHAR_PROGRAMMABLE_SWITCH_EVENT = "ProgrammableSwitchEvent" +CHAR_REMAINING_DURATION = "RemainingDuration" CHAR_REMOTE_KEY = "RemoteKey" CHAR_ROTATION_DIRECTION = "RotationDirection" CHAR_ROTATION_SPEED = "RotationSpeed" CHAR_SATURATION = "Saturation" +CHAR_SET_DURATION = "SetDuration" CHAR_SERIAL_NUMBER = "SerialNumber" CHAR_SERVICE_LABEL_INDEX = "ServiceLabelIndex" CHAR_SERVICE_LABEL_NAMESPACE = "ServiceLabelNamespace" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 18150c820c3..c011b8cd327 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -15,6 +15,11 @@ from pyhap.const import ( ) from homeassistant.components import button, input_button +from homeassistant.components.input_number import ( + ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, + DOMAIN as INPUT_NUMBER_DOMAIN, + SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, +) from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, @@ -45,6 +50,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.event import async_call_later +from homeassistant.util import dt as dt_util from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( @@ -54,7 +60,11 @@ from .const import ( CHAR_NAME, CHAR_ON, CHAR_OUTLET_IN_USE, + CHAR_REMAINING_DURATION, + CHAR_SET_DURATION, CHAR_VALVE_TYPE, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, SERV_OUTLET, SERV_SWITCH, SERV_VALVE, @@ -271,7 +281,21 @@ class ValveBase(HomeAccessory): self.on_service = on_service self.off_service = off_service - serv_valve = self.add_preload_service(SERV_VALVE) + self.chars = [] + + self.linked_duration_entity: str | None = self.config.get( + CONF_LINKED_VALVE_DURATION + ) + self.linked_end_time_entity: str | None = self.config.get( + CONF_LINKED_VALVE_END_TIME + ) + + if self.linked_duration_entity: + self.chars.append(CHAR_SET_DURATION) + if self.linked_end_time_entity: + self.chars.append(CHAR_REMAINING_DURATION) + + serv_valve = self.add_preload_service(SERV_VALVE, self.chars) self.char_active = serv_valve.configure_char( CHAR_ACTIVE, value=False, setter_callback=self.set_state ) @@ -279,6 +303,25 @@ class ValveBase(HomeAccessory): self.char_valve_type = serv_valve.configure_char( CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type ) + + if CHAR_SET_DURATION in self.chars: + _LOGGER.debug( + "%s: Add characteristic %s", self.entity_id, CHAR_SET_DURATION + ) + self.char_set_duration = serv_valve.configure_char( + CHAR_SET_DURATION, + value=self.get_duration(), + setter_callback=self.set_duration, + ) + + if CHAR_REMAINING_DURATION in self.chars: + _LOGGER.debug( + "%s: Add characteristic %s", self.entity_id, CHAR_REMAINING_DURATION + ) + self.char_remaining_duration = serv_valve.configure_char( + CHAR_REMAINING_DURATION, getter_callback=self.get_remaining_duration + ) + # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup self.async_update_state(state) @@ -294,12 +337,75 @@ class ValveBase(HomeAccessory): @callback def async_update_state(self, new_state: State) -> None: """Update switch state after state changed.""" + self._update_duration_chars() current_state = 1 if new_state.state in self.open_states else 0 _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) self.char_active.set_value(current_state) _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) self.char_in_use.set_value(current_state) + def _update_duration_chars(self) -> None: + """Update valve duration related properties if characteristics are available.""" + if CHAR_SET_DURATION in self.chars: + self.char_set_duration.set_value(self.get_duration()) + if CHAR_REMAINING_DURATION in self.chars: + self.char_remaining_duration.set_value(self.get_remaining_duration()) + + def set_duration(self, value: int) -> None: + """Set default duration for how long the valve should remain open.""" + _LOGGER.debug("%s: Set default run time to %s", self.entity_id, value) + self.async_call_service( + INPUT_NUMBER_DOMAIN, + INPUT_NUMBER_SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: self.linked_duration_entity, + INPUT_NUMBER_ATTR_VALUE: value, + }, + value, + ) + + def get_duration(self) -> int: + """Get the default duration from Home Assistant.""" + duration_state = self._get_entity_state(self.linked_duration_entity) + if duration_state is None: + _LOGGER.debug( + "%s: No linked duration entity state available", self.entity_id + ) + return 0 + + try: + duration = float(duration_state) + return max(int(duration), 0) + except ValueError: + _LOGGER.debug("%s: Cannot parse linked duration entity", self.entity_id) + return 0 + + def get_remaining_duration(self) -> int: + """Calculate the remaining duration based on end time in Home Assistant.""" + end_time_state = self._get_entity_state(self.linked_end_time_entity) + if end_time_state is None: + _LOGGER.debug( + "%s: No linked end time entity state available", self.entity_id + ) + return self.get_duration() + + end_time = dt_util.parse_datetime(end_time_state) + if end_time is None: + _LOGGER.debug("%s: Cannot parse linked end time entity", self.entity_id) + return self.get_duration() + + remaining_time = (end_time - dt_util.utcnow()).total_seconds() + return max(int(remaining_time), 0) + + def _get_entity_state(self, entity_id: str | None) -> str | None: + """Fetch the state of a linked entity.""" + if entity_id is None: + return None + state = self.hass.states.get(entity_id) + if state is None: + return None + return state.state + @TYPES.register("ValveSwitch") class ValveSwitch(ValveBase): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 85207e09626..ea67e30a3c1 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -17,6 +17,7 @@ import voluptuous as vol from homeassistant.components import ( binary_sensor, + input_number, media_player, persistent_notification, sensor, @@ -69,6 +70,8 @@ from .const import ( CONF_LINKED_OBSTRUCTION_SENSOR, CONF_LINKED_PM25_SENSOR, CONF_LINKED_TEMPERATURE_SENSOR, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -266,7 +269,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend( TYPE_VALVE, ) ), - ) + ), + vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN), + vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN), } ) @@ -277,6 +282,12 @@ SENSOR_SCHEMA = BASIC_INFO_SCHEMA.extend( } ) +VALVE_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN), + vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN), + } +) HOMEKIT_CHAR_TRANSLATIONS = { 0: " ", # nul @@ -360,6 +371,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]: elif domain == "sensor": config = SENSOR_SCHEMA(config) + elif domain == "valve": + config = VALVE_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 931b689fb08..e846a360d39 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -283,19 +283,19 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" - return self._device.doorState == DoorState.CLOSED + return self.functional_channel.doorState == DoorState.CLOSED async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._device.send_door_command_async(DoorCommand.OPEN) + await self.functional_channel.async_send_door_command(DoorCommand.OPEN) async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._device.send_door_command_async(DoorCommand.CLOSE) + await self.functional_channel.async_send_door_command(DoorCommand.CLOSE) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self._device.send_door_command_async(DoorCommand.STOP) + await self.functional_channel.async_send_door_command(DoorCommand.STOP) class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index b7251382296..36dfdd423ef 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -163,6 +163,7 @@ async def async_setup_entry( name="light", update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), @@ -197,6 +198,7 @@ async def async_setup_entry( name="group", update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), update_interval=SCAN_INTERVAL, + config_entry=config_entry, request_refresh_debouncer=Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 393069b0c7c..fb8f3c572c1 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -53,6 +53,7 @@ class SensorManager: LOGGER, name="sensor", update_method=self.async_update_data, + config_entry=bridge.config_entry, update_interval=self.SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 7f2921f17fa..c5af18c6387 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -71,10 +71,10 @@ ERROR_KEYS = [ "cutting_drive_motor_2_defect", "cutting_drive_motor_3_defect", "cutting_height_blocked", + "cutting_height_problem", "cutting_height_problem_curr", "cutting_height_problem_dir", "cutting_height_problem_drive", - "cutting_height_problem", "cutting_motor_problem", "cutting_stopped_slope_too_steep", "cutting_system_blocked", @@ -117,7 +117,6 @@ ERROR_KEYS = [ "no_accurate_position_from_satellites", "no_confirmed_position", "no_drive", - "no_error", "no_loop_signal", "no_power_in_charging_station", "no_response_from_charger", @@ -169,8 +168,8 @@ ERROR_KEYS = [ ] -ERROR_KEY_LIST = list( - dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES]) +ERROR_KEY_LIST = sorted( + set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"} ) INACTIVE_REASONS: list = [ diff --git a/homeassistant/components/husqvarna_automower_ble/__init__.py b/homeassistant/components/husqvarna_automower_ble/__init__.py index f168e84be4c..fd4521549a2 100644 --- a/homeassistant/components/husqvarna_automower_ble/__init__.py +++ b/homeassistant/components/husqvarna_automower_ble/__init__.py @@ -19,6 +19,7 @@ type HusqvarnaConfigEntry = ConfigEntry[HusqvarnaCoordinator] PLATFORMS = [ Platform.LAWN_MOWER, + Platform.SENSOR, ] diff --git a/homeassistant/components/husqvarna_automower_ble/coordinator.py b/homeassistant/components/husqvarna_automower_ble/coordinator.py index c7781becd76..ef9ccfa5a47 100644 --- a/homeassistant/components/husqvarna_automower_ble/coordinator.py +++ b/homeassistant/components/husqvarna_automower_ble/coordinator.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: SCAN_INTERVAL = timedelta(seconds=60) -class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]): +class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, str | int]]): """Class to manage fetching data.""" def __init__( @@ -67,11 +67,11 @@ class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]): except BleakError as err: raise UpdateFailed("Failed to connect") from err - async def _async_update_data(self) -> dict[str, bytes]: + async def _async_update_data(self) -> dict[str, str | int]: """Poll the device.""" LOGGER.debug("Polling device") - data: dict[str, bytes] = {} + data: dict[str, str | int] = {} try: if not self.mower.is_connected(): diff --git a/homeassistant/components/husqvarna_automower_ble/entity.py b/homeassistant/components/husqvarna_automower_ble/entity.py index d2873d933ff..cb62f36027a 100644 --- a/homeassistant/components/husqvarna_automower_ble/entity.py +++ b/homeassistant/components/husqvarna_automower_ble/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER @@ -28,3 +29,18 @@ class HusqvarnaAutomowerBleEntity(CoordinatorEntity[HusqvarnaCoordinator]): def available(self) -> bool: """Return if entity is available.""" return super().available and self.coordinator.mower.is_connected() + + +class HusqvarnaAutomowerBleDescriptorEntity(HusqvarnaAutomowerBleEntity): + """Coordinator entity for entities with entity description.""" + + def __init__( + self, coordinator: HusqvarnaCoordinator, description: EntityDescription + ) -> None: + """Initialize description entity.""" + super().__init__(coordinator) + + self._attr_unique_id = ( + f"{coordinator.address}_{coordinator.channel_id}_{description.key}" + ) + self.entity_description = description diff --git a/homeassistant/components/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json index 6eb618cbb04..50430c2a9fa 100644 --- a/homeassistant/components/husqvarna_automower_ble/manifest.json +++ b/homeassistant/components/husqvarna_automower_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "iot_class": "local_polling", - "requirements": ["automower-ble==0.2.1"] + "requirements": ["automower-ble==0.2.7"] } diff --git a/homeassistant/components/husqvarna_automower_ble/sensor.py b/homeassistant/components/husqvarna_automower_ble/sensor.py new file mode 100644 index 00000000000..f747133c950 --- /dev/null +++ b/homeassistant/components/husqvarna_automower_ble/sensor.py @@ -0,0 +1,51 @@ +"""Support for sensor entities.""" + +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import HusqvarnaConfigEntry +from .entity import HusqvarnaAutomowerBleDescriptorEntity + +DESCRIPTIONS = ( + SensorEntityDescription( + key="battery_level", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HusqvarnaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Husqvarna Automower Ble sensor based on a config entry.""" + coordinator = entry.runtime_data + async_add_entities( + HusqvarnaAutomowerBleSensor(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.data + ) + + +class HusqvarnaAutomowerBleSensor(HusqvarnaAutomowerBleDescriptorEntity, SensorEntity): + """Representation of a sensor.""" + + entity_description: SensorEntityDescription + + @property + def native_value(self) -> str | int: + """Return the previously fetched value.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 62a4f41ba1f..e65ccf35fb5 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.1"] + "requirements": ["imgw_pib==1.5.2"] } diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 059fd11999f..056ace7011c 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload): ), } ) - self.add_suggested_values_to_schema( + data_schema = self.add_suggested_values_to_schema( data_schema, {"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}}, ) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index c981f3fd438..5c3158bddf2 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -135,6 +135,7 @@ class KrakenData: self._hass, _LOGGER, name=DOMAIN, + config_entry=self._config_entry, update_method=self.async_update, update_interval=timedelta( seconds=self._config_entry.options[CONF_SCAN_INTERVAL] diff --git a/homeassistant/components/leaone/strings.json b/homeassistant/components/leaone/strings.json index bb684941147..53332ce2fec 100644 --- a/homeassistant/components/leaone/strings.json +++ b/homeassistant/components/leaone/strings.json @@ -13,7 +13,7 @@ } }, "abort": { - "no_devices_found": "No supported LeaOne devices found in range; If the device is in range, ensure it has been activated in the last few minutes. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the LeaOne device is present.", + "no_devices_found": "No supported LeaOne devices found in range. If the device is in range, ensure it has been activated in the last few minutes. If you need clarification on whether the device is in range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the LeaOne device is present.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 33addd85ba2..e67c681ac53 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_push", "loggers": ["pylitterbot"], "quality_scale": "bronze", - "requirements": ["pylitterbot==2024.2.2"] + "requirements": ["pylitterbot==2024.2.3"] } diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index c040d665794..481cc4ccb7d 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -17,5 +17,7 @@ ATTR_INCLUDE_TAGS = "include_tags" ATTR_ENTRY_TYPE = "entry_type" ATTR_NOTE_TITLE = "note_title" ATTR_NOTE_TEXT = "note_text" +ATTR_SEARCH_TERMS = "search_terms" +ATTR_RESULT_LIMIT = "result_limit" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index d7e29cc8bbe..773d70afa5f 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -30,6 +30,9 @@ "get_recipe": { "service": "mdi:map" }, + "get_recipes": { + "service": "mdi:book-open-page-variant" + }, "import_recipe": { "service": "mdi:map-search" }, diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 804011b3d9a..a744b9e6ced 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["aiomealie==0.10.0"] + "requirements": ["aiomealie==0.10.1"] } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index 0d9a29392a4..f219cea1835 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -32,6 +32,8 @@ from .const import ( ATTR_NOTE_TEXT, ATTR_NOTE_TITLE, ATTR_RECIPE_ID, + ATTR_RESULT_LIMIT, + ATTR_SEARCH_TERMS, ATTR_START_DATE, ATTR_URL, DOMAIN, @@ -55,6 +57,15 @@ SERVICE_GET_RECIPE_SCHEMA = vol.Schema( } ) +SERVICE_GET_RECIPES = "get_recipes" +SERVICE_GET_RECIPES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Optional(ATTR_SEARCH_TERMS): str, + vol.Optional(ATTR_RESULT_LIMIT): int, + } +) + SERVICE_IMPORT_RECIPE = "import_recipe" SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema( { @@ -159,6 +170,27 @@ async def _async_get_recipe(call: ServiceCall) -> ServiceResponse: return {"recipe": asdict(recipe)} +async def _async_get_recipes(call: ServiceCall) -> ServiceResponse: + """Get recipes.""" + entry = _async_get_entry(call) + search_terms = call.data.get(ATTR_SEARCH_TERMS) + result_limit = call.data.get(ATTR_RESULT_LIMIT, 10) + client = entry.runtime_data.client + try: + recipes = await client.get_recipes(search=search_terms, per_page=result_limit) + except MealieConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from err + except MealieNotFoundError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_recipes_found", + ) from err + return {"recipes": asdict(recipes)} + + async def _async_import_recipe(call: ServiceCall) -> ServiceResponse: """Import a recipe.""" entry = _async_get_entry(call) @@ -242,6 +274,13 @@ def async_setup_services(hass: HomeAssistant) -> None: schema=SERVICE_GET_RECIPE_SCHEMA, supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_RECIPES, + _async_get_recipes, + schema=SERVICE_GET_RECIPES_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) hass.services.async_register( DOMAIN, SERVICE_IMPORT_RECIPE, diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index 47a79ba5756..6a78564a578 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -24,6 +24,27 @@ get_recipe: selector: text: +get_recipes: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mealie + search_terms: + required: false + selector: + text: + result_limit: + required: false + default: 10 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: recipes + import_recipe: fields: config_entry_id: diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 186fc4c4ac0..5533631f755 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -109,6 +109,9 @@ "recipe_not_found": { "message": "Recipe with ID or slug `{recipe_id}` not found." }, + "no_recipes_found": { + "message": "No recipes found matching your search." + }, "could_not_import_recipe": { "message": "Mealie could not import the recipe from the URL." }, @@ -176,6 +179,24 @@ } } }, + "get_recipes": { + "name": "Get recipes", + "description": "Searches for recipes with any matching properties in Mealie", + "fields": { + "config_entry_id": { + "name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]", + "description": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::description%]" + }, + "search_terms": { + "name": "Search terms", + "description": "Terms to search for in recipe properties." + }, + "result_limit": { + "name": "Result limit", + "description": "Maximum number of recipes to return (default: 10)." + } + } + }, "import_recipe": { "name": "Import recipe", "description": "Imports a recipe from an URL", diff --git a/homeassistant/components/mealie/todo.py b/homeassistant/components/mealie/todo.py index d42c9033922..e31af281783 100644 --- a/homeassistant/components/mealie/todo.py +++ b/homeassistant/components/mealie/todo.py @@ -174,7 +174,8 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity): if list_item.display.strip() != stripped_item_summary: update_shopping_item.note = stripped_item_summary update_shopping_item.position = position - update_shopping_item.is_food = False + if update_shopping_item.is_food is not None: + update_shopping_item.is_food = False update_shopping_item.food_id = None update_shopping_item.quantity = 0.0 update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED @@ -249,7 +250,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity): mutate_shopping_item.note = item.note mutate_shopping_item.checked = item.checked - if item.is_food: + if item.is_food or item.food_id: mutate_shopping_item.food_id = item.food_id mutate_shopping_item.unit_id = item.unit_id diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 20068efccef..db622d21f1a 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.06.09"], + "requirements": ["yt-dlp[default]==2025.07.21"], "single_config_entry": true } diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 20e6c02f5d4..94918ab4d4f 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France forecast for city {entry.title}", + config_entry=entry, update_method=_async_update_data_forecast_forecast, update_interval=SCAN_INTERVAL, ) @@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France rain for city {entry.title}", + config_entry=entry, update_method=_async_update_data_rain, update_interval=SCAN_INTERVAL_RAIN, ) @@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, name=f"Météo-France alert for department {department}", + config_entry=entry, update_method=_async_update_data_alert, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 1cb2fc0fab1..2c5c250aee7 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo ) from err # Setup MieleAPI and coordinator for data fetch - coordinator = MieleDataUpdateCoordinator(hass, auth) + coordinator = MieleDataUpdateCoordinator(hass, entry, auth) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index a40df909e14..e8b626af785 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = { 38: "quick_power_wash", 42: "tall_items", 44: "power_wash", + 200: "eco", + 202: "automatic", + 203: "comfort_wash", + 204: "power_wash", + 205: "intensive", + 207: "extra_quiet", + 209: "comfort_wash_plus", + 210: "gentle", + 214: "maintenance", + 215: "rinse_salt", } TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 27456ffe04c..d5de2d79cb9 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): def __init__( self, hass: HomeAssistant, + config_entry: MieleConfigEntry, api: AsyncConfigEntryAuth, ) -> None: """Initialize the Miele data coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name=DOMAIN, update_interval=timedelta(seconds=120), ) diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 4a0eac7da85..77d94c49ffa 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -110,6 +110,9 @@ }, "set_program": { "service": "mdi:arrow-right-circle-outline" + }, + "set_program_oven": { + "service": "mdi:arrow-right-circle-outline" } } } diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 216b91ca68e..cc108841aae 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor): ) ).name if self.device.state_plate_step - else PlatePowerStep.plate_step_0 + else PlatePowerStep.plate_step_0.name ) diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 6d4dc77dd36..517b489173d 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -1,12 +1,13 @@ """Services for Miele integration.""" +from datetime import timedelta import logging from typing import cast import aiohttp import voluptuous as vol -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -32,6 +33,19 @@ SERVICE_SET_PROGRAM_SCHEMA = vol.Schema( }, ) +SERVICE_SET_PROGRAM_OVEN = "set_program_oven" +SERVICE_SET_PROGRAM_OVEN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM_ID): cv.positive_int, + vol.Optional(ATTR_TEMPERATURE): cv.positive_int, + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, + vol.Range(min=timedelta(minutes=1), max=timedelta(hours=12)), + ), + }, +) + SERVICE_GET_PROGRAMS = "get_programs" SERVICE_GET_PROGRAMS_SCHEMA = vol.Schema( { @@ -103,6 +117,36 @@ async def set_program(call: ServiceCall) -> None: ) from ex +async def set_program_oven(call: ServiceCall) -> None: + """Set a program on a Miele oven.""" + + _LOGGER.debug("Set program call: %s", call) + config_entry = await _extract_config_entry(call) + api = config_entry.runtime_data.api + + serial_number = await _get_serial_number(call) + data = {"programId": call.data[ATTR_PROGRAM_ID]} + if call.data.get(ATTR_DURATION) is not None: + td = call.data[ATTR_DURATION] + data["duration"] = [ + td.seconds // 3600, # hours + (td.seconds // 60) % 60, # minutes + ] + if call.data.get(ATTR_TEMPERATURE) is not None: + data["temperature"] = call.data[ATTR_TEMPERATURE] + try: + await api.set_program(serial_number, data) + except aiohttp.ClientResponseError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_program_oven_error", + translation_placeholders={ + "status": str(ex.status), + "message": ex.message, + }, + ) from ex + + async def get_programs(call: ServiceCall) -> ServiceResponse: """Get available programs from appliance.""" @@ -126,7 +170,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: "programs": [ { "program_id": item["programId"], - "program": item["program"], + "program": item["program"].strip(), "parameters": ( { "temperature": ( @@ -159,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: else {} ), } - if item["parameters"] + if item.get("parameters") else {} ), } @@ -172,7 +216,17 @@ async def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" hass.services.async_register( - DOMAIN, SERVICE_SET_PROGRAM, set_program, SERVICE_SET_PROGRAM_SCHEMA + DOMAIN, + SERVICE_SET_PROGRAM, + set_program, + SERVICE_SET_PROGRAM_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_PROGRAM_OVEN, + set_program_oven, + SERVICE_SET_PROGRAM_OVEN_SCHEMA, ) hass.services.async_register( diff --git a/homeassistant/components/miele/services.yaml b/homeassistant/components/miele/services.yaml index 6866e997c45..87114343ad1 100644 --- a/homeassistant/components/miele/services.yaml +++ b/homeassistant/components/miele/services.yaml @@ -23,3 +23,33 @@ set_program: max: 99999 mode: box example: 24 + +set_program_oven: + fields: + device_id: + selector: + device: + integration: miele + required: true + program_id: + required: true + selector: + number: + min: 0 + max: 99999 + mode: box + example: 24 + temperature: + required: false + selector: + number: + min: 30 + max: 300 + unit_of_measurement: "°C" + mode: box + example: 180 + duration: + required: false + selector: + duration: + example: 1:15:00 diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 5b5cac16b53..90689a3d9cc 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -203,27 +203,27 @@ "plate": { "name": "Plate {plate_no}", "state": { - "power_step_0": "0", - "power_step_warm": "Warming", - "power_step_1": "1", - "power_step_2": "1\u2022", - "power_step_3": "2", - "power_step_4": "2\u2022", - "power_step_5": "3", - "power_step_6": "3\u2022", - "power_step_7": "4", - "power_step_8": "4\u2022", - "power_step_9": "5", - "power_step_10": "5\u2022", - "power_step_11": "6", - "power_step_12": "6\u2022", - "power_step_13": "7", - "power_step_14": "7\u2022", - "power_step_15": "8", - "power_step_16": "8\u2022", - "power_step_17": "9", - "power_step_18": "9\u2022", - "power_step_boost": "Boost" + "plate_step_0": "0", + "plate_step_warm": "Warming", + "plate_step_1": "1", + "plate_step_2": "1\u2022", + "plate_step_3": "2", + "plate_step_4": "2\u2022", + "plate_step_5": "3", + "plate_step_6": "3\u2022", + "plate_step_7": "4", + "plate_step_8": "4\u2022", + "plate_step_9": "5", + "plate_step_10": "5\u2022", + "plate_step_11": "6", + "plate_step_12": "6\u2022", + "plate_step_13": "7", + "plate_step_14": "7\u2022", + "plate_step_15": "8", + "plate_step_16": "8\u2022", + "plate_step_17": "9", + "plate_step_18": "9\u2022", + "plate_step_boost": "Boost" } }, "drying_step": { @@ -485,6 +485,8 @@ "cook_bacon": "Cook bacon", "biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)", "biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)", + "comfort_wash": "Comfort wash", + "comfort_wash_plus": "Comfort wash plus", "cool_air": "Cool air", "corn_on_the_cob": "Corn on the cob", "cottons": "Cottons", @@ -827,6 +829,7 @@ "rice_pudding_steam_cooking": "Rice pudding (steam cooking)", "rinse": "Rinse", "rinse_out_lint": "Rinse out lint", + "rinse_salt": "Rinse salt", "risotto": "Risotto", "ristretto": "Ristretto", "roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)", @@ -1063,10 +1066,13 @@ "message": "Invalid device targeted." }, "get_programs_error": { - "message": "'Get programs' action failed {status} / {message}." + "message": "'Get programs' action failed: {status} / {message}" }, "set_program_error": { - "message": "'Set program' action failed {status} / {message}." + "message": "'Set program' action failed: {status} / {message}" + }, + "set_program_oven_error": { + "message": "'Set program on oven' action failed: {status} / {message}" }, "set_state_error": { "message": "Failed to set state for {entity}." @@ -1096,6 +1102,28 @@ "name": "Program ID" } } + }, + "set_program_oven": { + "name": "Set program on oven", + "description": "[%key:component::miele::services::set_program::description%]", + "fields": { + "device_id": { + "description": "[%key:component::miele::services::set_program::fields::device_id::description%]", + "name": "[%key:component::miele::services::set_program::fields::device_id::name%]" + }, + "program_id": { + "description": "[%key:component::miele::services::set_program::fields::program_id::description%]", + "name": "[%key:component::miele::services::set_program::fields::program_id::name%]" + }, + "temperature": { + "description": "The target temperature for the oven program.", + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "duration": { + "description": "The duration for the oven program.", + "name": "[%key:component::sensor::entity_component::duration::name%]" + } + } } } } diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 246ea778916..ce258712090 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: historic_data_coordinator = MillHistoricDataUpdateCoordinator( hass, + entry, mill_data_connection=mill_data_connection, ) historic_data_coordinator.async_add_listener(lambda: None) diff --git a/homeassistant/components/mill/coordinator.py b/homeassistant/components/mill/coordinator.py index a701acb8ddb..ea1295376ae 100644 --- a/homeassistant/components/mill/coordinator.py +++ b/homeassistant/components/mill/coordinator.py @@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, *, mill_data_connection: Mill, ) -> None: @@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name="MillHistoricDataUpdateCoordinator", + config_entry=config_entry, ) async def _async_update_data(self): diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 9cff2956a5f..04adc9f2d60 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -289,17 +289,23 @@ class MotionTiltDevice(MotionPositionDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 180) + await self.async_request_position_till_stop() + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 0) + await self.async_request_position_till_stop() + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, angle) + await self.async_request_position_till_stop() + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover.""" async with self._api_lock: @@ -360,11 +366,15 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Open) + await self.async_request_position_till_stop() + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Close) + await self.async_request_position_till_stop() + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] @@ -376,6 +386,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.async_request_position_till_stop() + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" angle = kwargs.get(ATTR_TILT_POSITION) @@ -390,6 +402,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice): async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.async_request_position_till_stop() + class MotionTDBUDevice(MotionBaseDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" diff --git a/homeassistant/components/motion_blinds/entity.py b/homeassistant/components/motion_blinds/entity.py index 483a638a0eb..9b52cbb01f5 100644 --- a/homeassistant/components/motion_blinds/entity.py +++ b/homeassistant/components/motion_blinds/entity.py @@ -42,6 +42,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind self._requesting_position: CALLBACK_TYPE | None = None self._previous_positions: list[int | dict | None] = [] + self._previous_angles: list[int | None] = [] if blind.device_type in DEVICE_TYPES_WIFI: self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI @@ -112,17 +113,27 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind """Request a state update from the blind at a scheduled point in time.""" # add the last position to the list and keep the list at max 2 items self._previous_positions.append(self._blind.position) + self._previous_angles.append(self._blind.angle) if len(self._previous_positions) > 2: del self._previous_positions[: len(self._previous_positions) - 2] + if len(self._previous_angles) > 2: + del self._previous_angles[: len(self._previous_angles) - 2] async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Update_trigger) self.coordinator.async_update_listeners() - if len(self._previous_positions) < 2 or not all( - self._blind.position == prev_position - for prev_position in self._previous_positions + if ( + len(self._previous_positions) < 2 + or not all( + self._blind.position == prev_position + for prev_position in self._previous_positions + ) + or len(self._previous_angles) < 2 + or not all( + self._blind.angle == prev_angle for prev_angle in self._previous_angles + ) ): # keep updating the position @self._update_interval_moving until the position does not change. self._requesting_position = async_call_later( @@ -132,6 +143,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind ) else: self._previous_positions = [] + self._previous_angles = [] self._requesting_position = None async def async_request_position_till_stop(self, delay: int | None = None) -> None: @@ -140,7 +152,8 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind delay = self._update_interval_moving self._previous_positions = [] - if self._blind.position is None: + self._previous_angles = [] + if self._blind.position is None and self._blind.angle is None: return if self._requesting_position is not None: self._requesting_position() diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index eca520d8946..ac5390f5c64 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.29"] + "requirements": ["motionblinds==0.6.30"] } diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 931a57a71cc..52db0bd25da 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -60,6 +60,17 @@ from .const import ( CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TEMPLATE, CONF_CURRENT_TEMP_TOPIC, + CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_FAN_MODE_LIST, + CONF_FAN_MODE_STATE_TEMPLATE, + CONF_FAN_MODE_STATE_TOPIC, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_MAX, + CONF_HUMIDITY_MIN, + CONF_HUMIDITY_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, CONF_MODE_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_LIST, @@ -68,14 +79,39 @@ from .const import ( CONF_POWER_COMMAND_TEMPLATE, CONF_POWER_COMMAND_TOPIC, CONF_PRECISION, + CONF_PRESET_MODE_COMMAND_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, + CONF_PRESET_MODE_VALUE_TEMPLATE, + CONF_PRESET_MODES_LIST, CONF_RETAIN, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, + CONF_SWING_HORIZONTAL_MODE_LIST, + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC, + CONF_SWING_MODE_COMMAND_TEMPLATE, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_LIST, + CONF_SWING_MODE_STATE_TEMPLATE, + CONF_SWING_MODE_STATE_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_STATE_TEMPLATE, + CONF_TEMP_HIGH_STATE_TOPIC, CONF_TEMP_INITIAL, + CONF_TEMP_LOW_COMMAND_TEMPLATE, + CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_STATE_TEMPLATE, + CONF_TEMP_LOW_STATE_TOPIC, CONF_TEMP_MAX, CONF_TEMP_MIN, CONF_TEMP_STATE_TEMPLATE, CONF_TEMP_STATE_TOPIC, + CONF_TEMP_STEP, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_OPTIMISTIC, PAYLOAD_NONE, ) @@ -95,49 +131,6 @@ PARALLEL_UPDATES = 0 DEFAULT_NAME = "MQTT HVAC" -CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" -CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" -CONF_FAN_MODE_LIST = "fan_modes" -CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" -CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" - -CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" -CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" -CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" -CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" -CONF_HUMIDITY_MAX = "max_humidity" -CONF_HUMIDITY_MIN = "min_humidity" - -CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" -CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" -CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" -CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" -CONF_PRESET_MODES_LIST = "preset_modes" - -CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" -CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC = "swing_horizontal_mode_command_topic" -CONF_SWING_HORIZONTAL_MODE_LIST = "swing_horizontal_modes" -CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE = "swing_horizontal_mode_state_template" -CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC = "swing_horizontal_mode_state_topic" - -CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" -CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" -CONF_SWING_MODE_LIST = "swing_modes" -CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template" -CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" - -CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template" -CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic" -CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template" -CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic" -CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template" -CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic" -CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template" -CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic" -CONF_TEMP_STEP = "temp_step" - -DEFAULT_INITIAL_TEMPERATURE = 21.0 - MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { climate.ATTR_CURRENT_HUMIDITY, @@ -299,8 +292,9 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_POWER_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PRECISION): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + vol.Optional(CONF_PRECISION): vol.All( + vol.Coerce(float), + vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), ), vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, @@ -577,7 +571,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): init_temp: float = config.get( CONF_TEMP_INITIAL, TemperatureConverter.convert( - DEFAULT_INITIAL_TEMPERATURE, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, UnitOfTemperature.CELSIUS, self.temperature_unit, ), diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 52f00c82c27..03f758dbdce 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -29,6 +29,13 @@ import yaml from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.climate import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, + DEFAULT_MIN_TEMP, + PRESET_NONE, +) from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState @@ -80,6 +87,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_STATE_TEMPLATE, + CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, @@ -89,8 +97,9 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, EntityCategory, + UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, async_get_hass, callback from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.hassio import is_hassio @@ -115,6 +124,7 @@ from homeassistant.helpers.selector import ( ) from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads +from homeassistant.util.unit_conversion import TemperatureConverter from .addon import get_addon_manager from .client import MqttClientSetup @@ -123,6 +133,8 @@ from .const import ( ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, + CONF_ACTION_TEMPLATE, + CONF_ACTION_TOPIC, CONF_AVAILABILITY_TEMPLATE, CONF_AVAILABILITY_TOPIC, CONF_BIRTH_MESSAGE, @@ -149,6 +161,10 @@ from .const import ( CONF_COMMAND_ON_TEMPLATE, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_CURRENT_HUMIDITY_TEMPLATE, + CONF_CURRENT_HUMIDITY_TOPIC, + CONF_CURRENT_TEMP_TEMPLATE, + CONF_CURRENT_TEMP_TOPIC, CONF_DIRECTION_COMMAND_TEMPLATE, CONF_DIRECTION_COMMAND_TOPIC, CONF_DIRECTION_STATE_TOPIC, @@ -162,6 +178,11 @@ from .const import ( CONF_EFFECT_VALUE_TEMPLATE, CONF_ENTITY_PICTURE, CONF_EXPIRE_AFTER, + CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_FAN_MODE_COMMAND_TOPIC, + CONF_FAN_MODE_LIST, + CONF_FAN_MODE_STATE_TEMPLATE, + CONF_FAN_MODE_STATE_TOPIC, CONF_FLASH, CONF_FLASH_TIME_LONG, CONF_FLASH_TIME_SHORT, @@ -172,10 +193,21 @@ from .const import ( CONF_HS_COMMAND_TOPIC, CONF_HS_STATE_TOPIC, CONF_HS_VALUE_TEMPLATE, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_MAX, + CONF_HUMIDITY_MIN, + CONF_HUMIDITY_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, CONF_KEEPALIVE, CONF_LAST_RESET_VALUE_TEMPLATE, CONF_MAX_KELVIN, CONF_MIN_KELVIN, + CONF_MODE_COMMAND_TEMPLATE, + CONF_MODE_COMMAND_TOPIC, + CONF_MODE_LIST, + CONF_MODE_STATE_TEMPLATE, + CONF_MODE_STATE_TOPIC, CONF_OFF_DELAY, CONF_ON_COMMAND_TYPE, CONF_OPTIONS, @@ -200,6 +232,9 @@ from .const import ( CONF_PERCENTAGE_VALUE_TEMPLATE, CONF_POSITION_CLOSED, CONF_POSITION_OPEN, + CONF_POWER_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TOPIC, + CONF_PRECISION, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TOPIC, CONF_PRESET_MODE_STATE_TOPIC, @@ -236,6 +271,32 @@ from .const import ( CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, + CONF_SWING_HORIZONTAL_MODE_LIST, + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE, + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC, + CONF_SWING_MODE_COMMAND_TEMPLATE, + CONF_SWING_MODE_COMMAND_TOPIC, + CONF_SWING_MODE_LIST, + CONF_SWING_MODE_STATE_TEMPLATE, + CONF_SWING_MODE_STATE_TOPIC, + CONF_TEMP_COMMAND_TEMPLATE, + CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_STATE_TEMPLATE, + CONF_TEMP_HIGH_STATE_TOPIC, + CONF_TEMP_INITIAL, + CONF_TEMP_LOW_COMMAND_TEMPLATE, + CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_STATE_TEMPLATE, + CONF_TEMP_LOW_STATE_TOPIC, + CONF_TEMP_MAX, + CONF_TEMP_MIN, + CONF_TEMP_STATE_TEMPLATE, + CONF_TEMP_STATE_TOPIC, + CONF_TEMP_STEP, CONF_TILT_CLOSED_POSITION, CONF_TILT_COMMAND_TEMPLATE, CONF_TILT_COMMAND_TOPIC, @@ -260,6 +321,7 @@ from .const import ( CONFIG_ENTRY_MINOR_VERSION, CONFIG_ENTRY_VERSION, DEFAULT_BIRTH, + DEFAULT_CLIMATE_INITIAL_TEMPERATURE, DEFAULT_DISCOVERY, DEFAULT_ENCODING, DEFAULT_KEEPALIVE, @@ -392,6 +454,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( SUBENTRY_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.FAN, Platform.LIGHT, @@ -493,6 +556,59 @@ TIMEOUT_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Climate specific selectors +CLIMATE_MODE_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["auto", "off", "cool", "heat", "dry", "fan_only"], + multiple=True, + translation_key="climate_modes", + ) +) + + +@callback +def temperature_selector(config: dict[str, Any]) -> Selector: + """Return a temperature selector with configured or system unit.""" + + return NumberSelector( + NumberSelectorConfig( + mode=NumberSelectorMode.BOX, + unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + +@callback +def temperature_step_selector(config: dict[str, Any]) -> Selector: + """Return a temperature step selector.""" + + return NumberSelector( + NumberSelectorConfig( + mode=NumberSelectorMode.BOX, + min=0.1, + max=10.0, + step=0.1, + unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + +TEMPERATURE_UNIT_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict(value="C", label="°C"), + SelectOptionDict(value="F", label="°F"), + ], + mode=SelectSelectorMode.DROPDOWN, + ) +) +PRECISION_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["1.0", "0.5", "0.1"], + mode=SelectSelectorMode.DROPDOWN, + ) +) + # Cover specific selectors POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX)) @@ -567,10 +683,91 @@ SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector( EXCLUDE_FROM_CONFIG_IF_NONE = {CONF_ENTITY_CATEGORY} +# Target temperature feature selector @callback -def validate_cover_platform_config( - config: dict[str, Any], -) -> dict[str, str]: +def configured_target_temperature_feature(config: dict[str, Any]) -> str: + """Calculate current target temperature feature from config.""" + if ( + config == {CONF_PLATFORM: Platform.CLIMATE.value} + or CONF_TEMP_COMMAND_TOPIC in config + ): + # default to single on initial set + return "single" + if CONF_TEMP_HIGH_COMMAND_TOPIC in config: + return "high_low" + return "none" + + +TARGET_TEMPERATURE_FEATURE_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=["single", "high_low", "none"], + mode=SelectSelectorMode.DROPDOWN, + translation_key="target_temperature_feature", + ) +) +HUMIDITY_SELECTOR = vol.All( + NumberSelector( + NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=100, step=1) + ), + vol.Coerce(int), +) + + +@callback +def temperature_default_from_celsius_to_system_default( + value: float, +) -> Callable[[dict[str, Any]], int]: + """Return temperature in Celsius in system default unit.""" + + def _default(config: dict[str, Any]) -> int: + return round( + TemperatureConverter.convert( + value, + UnitOfTemperature.CELSIUS, + cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]), + ) + ) + + return _default + + +@callback +def default_precision(config: dict[str, Any]) -> str: + """Return the thermostat precision for system default unit.""" + + return str( + config.get( + CONF_PRECISION, + 0.1 + if cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]) + is UnitOfTemperature.CELSIUS + else 1.0, + ) + ) + + +@callback +def validate_climate_platform_config(config: dict[str, Any]) -> dict[str, str]: + """Validate the climate platform options.""" + errors: dict[str, str] = {} + if ( + CONF_PRESET_MODES_LIST in config + and PRESET_NONE in config[CONF_PRESET_MODES_LIST] + ): + errors["climate_preset_mode_settings"] = "preset_mode_none_not_allowed" + if ( + CONF_HUMIDITY_MIN in config + and config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX] + ): + errors["target_humidity_settings"] = "max_below_min_humidity" + if CONF_TEMP_MIN in config and config[CONF_TEMP_MIN] >= config[CONF_TEMP_MAX]: + errors["target_temperature_settings"] = "max_below_min_temperature" + + return errors + + +@callback +def validate_cover_platform_config(config: dict[str, Any]) -> dict[str, str]: """Validate the cover platform options.""" errors: dict[str, str] = {} @@ -680,6 +877,14 @@ def validate_sensor_platform_config( return errors +@callback +def no_empty_list(value: list[Any]) -> list[Any]: + """Validate a selector returns at least one item.""" + if not value: + raise vol.Invalid("empty_list_not_allowed") + return value + + @callback def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: """Run validator, then return the unmodified input.""" @@ -695,13 +900,13 @@ def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: class PlatformField: """Stores a platform config field schema, required flag and validator.""" - selector: Selector[Any] | Callable[..., Selector[Any]] + selector: Selector[Any] | Callable[[dict[str, Any]], Selector[Any]] required: bool - validator: Callable[..., Any] | None = None + validator: Callable[[Any], Any] | None = None error: str | None = None - default: ( - str | int | bool | None | Callable[[dict[str, Any]], Any] | vol.Undefined - ) = vol.UNDEFINED + default: Any | None | Callable[[dict[str, Any]], Any] | vol.Undefined = ( + vol.UNDEFINED + ) is_schema_default: bool = False exclude_from_reconfig: bool = False exclude_from_config: bool = False @@ -790,6 +995,78 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { required=False, ), }, + Platform.CLIMATE.value: { + CONF_TEMPERATURE_UNIT: PlatformField( + selector=TEMPERATURE_UNIT_SELECTOR, + validator=validate(cv.temperature_unit), + required=True, + exclude_from_reconfig=True, + default=lambda _: "C" + if async_get_hass().config.units.temperature_unit + is UnitOfTemperature.CELSIUS + else "F", + ), + "climate_feature_action": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_ACTION_TOPIC)), + ), + "climate_feature_target_temperature": PlatformField( + selector=TARGET_TEMPERATURE_FEATURE_SELECTOR, + required=False, + exclude_from_config=True, + default=configured_target_temperature_feature, + ), + "climate_feature_current_temperature": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_CURRENT_TEMP_TOPIC)), + ), + "climate_feature_target_humidity": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_HUMIDITY_COMMAND_TOPIC)), + ), + "climate_feature_current_humidity": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_HUMIDITY_STATE_TOPIC)), + ), + "climate_feature_preset_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_PRESET_MODES_LIST)), + ), + "climate_feature_fan_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_FAN_MODE_LIST)), + ), + "climate_feature_swing_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_SWING_MODE_LIST)), + ), + "climate_feature_swing_horizontal_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_SWING_HORIZONTAL_MODE_LIST)), + ), + "climate_feature_power": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_POWER_COMMAND_TOPIC)), + ), + }, Platform.COVER.value: { CONF_DEVICE_CLASS: PlatformField( selector=COVER_DEVICE_CLASS_SELECTOR, @@ -929,6 +1206,496 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { ), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, + Platform.CLIMATE.value: { + # operation mode settings + CONF_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_MODE_LIST: PlatformField( + selector=CLIMATE_MODE_SELECTOR, + required=True, + default=[], + validator=validate(no_empty_list), + error="empty_list_not_allowed", + ), + CONF_RETAIN: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool) + ), + CONF_OPTIMISTIC: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool) + ), + # current action settings + CONF_ACTION_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_action_settings", + conditions=({"climate_feature_action": True},), + ), + CONF_ACTION_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_action_settings", + conditions=({"climate_feature_action": True},), + ), + # target temperature settings + CONF_TEMP_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "single"},), + ), + CONF_TEMP_LOW_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_LOW_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_HIGH_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_temperature_settings", + conditions=({"climate_feature_target_temperature": "high_low"},), + ), + CONF_TEMP_MIN: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=True, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_MIN_TEMP + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_MAX: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=True, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_MAX_TEMP + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_PRECISION: PlatformField( + selector=PRECISION_SELECTOR, + required=False, + default=default_precision, + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_STEP: PlatformField( + selector=temperature_step_selector, + custom_filtering=True, + required=False, + default=1.0, + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + CONF_TEMP_INITIAL: PlatformField( + selector=temperature_selector, + custom_filtering=True, + required=False, + default=temperature_default_from_celsius_to_system_default( + DEFAULT_CLIMATE_INITIAL_TEMPERATURE + ), + section="target_temperature_settings", + conditions=( + {"climate_feature_target_temperature": "high_low"}, + {"climate_feature_target_temperature": "single"}, + ), + ), + # current temperature settings + CONF_CURRENT_TEMP_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="current_temperature_settings", + conditions=({"climate_feature_current_temperature": True},), + ), + CONF_CURRENT_TEMP_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="current_temperature_settings", + conditions=({"climate_feature_current_temperature": True},), + ), + # target humidity settings + CONF_HUMIDITY_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_MIN: PlatformField( + selector=HUMIDITY_SELECTOR, + required=True, + default=DEFAULT_MIN_HUMIDITY, + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + CONF_HUMIDITY_MAX: PlatformField( + selector=HUMIDITY_SELECTOR, + required=True, + default=DEFAULT_MAX_HUMIDITY, + section="target_humidity_settings", + conditions=({"climate_feature_target_humidity": True},), + ), + # current humidity settings + CONF_CURRENT_HUMIDITY_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="current_humidity_settings", + conditions=({"climate_feature_current_humidity": True},), + ), + CONF_CURRENT_HUMIDITY_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="current_humidity_settings", + conditions=({"climate_feature_current_humidity": True},), + ), + # power on/off support + CONF_POWER_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_POWER_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_PAYLOAD_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OFF, + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + CONF_PAYLOAD_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ON, + section="climate_power_settings", + conditions=({"climate_feature_power": True},), + ), + # preset mode settings + CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + CONF_PRESET_MODES_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_preset_mode_settings", + conditions=({"climate_feature_preset_modes": True},), + ), + # fan mode settings + CONF_FAN_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + CONF_FAN_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_fan_mode_settings", + conditions=({"climate_feature_fan_modes": True},), + ), + # swing mode settings + CONF_SWING_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + CONF_SWING_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_swing_mode_settings", + conditions=({"climate_feature_swing_modes": True},), + ), + # swing horizontal mode settings + CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + CONF_SWING_HORIZONTAL_MODE_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + validator=validate(no_empty_list), + error="empty_list_not_allowed", + section="climate_swing_horizontal_mode_settings", + conditions=({"climate_feature_swing_horizontal_modes": True},), + ), + }, Platform.COVER.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -1904,6 +2671,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ ] = { Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, + Platform.CLIMATE.value: validate_climate_platform_config, Platform.COVER.value: validate_cover_platform_config, Platform.FAN.value: validate_fan_platform_config, Platform.LIGHT.value: validate_light_platform_config, @@ -2097,15 +2865,15 @@ def data_schema_from_fields( no_reconfig_options: set[Any] = set() for schema_section in sections: data_schema_element = { - vol.Required(field_name, default=field_details.default) + vol.Required(field_name, default=get_default(field_details)) if field_details.required else vol.Optional( field_name, default=get_default(field_details) if field_details.default is not None else vol.UNDEFINED, - ): field_details.selector(component_data_with_user_input) # type: ignore[operator] - if field_details.custom_filtering + ): field_details.selector(component_data_with_user_input or {}) + if callable(field_details.selector) and field_details.custom_filtering else field_details.selector for field_name, field_details in data_schema_fields.items() if not field_details.is_schema_default @@ -2127,12 +2895,20 @@ def data_schema_from_fields( if not data_schema_element: # Do not show empty sections continue + # Collapse if values are changed or required fields need to be set collapsed = ( not any( (default := data_schema_fields[str(option)].default) is vol.UNDEFINED - or component_data_with_user_input[str(option)] != default + or ( + str(option) in component_data_with_user_input + and component_data_with_user_input[str(option)] != default + ) for option in data_element_options if option in component_data_with_user_input + or ( + str(option) in data_schema_fields + and data_schema_fields[str(option)].required + ) ) if component_data_with_user_input is not None else True diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c60aa674b1b..1dfdb8dac53 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -26,7 +26,6 @@ CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_AVAILABILITY = "availability" - CONF_AVAILABILITY_MODE = "availability_mode" CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" @@ -53,7 +52,6 @@ CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_PAYLOAD_RESET = "payload_reset" CONF_SUPPORTED_FEATURES = "supported_features" - CONF_ACTION_TEMPLATE = "action_template" CONF_ACTION_TOPIC = "action_topic" CONF_BLUE_TEMPLATE = "blue_template" @@ -91,6 +89,11 @@ CONF_EFFECT_TEMPLATE = "effect_template" CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template" CONF_ENTITY_PICTURE = "entity_picture" CONF_EXPIRE_AFTER = "expire_after" +CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" +CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" +CONF_FAN_MODE_LIST = "fan_modes" +CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" +CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" CONF_FLASH = "flash" CONF_FLASH_TIME_LONG = "flash_time_long" CONF_FLASH_TIME_SHORT = "flash_time_short" @@ -101,6 +104,12 @@ CONF_HS_COMMAND_TEMPLATE = "hs_command_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" +CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" +CONF_HUMIDITY_MAX = "max_humidity" +CONF_HUMIDITY_MIN = "min_humidity" CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_MAX_KELVIN = "max_kelvin" CONF_MAX_MIREDS = "max_mireds" @@ -166,13 +175,32 @@ CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" +CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" +CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC = "swing_horizontal_mode_command_topic" +CONF_SWING_HORIZONTAL_MODE_LIST = "swing_horizontal_modes" +CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE = "swing_horizontal_mode_state_template" +CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC = "swing_horizontal_mode_state_topic" +CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" +CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" +CONF_SWING_MODE_LIST = "swing_modes" +CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template" +CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template" CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic" -CONF_TEMP_STATE_TEMPLATE = "temperature_state_template" -CONF_TEMP_STATE_TOPIC = "temperature_state_topic" +CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template" +CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic" +CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template" +CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic" CONF_TEMP_INITIAL = "initial" +CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template" +CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic" +CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template" +CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic" CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" +CONF_TEMP_STATE_TEMPLATE = "temperature_state_template" +CONF_TEMP_STATE_TOPIC = "temperature_state_topic" +CONF_TEMP_STEP = "temp_step" CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template" CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" CONF_TILT_STATUS_TOPIC = "tilt_status_topic" @@ -213,6 +241,7 @@ CONF_SUPPORT_URL = "support_url" DEFAULT_BRIGHTNESS = False DEFAULT_BRIGHTNESS_SCALE = 255 +DEFAULT_CLIMATE_INITIAL_TEMPERATURE = 21.0 DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 8a42797b0f2..4cc0424195a 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -364,6 +364,15 @@ class EntityTopicState: entity_id, entity = self.subscribe_calls.popitem() try: entity.async_write_ha_state() + except ValueError as exc: + _LOGGER.error( + "Value error while updating state of %s, topic: " + "'%s' with payload: %s: %s", + entity_id, + msg.topic, + msg.payload, + exc, + ) except Exception: _LOGGER.exception( "Exception raised while updating state of %s, topic: " diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 92900d8292d..0e248cfd2d2 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -239,6 +239,16 @@ "title": "Configure MQTT device \"{mqtt_device}\"", "description": "Please configure specific details for {platform} entity \"{entity}\":", "data": { + "climate_feature_action": "Current action support", + "climate_feature_current_humidity": "Current humidity support", + "climate_feature_current_temperature": "Current temperature support", + "climate_feature_fan_modes": "Fan mode support", + "climate_feature_power": "Power on/off support", + "climate_feature_preset_modes": "[%key:component::mqtt::config_subentries::device::step::entity_platform_config::data::fan_feature_preset_modes%]", + "climate_feature_swing_horizontal_modes": "Horizontal swing mode support", + "climate_feature_swing_modes": "Swing mode support", + "climate_feature_target_temperature": "Target temperature support", + "climate_feature_target_humidity": "Target humidity support", "device_class": "Device class", "entity_category": "Entity category", "fan_feature_speed": "Speed support", @@ -249,9 +259,20 @@ "schema": "Schema", "state_class": "State class", "suggested_display_precision": "Suggested display precision", + "temperature_unit": "Temperature unit", "unit_of_measurement": "Unit of measurement" }, "data_description": { + "climate_feature_action": "The climate supports reporting the current action.", + "climate_feature_current_humidity": "The climate supports reporting the current humidity.", + "climate_feature_current_temperature": "The climate supports reporting the current temperature.", + "climate_feature_fan_modes": "The climate supports fan modes.", + "climate_feature_power": "The climate supports the power \"on\" and \"off\" commands.", + "climate_feature_preset_modes": "The climate supports preset modes.", + "climate_feature_swing_horizontal_modes": "The climate supports horizontal swing modes.", + "climate_feature_swing_modes": "The climate supports swing modes.", + "climate_feature_target_temperature": "The climate supports setting the target temperature.", + "climate_feature_target_humidity": "The climate supports setting the target humidity.", "device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)", "entity_category": "Allows marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)", "fan_feature_speed": "The fan supports multiple speeds.", @@ -262,6 +283,7 @@ "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", "state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", + "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", "unit_of_measurement": "Defines the unit of measurement of the sensor, if any." }, "sections": { @@ -290,6 +312,11 @@ "force_update": "Force update", "green_template": "Green template", "last_reset_value_template": "Last reset value template", + "modes": "Supported operation modes", + "mode_command_topic": "Operation mode command topic", + "mode_command_template": "Operation mode command template", + "mode_state_topic": "Operation mode state topic", + "mode_state_template": "Operation mode value template", "on_command_type": "ON command type", "optimistic": "Optimistic", "payload_off": "Payload \"off\"", @@ -317,6 +344,11 @@ "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", + "modes": "A list of supported operation modes. [Learn more.]({url}#modes)", + "mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)", + "mode_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)", + "mode_state_topic": "The MQTT topic subscribed to receive operation mode state messages. [Learn more.]({url}#mode_state_topic)", + "mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)", "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "payload_off": "The payload that represents the \"off\" state.", @@ -356,6 +388,100 @@ "transition": "Enable the transition feature for this light" } }, + "climate_action_settings": { + "name": "Current action settings", + "data": { + "action_template": "Action template", + "action_topic": "Action topic" + }, + "data_description": { + "action_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the action topic with.", + "action_topic": "The MQTT topic to subscribe for changes of the current action. If this is set, the climate graph uses the value received as data source. A \"None\" payload resets the current action state. An empty payload is ignored. Valid action values are: \"off\", \"heating\", \"cooling\", \"drying\", \"idle\" and \"fan\". [Learn more.]({url}#action_topic)" + } + }, + "climate_fan_mode_settings": { + "name": "Fan mode settings", + "data": { + "fan_modes": "Fan modes", + "fan_mode_command_topic": "Fan mode command topic", + "fan_mode_command_template": "Fan mode command template", + "fan_mode_state_topic": "Fan mode state topic", + "fan_mode_state_template": "Fan mode state template" + }, + "data_description": { + "fan_modes": "List of fan modes this climate is capable of running at. Common fan modes that offer translations are `off`, `on`, `auto`, `low`, `medium`, `high`, `middle`, `focus` and `diffuse`.", + "fan_mode_command_topic": "The MQTT topic to publish commands to change the climate fan mode. [Learn more.]({url}#fan_mode_command_topic)", + "fan_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the fan mode command topic.", + "fan_mode_state_topic": "The MQTT topic subscribed to receive the climate fan mode. [Learn more.]({url}#fan_mode_state_topic)", + "fan_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate fan mode value." + } + }, + "climate_power_settings": { + "name": "Power settings", + "data": { + "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data::payload_on%]", + "power_command_template": "Power command template", + "power_command_topic": "Power command topic" + }, + "data_description": { + "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", + "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]", + "power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", + "power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)" + } + }, + "climate_preset_mode_settings": { + "name": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::name%]", + "data": { + "preset_mode_command_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_command_template%]", + "preset_mode_command_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_command_topic%]", + "preset_mode_value_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_value_template%]", + "preset_mode_state_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_mode_state_topic%]", + "preset_modes": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data::preset_modes%]" + }, + "data_description": { + "preset_mode_command_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_command_template%]", + "preset_mode_command_topic": "The MQTT topic to publish commands to change the climate preset mode. [Learn more.]({url}#preset_mode_command_topic)", + "preset_mode_value_template": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_value_template%]", + "preset_mode_state_topic": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::sections::fan_preset_mode_settings::data_description::preset_mode_state_topic%]", + "preset_modes": "List of preset modes this climate is capable of running at. Common preset modes that offer translations are `none`, `away`, `eco`, `boost`, `comfort`, `home`, `sleep` and `activity`." + } + }, + "climate_swing_horizontal_mode_settings": { + "name": "Horizontal swing mode settings", + "data": { + "swing_horizontal_modes": "Horizontal swing modes", + "swing_horizontal_mode_command_topic": "Horizontal swing mode command topic", + "swing_horizontal_mode_command_template": "Horizontal swing mode command template", + "swing_horizontal_mode_state_topic": "Horizontal swing mode state topic", + "swing_horizontal_mode_state_template": "Horizontal swing mode state template" + }, + "data_description": { + "swing_horizontal_modes": "List of horizontal swing modes this climate is capable of running at. Common horizontal swing modes that offer translations are `off` and `on`.", + "swing_horizontal_mode_command_topic": "The MQTT topic to publish commands to change the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_command_topic)", + "swing_horizontal_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the horizontal swing mode command topic.", + "swing_horizontal_mode_state_topic": "The MQTT topic subscribed to receive the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_state_topic)", + "swing_horizontal_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate horizontal swing mode value." + } + }, + "climate_swing_mode_settings": { + "name": "Swing mode settings", + "data": { + "swing_modes": "Swing modes", + "swing_mode_command_topic": "Swing mode command topic", + "swing_mode_command_template": "Swing mode command template", + "swing_mode_state_topic": "Swing mode state topic", + "swing_mode_state_template": "Swing mode state template" + }, + "data_description": { + "swing_modes": "List of swing modes this climate is capable of running at. Common swing modes that offer translations are `off`, `on`, `vertical`, `horizontal` and `both`.", + "swing_mode_command_topic": "The MQTT topic to publish commands to change the climate swing mode. [Learn more.]({url}#swing_mode_command_topic)", + "swing_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the swing mode command topic.", + "swing_mode_state_topic": "The MQTT topic subscribed to receive the climate swing mode. [Learn more.]({url}#swing_mode_state_topic)", + "swing_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate swing mode value." + } + }, "cover_payload_settings": { "name": "Payload settings", "data": { @@ -425,6 +551,28 @@ "tilt_optimistic": "Flag that defines if tilt works in optimistic mode. If tilt status topic is not defined, tilt works in optimistic mode by default. [Learn more.]({url}#tilt_optimistic)" } }, + "current_humidity_settings": { + "name": "Current humidity settings", + "data": { + "current_humidity_template": "Current humidity template", + "current_humidity_topic": "Current humidity topic" + }, + "data_description": { + "current_humidity_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current humidity value. [Learn more.]({url}#current_humidity_template)", + "current_humidity_topic": "The MQTT topic subscribed to receive current humidity update values. [Learn more.]({url}#current_humidity_topic)" + } + }, + "current_temperature_settings": { + "name": "Current temperature settings", + "data": { + "current_temperature_template": "Current temperature template", + "current_temperature_topic": "Current temperature topic" + }, + "data_description": { + "current_temperature_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current temperature value. [Learn more.]({url}#current_temperature_template)", + "current_temperature_topic": "The MQTT topic subscribed to receive current temperature update values. [Learn more.]({url}#current_temperature_topic)" + } + }, "light_brightness_settings": { "name": "Brightness settings", "data": { @@ -648,6 +796,66 @@ "xy_state_topic": "The MQTT topic subscribed to receive XY state updates. The expected payload is the X and Y color values separated by commas, for example, `0.675,0.322`. [Learn more.]({url}#xy_state_topic)", "xy_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the XY value." } + }, + "target_humidity_settings": { + "name": "Target humidity settings", + "data": { + "max_humidity": "Maximum humidity", + "min_humidity": "Minimum humidity", + "target_humidity_command_template": "Target humidity command template", + "target_humidity_command_topic": "Target humidity command topic", + "target_humidity_state_template": "Target humidity state template", + "target_humidity_state_topic": "Target humidity state topic" + }, + "data_description": { + "max_humidity": "The maximum target humidity that can be set.", + "min_humidity": "The minimum target humidity that can be set.", + "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.", + "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", + "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", + "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" + } + }, + "target_temperature_settings": { + "name": "Target temperature settings", + "data": { + "initial": "Initial temperature", + "max_temp": "Maximum temperature", + "min_temp": "Minimum temperature", + "precision": "Precision", + "temp_step": "Temperature step", + "temperature_command_template": "Temperature command template", + "temperature_command_topic": "Temperature command topic", + "temperature_high_command_template": "Upper temperature command template", + "temperature_high_command_topic": "Upper temperature command topic", + "temperature_low_command_template": "Lower temperature command template", + "temperature_low_command_topic": "Lower temperature command topic", + "temperature_state_template": "Temperature state template", + "temperature_state_topic": "Temperature state topic", + "temperature_high_state_template": "Upper temperature state template", + "temperature_high_state_topic": "Upper temperature state topic", + "temperature_low_state_template": "Lower temperature state template", + "temperature_low_state_topic": "Lower temperature state topic" + }, + "data_description": { + "initial": "The climate initializes with this target temperature.", + "max_temp": "The maximum target temperature that can be set.", + "min_temp": "The minimum target temperature that can be set.", + "precision": "The precision in degrees the thermostat is working at.", + "temp_step": "The target temperature step in degrees Celsius or Fahrenheit.", + "temperature_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the temperature command topic.", + "temperature_command_topic": "The MQTT topic to publish commands to change the climate target temperature. [Learn more.]({url}#temperature_command_topic)", + "temperature_high_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the upper temperature command topic.", + "temperature_high_command_topic": "The MQTT topic to publish commands to change the climate upper target temperature. [Learn more.]({url}#temperature_high_command_topic)", + "temperature_low_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the lower temperature command topic.", + "temperature_low_command_topic": "The MQTT topic to publish commands to change the climate lower target temperature. [Learn more.]({url}#temperature_low_command_topic)", + "temperature_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the temperature state topic with.", + "temperature_state_topic": "The MQTT topic to subscribe for changes of the target temperature. [Learn more.]({url}#temperature_state_topic)", + "temperature_high_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the upper temperature state topic with.", + "temperature_high_state_topic": "The MQTT topic to subscribe for changes of the upper target temperature. [Learn more.]({url}#temperature_high_state_topic)", + "temperature_low_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the lower temperature state topic with.", + "temperature_low_state_topic": "The MQTT topic to subscribe for changes of the lower target temperature. [Learn more.]({url}#temperature_low_state_topic)" + } } } }, @@ -695,6 +903,7 @@ "cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic", "cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic", "cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option", + "empty_list_not_allowed": "Empty list is not allowed. Add at least one item", "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", "fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode", "invalid_input": "Invalid value", @@ -705,10 +914,13 @@ "invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", "invalid_url": "Invalid URL", "last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only", + "max_below_min_humidity": "Max humidity value should be greater than min humidity value", "max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value", + "max_below_min_temperature": "Max temperature value should be greater than min temperature value", "options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used", "options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class. If you continue, the existing options will be reset", "options_with_enum_device_class": "Configure options for the enumeration sensor", + "preset_mode_none_not_allowed": "Preset \"none\" is not a valid preset mode", "uom_required_for_device_class": "The selected device class requires a unit" } } @@ -826,6 +1038,17 @@ } }, "selector": { + "climate_modes": { + "options": { + "off": "[%key:common::state::off%]", + "auto": "[%key:common::state::auto%]", + "heat": "[%key:component::climate::entity_component::_::state::heat%]", + "cool": "[%key:component::climate::entity_component::_::state::cool%]", + "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]", + "dry": "[%key:component::climate::entity_component::_::state::dry%]", + "fan_only": "[%key:component::climate::entity_component::_::state::fan_only%]" + } + }, "device_class_binary_sensor": { "options": { "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", @@ -881,6 +1104,7 @@ }, "device_class_sensor": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", @@ -969,6 +1193,7 @@ "options": { "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", + "climate": "[%key:component::climate::title%]", "cover": "[%key:component::cover::title%]", "fan": "[%key:component::fan::title%]", "light": "[%key:component::light::title%]", @@ -1004,6 +1229,13 @@ "rgbww": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::rgbww%]", "white": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::white%]" } + }, + "target_temperature_feature": { + "options": { + "single": "Single target temperature", + "high_low": "Upper/lower target temperature", + "none": "No target temperature" + } } }, "services": { diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index a4b802f001c..f9cabda90b7 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mysensors", "iot_class": "local_push", "loggers": ["mysensors"], - "requirements": ["pymysensors==0.25.0"] + "requirements": ["pymysensors==0.26.0"] } diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 057993be181..83dc238d2c4 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,7 +1,7 @@ """The ONVIF integration.""" import asyncio -from contextlib import suppress +from contextlib import AsyncExitStack, suppress from http import HTTPStatus import logging @@ -45,50 +45,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = ONVIFDevice(hass, entry) - try: - await device.async_setup() - if not entry.data.get(CONF_SNAPSHOT_AUTH): - await async_populate_snapshot_auth(hass, device, entry) - except (TimeoutError, aiohttp.ClientError) as err: - await device.device.close() - raise ConfigEntryNotReady( - f"Could not connect to camera {device.device.host}:{device.device.port}: {err}" - ) from err - except Fault as err: - await device.device.close() - if is_auth_error(err): - raise ConfigEntryAuthFailed( - f"Auth Failed: {stringify_onvif_error(err)}" - ) from err - raise ConfigEntryNotReady( - f"Could not connect to camera: {stringify_onvif_error(err)}" - ) from err - except ONVIFError as err: - await device.device.close() - raise ConfigEntryNotReady( - f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}" - ) from err - except TransportError as err: - await device.device.close() - stringified_onvif_error = stringify_onvif_error(err) - if err.status_code in ( - HTTPStatus.UNAUTHORIZED.value, - HTTPStatus.FORBIDDEN.value, - ): - raise ConfigEntryAuthFailed( - f"Auth Failed: {stringified_onvif_error}" - ) from err - raise ConfigEntryNotReady( - f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}" - ) from err - except asyncio.CancelledError as err: - # After https://github.com/agronholm/anyio/issues/374 is resolved - # this may be able to be removed - await device.device.close() - raise ConfigEntryNotReady(f"Setup was unexpectedly canceled: {err}") from err + async with AsyncExitStack() as stack: + # Register cleanup callback for device + @stack.push_async_callback + async def _cleanup(): + await _async_stop_device(hass, device) - if not device.available: - raise ConfigEntryNotReady + try: + await device.async_setup() + if not entry.data.get(CONF_SNAPSHOT_AUTH): + await async_populate_snapshot_auth(hass, device, entry) + except (TimeoutError, aiohttp.ClientError) as err: + raise ConfigEntryNotReady( + f"Could not connect to camera {device.device.host}:{device.device.port}: {err}" + ) from err + except Fault as err: + if is_auth_error(err): + raise ConfigEntryAuthFailed( + f"Auth Failed: {stringify_onvif_error(err)}" + ) from err + raise ConfigEntryNotReady( + f"Could not connect to camera: {stringify_onvif_error(err)}" + ) from err + except ONVIFError as err: + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}" + ) from err + except TransportError as err: + stringified_onvif_error = stringify_onvif_error(err) + if err.status_code in ( + HTTPStatus.UNAUTHORIZED.value, + HTTPStatus.FORBIDDEN.value, + ): + raise ConfigEntryAuthFailed( + f"Auth Failed: {stringified_onvif_error}" + ) from err + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}" + ) from err + except asyncio.CancelledError as err: + # After https://github.com/agronholm/anyio/issues/374 is resolved + # this may be able to be removed + raise ConfigEntryNotReady( + f"Setup was unexpectedly canceled: {err}" + ) from err + + if not device.available: + raise ConfigEntryNotReady + + # If we get here, setup was successful - prevent cleanup + stack.pop_all() hass.data[DOMAIN][entry.unique_id] = device @@ -111,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - - device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id] - +async def _async_stop_device(hass: HomeAssistant, device: ONVIFDevice) -> None: + """Stop the ONVIF device.""" if device.capabilities.events and device.events.started: try: await device.events.async_stop() except (TimeoutError, ONVIFError, Fault, aiohttp.ClientError, TransportError): LOGGER.warning("Error while stopping events: %s", device.name) + await device.device.close() + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id] + await _async_stop_device(hass, device) return await hass.config_entries.async_unload_platforms(entry, device.platforms) diff --git a/homeassistant/components/open_router/__init__.py b/homeassistant/components/open_router/__init__.py index 477fabca54c..9850f72f71d 100644 --- a/homeassistant/components/open_router/__init__.py +++ b/homeassistant/components/open_router/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.httpx_client import get_async_client from .const import LOGGER -PLATFORMS = [Platform.CONVERSATION] +PLATFORMS = [Platform.AI_TASK, Platform.CONVERSATION] type OpenRouterConfigEntry = ConfigEntry[AsyncOpenAI] diff --git a/homeassistant/components/open_router/ai_task.py b/homeassistant/components/open_router/ai_task.py new file mode 100644 index 00000000000..fa5d8d0f68e --- /dev/null +++ b/homeassistant/components/open_router/ai_task.py @@ -0,0 +1,75 @@ +"""AI Task integration for OpenRouter.""" + +from __future__ import annotations + +from json import JSONDecodeError +import logging + +from homeassistant.components import ai_task, conversation +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.json import json_loads + +from . import OpenRouterConfigEntry +from .entity import OpenRouterEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: OpenRouterConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up AI Task entities.""" + for subentry in config_entry.subentries.values(): + if subentry.subentry_type != "ai_task_data": + continue + + async_add_entities( + [OpenRouterAITaskEntity(config_entry, subentry)], + config_subentry_id=subentry.subentry_id, + ) + + +class OpenRouterAITaskEntity( + ai_task.AITaskEntity, + OpenRouterEntity, +): + """OpenRouter AI Task entity.""" + + _attr_name = None + _attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA + + async def _async_generate_data( + self, + task: ai_task.GenDataTask, + chat_log: conversation.ChatLog, + ) -> ai_task.GenDataTaskResult: + """Handle a generate data task.""" + await self._async_handle_chat_log(chat_log, task.name, task.structure) + + if not isinstance(chat_log.content[-1], conversation.AssistantContent): + raise HomeAssistantError( + "Last content in chat log is not an AssistantContent" + ) + + text = chat_log.content[-1].content or "" + + if not task.structure: + return ai_task.GenDataTaskResult( + conversation_id=chat_log.conversation_id, + data=text, + ) + try: + data = json_loads(text) + except JSONDecodeError as err: + raise HomeAssistantError( + "Error with OpenRouter structured response" + ) from err + + return ai_task.GenDataTaskResult( + conversation_id=chat_log.conversation_id, + data=data, + ) diff --git a/homeassistant/components/open_router/config_flow.py b/homeassistant/components/open_router/config_flow.py index 96f3769575b..2afe2129a4c 100644 --- a/homeassistant/components/open_router/config_flow.py +++ b/homeassistant/components/open_router/config_flow.py @@ -5,7 +5,12 @@ from __future__ import annotations import logging from typing import Any -from python_open_router import Model, OpenRouterClient, OpenRouterError +from python_open_router import ( + Model, + OpenRouterClient, + OpenRouterError, + SupportedParameter, +) import voluptuous as vol from homeassistant.config_entries import ( @@ -43,7 +48,10 @@ class OpenRouterConfigFlow(ConfigFlow, domain=DOMAIN): cls, config_entry: ConfigEntry ) -> dict[str, type[ConfigSubentryFlow]]: """Return subentries supported by this handler.""" - return {"conversation": ConversationFlowHandler} + return { + "conversation": ConversationFlowHandler, + "ai_task_data": AITaskDataFlowHandler, + } async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -78,13 +86,26 @@ class OpenRouterConfigFlow(ConfigFlow, domain=DOMAIN): ) -class ConversationFlowHandler(ConfigSubentryFlow): - """Handle subentry flow.""" +class OpenRouterSubentryFlowHandler(ConfigSubentryFlow): + """Handle subentry flow for OpenRouter.""" def __init__(self) -> None: """Initialize the subentry flow.""" self.models: dict[str, Model] = {} + async def _get_models(self) -> None: + """Fetch models from OpenRouter.""" + entry = self._get_entry() + client = OpenRouterClient( + entry.data[CONF_API_KEY], async_get_clientsession(self.hass) + ) + models = await client.get_models() + self.models = {model.id: model for model in models} + + +class ConversationFlowHandler(OpenRouterSubentryFlowHandler): + """Handle subentry flow.""" + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> SubentryFlowResult: @@ -95,14 +116,16 @@ class ConversationFlowHandler(ConfigSubentryFlow): return self.async_create_entry( title=self.models[user_input[CONF_MODEL]].name, data=user_input ) - entry = self._get_entry() - client = OpenRouterClient( - entry.data[CONF_API_KEY], async_get_clientsession(self.hass) - ) - models = await client.get_models() - self.models = {model.id: model for model in models} + try: + await self._get_models() + except OpenRouterError: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") options = [ - SelectOptionDict(value=model.id, label=model.name) for model in models + SelectOptionDict(value=model.id, label=model.name) + for model in self.models.values() ] hass_apis: list[SelectOptionDict] = [ @@ -138,3 +161,40 @@ class ConversationFlowHandler(ConfigSubentryFlow): } ), ) + + +class AITaskDataFlowHandler(OpenRouterSubentryFlowHandler): + """Handle subentry flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> SubentryFlowResult: + """User flow to create a sensor subentry.""" + if user_input is not None: + return self.async_create_entry( + title=self.models[user_input[CONF_MODEL]].name, data=user_input + ) + try: + await self._get_models() + except OpenRouterError: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + options = [ + SelectOptionDict(value=model.id, label=model.name) + for model in self.models.values() + if SupportedParameter.STRUCTURED_OUTPUTS in model.supported_parameters + ] + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_MODEL): SelectSelector( + SelectSelectorConfig( + options=options, mode=SelectSelectorMode.DROPDOWN, sort=True + ), + ), + } + ), + ) diff --git a/homeassistant/components/open_router/conversation.py b/homeassistant/components/open_router/conversation.py index 826931d3da7..3c185ecd77c 100644 --- a/homeassistant/components/open_router/conversation.py +++ b/homeassistant/components/open_router/conversation.py @@ -20,6 +20,8 @@ async def async_setup_entry( ) -> None: """Set up conversation entities.""" for subentry_id, subentry in config_entry.subentries.items(): + if subentry.subentry_type != "conversation": + continue async_add_entities( [OpenRouterConversationEntity(config_entry, subentry)], config_subentry_id=subentry_id, diff --git a/homeassistant/components/open_router/entity.py b/homeassistant/components/open_router/entity.py index e706656d377..ac01ec89704 100644 --- a/homeassistant/components/open_router/entity.py +++ b/homeassistant/components/open_router/entity.py @@ -4,10 +4,9 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Callable import json -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import openai -from openai import NOT_GIVEN from openai.types.chat import ( ChatCompletionAssistantMessageParam, ChatCompletionMessage, @@ -19,7 +18,9 @@ from openai.types.chat import ( ChatCompletionUserMessageParam, ) from openai.types.chat.chat_completion_message_tool_call_param import Function -from openai.types.shared_params import FunctionDefinition +from openai.types.shared_params import FunctionDefinition, ResponseFormatJSONSchema +from openai.types.shared_params.response_format_json_schema import JSONSchema +import voluptuous as vol from voluptuous_openapi import convert from homeassistant.components import conversation @@ -36,6 +37,50 @@ from .const import DOMAIN, LOGGER MAX_TOOL_ITERATIONS = 10 +def _adjust_schema(schema: dict[str, Any]) -> None: + """Adjust the schema to be compatible with OpenRouter API.""" + if schema["type"] == "object": + if "properties" not in schema: + return + + if "required" not in schema: + schema["required"] = [] + + # Ensure all properties are required + for prop, prop_info in schema["properties"].items(): + _adjust_schema(prop_info) + if prop not in schema["required"]: + prop_info["type"] = [prop_info["type"], "null"] + schema["required"].append(prop) + + elif schema["type"] == "array": + if "items" not in schema: + return + + _adjust_schema(schema["items"]) + + +def _format_structured_output( + name: str, schema: vol.Schema, llm_api: llm.APIInstance | None +) -> JSONSchema: + """Format the schema to be compatible with OpenRouter API.""" + result: JSONSchema = { + "name": name, + "strict": True, + } + result_schema = convert( + schema, + custom_serializer=( + llm_api.custom_serializer if llm_api else llm.selector_serializer + ), + ) + + _adjust_schema(result_schema) + + result["schema"] = result_schema + return result + + def _format_tool( tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None, @@ -136,9 +181,24 @@ class OpenRouterEntity(Entity): entry_type=dr.DeviceEntryType.SERVICE, ) - async def _async_handle_chat_log(self, chat_log: conversation.ChatLog) -> None: + async def _async_handle_chat_log( + self, + chat_log: conversation.ChatLog, + structure_name: str | None = None, + structure: vol.Schema | None = None, + ) -> None: """Generate an answer for the chat log.""" + model_args = { + "model": self.model, + "user": chat_log.conversation_id, + "extra_headers": { + "X-Title": "Home Assistant", + "HTTP-Referer": "https://www.home-assistant.io/integrations/open_router", + }, + "extra_body": {"require_parameters": True}, + } + tools: list[ChatCompletionToolParam] | None = None if chat_log.llm_api: tools = [ @@ -146,33 +206,37 @@ class OpenRouterEntity(Entity): for tool in chat_log.llm_api.tools ] - messages = [ + if tools: + model_args["tools"] = tools + + model_args["messages"] = [ m for content in chat_log.content if (m := _convert_content_to_chat_message(content)) ] + if structure: + if TYPE_CHECKING: + assert structure_name is not None + model_args["response_format"] = ResponseFormatJSONSchema( + type="json_schema", + json_schema=_format_structured_output( + structure_name, structure, chat_log.llm_api + ), + ) + client = self.entry.runtime_data for _iteration in range(MAX_TOOL_ITERATIONS): try: - result = await client.chat.completions.create( - model=self.model, - messages=messages, - tools=tools or NOT_GIVEN, - user=chat_log.conversation_id, - extra_headers={ - "X-Title": "Home Assistant", - "HTTP-Referer": "https://www.home-assistant.io/integrations/open_router", - }, - ) + result = await client.chat.completions.create(**model_args) except openai.OpenAIError as err: LOGGER.error("Error talking to API: %s", err) raise HomeAssistantError("Error talking to API") from err result_message = result.choices[0].message - messages.extend( + model_args["messages"].extend( [ msg async for content in chat_log.async_add_delta_content_stream( diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index fab62e7971c..8f989e63189 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.93.3", "python-open-router==0.3.0"] + "requirements": ["openai==1.93.3", "python-open-router==0.3.1"] } diff --git a/homeassistant/components/open_router/strings.json b/homeassistant/components/open_router/strings.json index 91c4cc350ae..e73a65cd178 100644 --- a/homeassistant/components/open_router/strings.json +++ b/homeassistant/components/open_router/strings.json @@ -37,7 +37,28 @@ "initiate_flow": { "user": "Add conversation agent" }, - "entry_type": "Conversation agent" + "entry_type": "Conversation agent", + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "ai_task_data": { + "step": { + "user": { + "data": { + "model": "[%key:component::open_router::config_subentries::conversation::step::user::data::model%]" + } + } + }, + "initiate_flow": { + "user": "Add Generate data with AI service" + }, + "entry_type": "Generate data with AI service", + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } } } } diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index 60b67731eac..48b99749ca1 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -211,7 +211,7 @@ }, "turn_away_mode_on": { "name": "Set away mode", - "description": "Turns away mode on for the heater", + "description": "Turns on away mode for the water heater", "fields": { "duration_days": { "name": "Duration in days", diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index 996faa99c5b..8000cbcddde 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -79,6 +79,7 @@ class PingDataICMPLib(PingData): "min": data.min_rtt, "max": data.max_rtt, "avg": data.avg_rtt, + "jitter": data.jitter, } diff --git a/homeassistant/components/ping/sensor.py b/homeassistant/components/ping/sensor.py index 82d88064e02..b3866c9f0e7 100644 --- a/homeassistant/components/ping/sensor.py +++ b/homeassistant/components/ping/sensor.py @@ -71,6 +71,17 @@ SENSORS: tuple[PingSensorEntityDescription, ...] = ( value_fn=lambda result: result.data.get("min"), has_fn=lambda result: "min" in result.data, ), + PingSensorEntityDescription( + key="jitter", + translation_key="jitter", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("jitter"), + has_fn=lambda result: "jitter" in result.data, + ), ) diff --git a/homeassistant/components/ping/strings.json b/homeassistant/components/ping/strings.json index c301a1b277d..4dc2e8ec7fc 100644 --- a/homeassistant/components/ping/strings.json +++ b/homeassistant/components/ping/strings.json @@ -12,6 +12,9 @@ }, "round_trip_time_min": { "name": "Round-trip time minimum" + }, + "jitter": { + "name": "Jitter" } } }, diff --git a/homeassistant/components/playstation_network/__init__.py b/homeassistant/components/playstation_network/__init__.py index bfa9de5d5cb..c2399c61f93 100644 --- a/homeassistant/components/playstation_network/__init__.py +++ b/homeassistant/components/playstation_network/__init__.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant from .const import CONF_NPSSO from .coordinator import ( PlaystationNetworkConfigEntry, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkGroupsUpdateCoordinator, PlaystationNetworkRuntimeData, PlaystationNetworkTrophyTitlesCoordinator, @@ -39,14 +40,33 @@ async def async_setup_entry( groups = PlaystationNetworkGroupsUpdateCoordinator(hass, psn, entry) await groups.async_config_entry_first_refresh() + friends = {} + + for subentry_id, subentry in entry.subentries.items(): + friend_coordinator = PlaystationNetworkFriendDataCoordinator( + hass, psn, entry, subentry + ) + await friend_coordinator.async_config_entry_first_refresh() + friends[subentry_id] = friend_coordinator + entry.runtime_data = PlaystationNetworkRuntimeData( - coordinator, trophy_titles, groups + coordinator, trophy_titles, groups, friends ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True +async def _async_update_listener( + hass: HomeAssistant, entry: PlaystationNetworkConfigEntry +) -> None: + """Handle update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry( hass: HomeAssistant, entry: PlaystationNetworkConfigEntry ) -> bool: diff --git a/homeassistant/components/playstation_network/config_flow.py b/homeassistant/components/playstation_network/config_flow.py index 0e69abf1080..d4822225c61 100644 --- a/homeassistant/components/playstation_network/config_flow.py +++ b/homeassistant/components/playstation_network/config_flow.py @@ -10,13 +10,28 @@ from psnawp_api.core.psnawp_exceptions import ( PSNAWPInvalidTokenError, PSNAWPNotFoundError, ) +from psnawp_api.models import User from psnawp_api.utils.misc import parse_npsso_token import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + ConfigSubentryFlow, + SubentryFlowResult, +) from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) -from .const import CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK +from .const import CONF_ACCOUNT_ID, CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK +from .coordinator import PlaystationNetworkConfigEntry from .helpers import PlaystationNetwork _LOGGER = logging.getLogger(__name__) @@ -27,6 +42,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_NPSSO): str}) class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Playstation Network.""" + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: ConfigEntry + ) -> dict[str, type[ConfigSubentryFlow]]: + """Return subentries supported by this integration.""" + return {"friend": FriendSubentryFlowHandler} + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -54,6 +77,15 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(user.account_id) self._abort_if_unique_id_configured() + config_entries = self.hass.config_entries.async_entries(DOMAIN) + for entry in config_entries: + if user.account_id in { + subentry.unique_id for subentry in entry.subentries.values() + }: + return self.async_abort( + reason="already_configured_as_subentry" + ) + return self.async_create_entry( title=user.online_id, data={CONF_NPSSO: npsso}, @@ -132,3 +164,61 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): "psn_link": PSN_LINK, }, ) + + +class FriendSubentryFlowHandler(ConfigSubentryFlow): + """Handle subentry flow for adding a friend.""" + + friends_list: dict[str, User] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> SubentryFlowResult: + """Subentry user flow.""" + config_entry: PlaystationNetworkConfigEntry = self._get_entry() + + if user_input is not None: + config_entries = self.hass.config_entries.async_entries(DOMAIN) + if user_input[CONF_ACCOUNT_ID] in { + entry.unique_id for entry in config_entries + }: + return self.async_abort(reason="already_configured_as_entry") + for entry in config_entries: + if user_input[CONF_ACCOUNT_ID] in { + subentry.unique_id for subentry in entry.subentries.values() + }: + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=self.friends_list[user_input[CONF_ACCOUNT_ID]].online_id, + data={}, + unique_id=user_input[CONF_ACCOUNT_ID], + ) + + self.friends_list = await self.hass.async_add_executor_job( + lambda: { + friend.account_id: friend + for friend in config_entry.runtime_data.user_data.psn.user.friends_list() + } + ) + + options = [ + SelectOptionDict( + value=friend.account_id, + label=friend.online_id, + ) + for friend in self.friends_list.values() + ] + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_ACCOUNT_ID): SelectSelector( + SelectSelectorConfig(options=options) + ) + } + ), + user_input, + ), + ) diff --git a/homeassistant/components/playstation_network/const.py b/homeassistant/components/playstation_network/const.py index f4c5c7a3e5b..df553a2ec01 100644 --- a/homeassistant/components/playstation_network/const.py +++ b/homeassistant/components/playstation_network/const.py @@ -6,6 +6,7 @@ from psnawp_api.models.trophies import PlatformType DOMAIN = "playstation_network" CONF_NPSSO: Final = "npsso" +CONF_ACCOUNT_ID: Final = "account_id" SUPPORTED_PLATFORMS = { PlatformType.PS_VITA, diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index 19153d1bb01..977632de23b 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -6,18 +6,27 @@ from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta import logging +from typing import TYPE_CHECKING, Any from psnawp_api.core.psnawp_exceptions import ( PSNAWPAuthenticationError, PSNAWPClientError, + PSNAWPError, + PSNAWPForbiddenError, + PSNAWPNotFoundError, PSNAWPServerError, ) +from psnawp_api.models import User from psnawp_api.models.group.group_datatypes import GroupDetails from psnawp_api.models.trophies import TrophyTitle -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigSubentry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -35,6 +44,7 @@ class PlaystationNetworkRuntimeData: user_data: PlaystationNetworkUserDataCoordinator trophy_titles: PlaystationNetworkTrophyTitlesCoordinator groups: PlaystationNetworkGroupsUpdateCoordinator + friends: dict[str, PlaystationNetworkFriendDataCoordinator] class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): @@ -140,3 +150,80 @@ class PlaystationNetworkGroupsUpdateCoordinator( if not group_info.group_id.startswith("~") } ) + + +class PlaystationNetworkFriendDataCoordinator( + PlayStationNetworkBaseCoordinator[PlaystationNetworkData] +): + """Friend status data update coordinator for PSN.""" + + user: User + profile: dict[str, Any] + + def __init__( + self, + hass: HomeAssistant, + psn: PlaystationNetwork, + config_entry: PlaystationNetworkConfigEntry, + subentry: ConfigSubentry, + ) -> None: + """Initialize the Coordinator.""" + self._update_interval = timedelta( + seconds=max(9 * len(config_entry.subentries), 180) + ) + super().__init__(hass, psn, config_entry) + self.subentry = subentry + + def _setup(self) -> None: + """Set up the coordinator.""" + if TYPE_CHECKING: + assert self.subentry.unique_id + self.user = self.psn.psn.user(account_id=self.subentry.unique_id) + self.profile = self.user.profile() + + async def _async_setup(self) -> None: + """Set up the coordinator.""" + + try: + await self.hass.async_add_executor_job(self._setup) + except PSNAWPNotFoundError as error: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="user_not_found", + translation_placeholders={"user": self.subentry.title}, + ) from error + + except PSNAWPAuthenticationError as error: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="not_ready", + ) from error + + except (PSNAWPServerError, PSNAWPClientError) as error: + _LOGGER.debug("Update failed", exc_info=True) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="update_failed", + ) from error + + def _update_data(self) -> PlaystationNetworkData: + """Update friend status data.""" + try: + return PlaystationNetworkData( + username=self.user.online_id, + account_id=self.user.account_id, + presence=self.user.get_presence(), + profile=self.profile, + ) + except PSNAWPForbiddenError as error: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="user_profile_private", + translation_placeholders={"user": self.subentry.title}, + ) from error + except PSNAWPError: + raise + + async def update_data(self) -> PlaystationNetworkData: + """Update friend status data.""" + return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/playstation_network/entity.py b/homeassistant/components/playstation_network/entity.py index ad7c52bdb39..dc1f126505c 100644 --- a/homeassistant/components/playstation_network/entity.py +++ b/homeassistant/components/playstation_network/entity.py @@ -2,12 +2,14 @@ from typing import TYPE_CHECKING +from homeassistant.config_entries import ConfigSubentry from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import PlayStationNetworkBaseCoordinator +from .helpers import PlaystationNetworkData class PlaystationNetworkServiceEntity( @@ -21,18 +23,32 @@ class PlaystationNetworkServiceEntity( self, coordinator: PlayStationNetworkBaseCoordinator, entity_description: EntityDescription, + subentry: ConfigSubentry | None = None, ) -> None: """Initialize PlayStation Network Service Entity.""" super().__init__(coordinator) if TYPE_CHECKING: assert coordinator.config_entry.unique_id self.entity_description = entity_description - self._attr_unique_id = ( - f"{coordinator.config_entry.unique_id}_{entity_description.key}" + self.subentry = subentry + unique_id = ( + subentry.unique_id + if subentry is not None and subentry.unique_id + else coordinator.config_entry.unique_id ) + + self._attr_unique_id = f"{unique_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, - name=coordinator.psn.user.online_id, + identifiers={(DOMAIN, unique_id)}, + name=( + coordinator.data.username + if isinstance(coordinator.data, PlaystationNetworkData) + else coordinator.psn.user.online_id + ), entry_type=DeviceEntryType.SERVICE, manufacturer="Sony Interactive Entertainment", ) + if subentry: + self._attr_device_info.update( + DeviceInfo(via_device=(DOMAIN, coordinator.config_entry.unique_id)) + ) diff --git a/homeassistant/components/playstation_network/helpers.py b/homeassistant/components/playstation_network/helpers.py index 9960d8afd79..492a011cf78 100644 --- a/homeassistant/components/playstation_network/helpers.py +++ b/homeassistant/components/playstation_network/helpers.py @@ -38,7 +38,6 @@ class PlaystationNetworkData: presence: dict[str, Any] = field(default_factory=dict) username: str = "" account_id: str = "" - availability: str = "unavailable" active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict) registered_platforms: set[PlatformType] = field(default_factory=set) trophy_summary: TrophySummary | None = None @@ -61,6 +60,7 @@ class PlaystationNetwork: self.legacy_profile: dict[str, Any] | None = None self.trophy_titles: list[TrophyTitle] = [] self._title_icon_urls: dict[str, str] = {} + self.friends_list: dict[str, User] | None = None def _setup(self) -> None: """Setup PSN.""" @@ -97,6 +97,7 @@ class PlaystationNetwork: # check legacy platforms if owned if LEGACY_PLATFORMS & data.registered_platforms: self.legacy_profile = self.client.get_profile_legacy() + return data async def get_data(self) -> PlaystationNetworkData: @@ -105,7 +106,6 @@ class PlaystationNetwork: data.username = self.user.online_id data.account_id = self.user.account_id data.shareable_profile_link = self.shareable_profile_link - data.availability = data.presence["basicPresence"]["availability"] if "platform" in data.presence["basicPresence"]["primaryPlatformInfo"]: primary_platform = PlatformType( @@ -193,3 +193,17 @@ class PlaystationNetwork: def normalize_title(name: str) -> str: """Normalize trophy title.""" return name.removesuffix("Trophies").removesuffix("Trophy Set").strip() + + +def get_game_title_info(presence: dict[str, Any]) -> dict[str, Any]: + """Retrieve title info from presence.""" + + return ( + next((title for title in game_title_info), {}) + if ( + game_title_info := presence.get("basicPresence", {}).get( + "gameTitleInfoList" + ) + ) + else {} + ) diff --git a/homeassistant/components/playstation_network/icons.json b/homeassistant/components/playstation_network/icons.json index af2236bd126..5997f43fb5c 100644 --- a/homeassistant/components/playstation_network/icons.json +++ b/homeassistant/components/playstation_network/icons.json @@ -42,6 +42,13 @@ "availabletocommunicate": "mdi:cellphone", "offline": "mdi:account-off-outline" } + }, + "now_playing": { + "default": "mdi:controller", + "state": { + "unknown": "mdi:controller-off", + "unavailable": "mdi:controller-off" + } } }, "image": { diff --git a/homeassistant/components/playstation_network/image.py b/homeassistant/components/playstation_network/image.py index b0195002c66..0a8e5daed62 100644 --- a/homeassistant/components/playstation_network/image.py +++ b/homeassistant/components/playstation_network/image.py @@ -5,18 +5,23 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum +from typing import TYPE_CHECKING from homeassistant.components.image import ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util from .coordinator import ( + PlayStationNetworkBaseCoordinator, PlaystationNetworkConfigEntry, PlaystationNetworkData, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkUserDataCoordinator, ) from .entity import PlaystationNetworkServiceEntity +from .helpers import get_game_title_info PARALLEL_UPDATES = 0 @@ -26,6 +31,7 @@ class PlaystationNetworkImage(StrEnum): AVATAR = "avatar" SHARE_PROFILE = "share_profile" + NOW_PLAYING_IMAGE = "now_playing_image" @dataclass(kw_only=True, frozen=True) @@ -35,12 +41,14 @@ class PlaystationNetworkImageEntityDescription(ImageEntityDescription): image_url_fn: Callable[[PlaystationNetworkData], str | None] -IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = ( +IMAGE_DESCRIPTIONS_ME: tuple[PlaystationNetworkImageEntityDescription, ...] = ( PlaystationNetworkImageEntityDescription( key=PlaystationNetworkImage.SHARE_PROFILE, translation_key=PlaystationNetworkImage.SHARE_PROFILE, image_url_fn=lambda data: data.shareable_profile_link["shareImageUrl"], ), +) +IMAGE_DESCRIPTIONS_ALL: tuple[PlaystationNetworkImageEntityDescription, ...] = ( PlaystationNetworkImageEntityDescription( key=PlaystationNetworkImage.AVATAR, translation_key=PlaystationNetworkImage.AVATAR, @@ -55,6 +63,14 @@ IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = ( ) ), ), + PlaystationNetworkImageEntityDescription( + key=PlaystationNetworkImage.NOW_PLAYING_IMAGE, + translation_key=PlaystationNetworkImage.NOW_PLAYING_IMAGE, + image_url_fn=( + lambda data: get_game_title_info(data.presence).get("conceptIconUrl") + or get_game_title_info(data.presence).get("npTitleIconUrl") + ), + ), ) @@ -70,25 +86,43 @@ async def async_setup_entry( async_add_entities( [ PlaystationNetworkImageEntity(hass, coordinator, description) - for description in IMAGE_DESCRIPTIONS + for description in IMAGE_DESCRIPTIONS_ME + IMAGE_DESCRIPTIONS_ALL ] ) + for ( + subentry_id, + friend_data_coordinator, + ) in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkFriendImageEntity( + hass, + friend_data_coordinator, + description, + config_entry.subentries[subentry_id], + ) + for description in IMAGE_DESCRIPTIONS_ALL + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity): + +class PlaystationNetworkImageBaseEntity(PlaystationNetworkServiceEntity, ImageEntity): """An image entity.""" entity_description: PlaystationNetworkImageEntityDescription - coordinator: PlaystationNetworkUserDataCoordinator + coordinator: PlayStationNetworkBaseCoordinator def __init__( self, hass: HomeAssistant, - coordinator: PlaystationNetworkUserDataCoordinator, + coordinator: PlayStationNetworkBaseCoordinator, entity_description: PlaystationNetworkImageEntityDescription, + subentry: ConfigSubentry | None = None, ) -> None: """Initialize the image entity.""" - super().__init__(coordinator, entity_description) + super().__init__(coordinator, entity_description, subentry) ImageEntity.__init__(self, hass) self._attr_image_url = self.entity_description.image_url_fn(coordinator.data) @@ -96,6 +130,8 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" + if TYPE_CHECKING: + assert isinstance(self.coordinator.data, PlaystationNetworkData) url = self.entity_description.image_url_fn(self.coordinator.data) if url != self._attr_image_url: @@ -104,3 +140,15 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity self._attr_image_last_updated = dt_util.utcnow() super()._handle_coordinator_update() + + +class PlaystationNetworkImageEntity(PlaystationNetworkImageBaseEntity): + """An image entity.""" + + coordinator: PlaystationNetworkUserDataCoordinator + + +class PlaystationNetworkFriendImageEntity(PlaystationNetworkImageBaseEntity): + """An image entity.""" + + coordinator: PlaystationNetworkFriendDataCoordinator diff --git a/homeassistant/components/playstation_network/notify.py b/homeassistant/components/playstation_network/notify.py index 872ad98a594..a06359ebffc 100644 --- a/homeassistant/components/playstation_network/notify.py +++ b/homeassistant/components/playstation_network/notify.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import StrEnum +from typing import TYPE_CHECKING from psnawp_api.core.psnawp_exceptions import ( PSNAWPClientError, @@ -10,12 +11,14 @@ from psnawp_api.core.psnawp_exceptions import ( PSNAWPNotFoundError, PSNAWPServerError, ) +from psnawp_api.models.group.group import Group from homeassistant.components.notify import ( DOMAIN as NOTIFY_DOMAIN, NotifyEntity, NotifyEntityDescription, ) +from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -24,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ( PlaystationNetworkConfigEntry, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkGroupsUpdateCoordinator, ) from .entity import PlaystationNetworkServiceEntity @@ -35,6 +39,7 @@ class PlaystationNetworkNotify(StrEnum): """PlayStation Network sensors.""" GROUP_MESSAGE = "group_message" + DIRECT_MESSAGE = "direct_message" async def async_setup_entry( @@ -45,6 +50,7 @@ async def async_setup_entry( """Set up the notify entity platform.""" coordinator = config_entry.runtime_data.groups + groups_added: set[str] = set() entity_registry = er.async_get(hass) @@ -72,8 +78,50 @@ async def async_setup_entry( coordinator.async_add_listener(add_entities) add_entities() + for subentry_id, friend_coordinator in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkDirectMessageNotifyEntity( + friend_coordinator, + config_entry.subentries[subentry_id], + ) + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEntity): + +class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity): + """Base class of PlayStation Network notify entity.""" + + group: Group | None = None + + def send_message(self, message: str, title: str | None = None) -> None: + """Send a message.""" + if TYPE_CHECKING: + assert self.group + try: + self.group.send_message(message) + except PSNAWPNotFoundError as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="group_invalid", + translation_placeholders=dict(self.translation_placeholders), + ) from e + except PSNAWPForbiddenError as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="send_message_forbidden", + translation_placeholders=dict(self.translation_placeholders), + ) from e + except (PSNAWPServerError, PSNAWPClientError) as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="send_message_failed", + translation_placeholders=dict(self.translation_placeholders), + ) from e + + +class PlaystationNetworkNotifyEntity(PlaystationNetworkNotifyBaseEntity): """Representation of a PlayStation Network notify entity.""" coordinator: PlaystationNetworkGroupsUpdateCoordinator @@ -101,26 +149,31 @@ class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEnti super().__init__(coordinator, self.entity_description) + +class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity): + """Representation of a PlayStation Network notify entity for sending direct messages.""" + + coordinator: PlaystationNetworkFriendDataCoordinator + + def __init__( + self, + coordinator: PlaystationNetworkFriendDataCoordinator, + subentry: ConfigSubentry, + ) -> None: + """Initialize a notification entity.""" + + self.entity_description = NotifyEntityDescription( + key=PlaystationNetworkNotify.DIRECT_MESSAGE, + translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE, + ) + + super().__init__(coordinator, self.entity_description, subentry) + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" - try: - self.group.send_message(message) - except PSNAWPNotFoundError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="group_invalid", - translation_placeholders=dict(self.translation_placeholders), - ) from e - except PSNAWPForbiddenError as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="send_message_forbidden", - translation_placeholders=dict(self.translation_placeholders), - ) from e - except (PSNAWPServerError, PSNAWPClientError) as e: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="send_message_failed", - translation_placeholders=dict(self.translation_placeholders), - ) from e + if not self.group: + self.group = self.coordinator.psn.psn.group( + users_list=[self.coordinator.user] + ) + super().send_message(message, title) diff --git a/homeassistant/components/playstation_network/sensor.py b/homeassistant/components/playstation_network/sensor.py index 63cca074c3e..16d1ff13906 100644 --- a/homeassistant/components/playstation_network/sensor.py +++ b/homeassistant/components/playstation_network/sensor.py @@ -19,11 +19,14 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .coordinator import ( + PlayStationNetworkBaseCoordinator, PlaystationNetworkConfigEntry, PlaystationNetworkData, + PlaystationNetworkFriendDataCoordinator, PlaystationNetworkUserDataCoordinator, ) from .entity import PlaystationNetworkServiceEntity +from .helpers import get_game_title_info PARALLEL_UPDATES = 0 @@ -33,7 +36,6 @@ class PlaystationNetworkSensorEntityDescription(SensorEntityDescription): """PlayStation Network sensor description.""" value_fn: Callable[[PlaystationNetworkData], StateType | datetime] - entity_picture: str | None = None available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True @@ -49,9 +51,10 @@ class PlaystationNetworkSensor(StrEnum): ONLINE_ID = "online_id" LAST_ONLINE = "last_online" ONLINE_STATUS = "online_status" + NOW_PLAYING = "now_playing" -SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( +SENSOR_DESCRIPTIONS_TROPHY: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.TROPHY_LEVEL, translation_key=PlaystationNetworkSensor.TROPHY_LEVEL, @@ -103,6 +106,8 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( else None ), ), +) +SENSOR_DESCRIPTIONS_USER: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.ONLINE_ID, translation_key=PlaystationNetworkSensor.ONLINE_ID, @@ -122,10 +127,19 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = ( PlaystationNetworkSensorEntityDescription( key=PlaystationNetworkSensor.ONLINE_STATUS, translation_key=PlaystationNetworkSensor.ONLINE_STATUS, - value_fn=lambda psn: psn.availability.lower().replace("unavailable", "offline"), + value_fn=( + lambda psn: psn.presence["basicPresence"]["availability"] + .lower() + .replace("unavailable", "offline") + ), device_class=SensorDeviceClass.ENUM, options=["offline", "availabletoplay", "availabletocommunicate", "busy"], ), + PlaystationNetworkSensorEntityDescription( + key=PlaystationNetworkSensor.NOW_PLAYING, + translation_key=PlaystationNetworkSensor.NOW_PLAYING, + value_fn=lambda psn: get_game_title_info(psn.presence).get("titleName"), + ), ) @@ -138,18 +152,34 @@ async def async_setup_entry( coordinator = config_entry.runtime_data.user_data async_add_entities( PlaystationNetworkSensorEntity(coordinator, description) - for description in SENSOR_DESCRIPTIONS + for description in SENSOR_DESCRIPTIONS_TROPHY + SENSOR_DESCRIPTIONS_USER ) + for ( + subentry_id, + friend_data_coordinator, + ) in config_entry.runtime_data.friends.items(): + async_add_entities( + [ + PlaystationNetworkFriendSensorEntity( + friend_data_coordinator, + description, + config_entry.subentries[subentry_id], + ) + for description in SENSOR_DESCRIPTIONS_USER + ], + config_subentry_id=subentry_id, + ) -class PlaystationNetworkSensorEntity( + +class PlaystationNetworkSensorBaseEntity( PlaystationNetworkServiceEntity, SensorEntity, ): - """Representation of a PlayStation Network sensor entity.""" + """Base sensor entity.""" entity_description: PlaystationNetworkSensorEntityDescription - coordinator: PlaystationNetworkUserDataCoordinator + coordinator: PlayStationNetworkBaseCoordinator @property def native_value(self) -> StateType | datetime: @@ -169,14 +199,24 @@ class PlaystationNetworkSensorEntity( (pic.get("url") for pic in profile_pictures if pic.get("size") == "xl"), None, ) - return super().entity_picture @property def available(self) -> bool: """Return True if entity is available.""" - return ( - self.entity_description.available_fn(self.coordinator.data) - and super().available + return super().available and self.entity_description.available_fn( + self.coordinator.data ) + + +class PlaystationNetworkSensorEntity(PlaystationNetworkSensorBaseEntity): + """Representation of a PlayStation Network sensor entity.""" + + coordinator: PlaystationNetworkUserDataCoordinator + + +class PlaystationNetworkFriendSensorEntity(PlaystationNetworkSensorBaseEntity): + """Representation of a PlayStation Network sensor entity.""" + + coordinator: PlaystationNetworkFriendDataCoordinator diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index 4fefc508ea2..26a1b336e2d 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -39,11 +39,40 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_as_subentry": "Already configured as a friend for another account. Delete the existing entry first.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unique_id_mismatch": "The provided NPSSO token corresponds to the account {wrong_account}. Please re-authenticate with the account **{name}**", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, + "config_subentries": { + "friend": { + "step": { + "user": { + "title": "Friend online status", + "description": "Track the online status of a PlayStation Network friend.", + "data": { + "account_id": "Online ID" + }, + "data_description": { + "account_id": "Select a friend from your friend list to track their online status." + } + } + }, + "initiate_flow": { + "user": "Add friend" + }, + "entry_type": "Friend", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured_as_entry": "Already configured as a service. This account cannot be added as a friend.", + "already_configured": "Already configured as a friend in this or another account." + } + } + }, "exceptions": { "not_ready": { "message": "Authentication to the PlayStation Network failed." @@ -59,6 +88,12 @@ }, "send_message_failed": { "message": "Failed to send message to group {group_name}. Try again later." + }, + "user_profile_private": { + "message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity." + }, + "user_not_found": { + "message": "Unable to retrieve data for {user}. User does not exist or has been removed." } }, "entity": { @@ -104,6 +139,9 @@ "availabletocommunicate": "Online on PS App", "busy": "Away" } + }, + "now_playing": { + "name": "Now playing" } }, "image": { @@ -112,11 +150,17 @@ }, "avatar": { "name": "Avatar" + }, + "now_playing_image": { + "name": "[%key:component::playstation_network::entity::sensor::now_playing::name%]" } }, "notify": { "group_message": { "name": "Group: {group_name}" + }, + "direct_message": { + "name": "Direct message" } } } diff --git a/homeassistant/components/qbus/climate.py b/homeassistant/components/qbus/climate.py index caaec2f95d7..a19ec4d0156 100644 --- a/homeassistant/components/qbus/climate.py +++ b/homeassistant/components/qbus/climate.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -42,13 +42,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "thermo", QbusClimate, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/const.py b/homeassistant/components/qbus/const.py index 73819d2a11b..133a3b8fea9 100644 --- a/homeassistant/components/qbus/const.py +++ b/homeassistant/components/qbus/const.py @@ -10,6 +10,7 @@ PLATFORMS: list[Platform] = [ Platform.COVER, Platform.LIGHT, Platform.SCENE, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/qbus/cover.py b/homeassistant/components/qbus/cover.py index 2adb8253551..3fc1b20602a 100644 --- a/homeassistant/components/qbus/cover.py +++ b/homeassistant/components/qbus/cover.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -36,13 +36,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "shutter", QbusCover, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 91e4d83b548..9fb481d4515 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -14,7 +14,6 @@ from qbusmqttapi.state import QbusMqttState from homeassistant.components.mqtt import ReceiveMessage, client as mqtt from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, MANUFACTURER from .coordinator import QbusControllerCoordinator @@ -24,14 +23,24 @@ _REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$") StateT = TypeVar("StateT", bound=QbusMqttState) -def add_new_outputs( +def create_new_entities( coordinator: QbusControllerCoordinator, added_outputs: list[QbusMqttOutput], filter_fn: Callable[[QbusMqttOutput], bool], entity_type: type[QbusEntity], - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Call async_add_entities for new outputs.""" +) -> list[QbusEntity]: + """Create entities for new outputs.""" + + new_outputs = determine_new_outputs(coordinator, added_outputs, filter_fn) + return [entity_type(output) for output in new_outputs] + + +def determine_new_outputs( + coordinator: QbusControllerCoordinator, + added_outputs: list[QbusMqttOutput], + filter_fn: Callable[[QbusMqttOutput], bool], +) -> list[QbusMqttOutput]: + """Determine new outputs.""" added_ref_ids = {k.ref_id for k in added_outputs} @@ -43,7 +52,8 @@ def add_new_outputs( if new_outputs: added_outputs.extend(new_outputs) - async_add_entities([entity_type(output) for output in new_outputs]) + + return new_outputs def format_ref_id(ref_id: str) -> str | None: @@ -67,7 +77,13 @@ class QbusEntity(Entity, Generic[StateT], ABC): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, mqtt_output: QbusMqttOutput) -> None: + def __init__( + self, + mqtt_output: QbusMqttOutput, + *, + id_suffix: str = "", + link_to_main_device: bool = False, + ) -> None: """Initialize the Qbus entity.""" self._mqtt_output = mqtt_output @@ -79,17 +95,25 @@ class QbusEntity(Entity, Generic[StateT], ABC): ) ref_id = format_ref_id(mqtt_output.ref_id) + unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" - self._attr_unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" + if id_suffix: + unique_id += f"_{id_suffix}" - # Create linked device - self._attr_device_info = DeviceInfo( - name=mqtt_output.name.title(), - manufacturer=MANUFACTURER, - identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, - suggested_area=mqtt_output.location.title(), - via_device=create_main_device_identifier(mqtt_output), - ) + self._attr_unique_id = unique_id + + if link_to_main_device: + self._attr_device_info = DeviceInfo( + identifiers={create_main_device_identifier(mqtt_output)} + ) + else: + self._attr_device_info = DeviceInfo( + name=mqtt_output.name.title(), + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, + suggested_area=mqtt_output.location.title(), + via_device=create_main_device_identifier(mqtt_output), + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/components/qbus/light.py b/homeassistant/components/qbus/light.py index 4385cfe60f0..61225f11243 100644 --- a/homeassistant/components/qbus/light.py +++ b/homeassistant/components/qbus/light.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.color import brightness_to_value, value_to_brightness from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -27,13 +27,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "analog", QbusLight, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/qbus/manifest.json b/homeassistant/components/qbus/manifest.json index feffa6e492c..15392f6cc97 100644 --- a/homeassistant/components/qbus/manifest.json +++ b/homeassistant/components/qbus/manifest.json @@ -7,6 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/qbus", "integration_type": "hub", "iot_class": "local_push", + "loggers": ["qbusmqttapi"], "mqtt": [ "cloudapp/QBUSMQTTGW/state", "cloudapp/QBUSMQTTGW/config", diff --git a/homeassistant/components/qbus/scene.py b/homeassistant/components/qbus/scene.py index 8d18feb26d3..706fb089dde 100644 --- a/homeassistant/components/qbus/scene.py +++ b/homeassistant/components/qbus/scene.py @@ -7,11 +7,10 @@ from qbusmqttapi.state import QbusMqttState, StateAction, StateType from homeassistant.components.scene import Scene from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs, create_main_device_identifier +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -27,13 +26,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "scene", QbusScene, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) @@ -45,12 +44,8 @@ class QbusScene(QbusEntity, Scene): def __init__(self, mqtt_output: QbusMqttOutput) -> None: """Initialize scene entity.""" - super().__init__(mqtt_output) + super().__init__(mqtt_output, link_to_main_device=True) - # Add to main controller device - self._attr_device_info = DeviceInfo( - identifiers={create_main_device_identifier(mqtt_output)} - ) self._attr_name = mqtt_output.name.title() async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/qbus/sensor.py b/homeassistant/components/qbus/sensor.py new file mode 100644 index 00000000000..e983e0a8cbb --- /dev/null +++ b/homeassistant/components/qbus/sensor.py @@ -0,0 +1,378 @@ +"""Support for Qbus sensor.""" + +from dataclasses import dataclass + +from qbusmqttapi.discovery import QbusMqttOutput +from qbusmqttapi.state import ( + GaugeStateProperty, + QbusMqttGaugeState, + QbusMqttHumidityState, + QbusMqttThermoState, + QbusMqttVentilationState, + QbusMqttWeatherState, +) + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, + UnitOfVolumeFlowRate, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import QbusConfigEntry +from .entity import QbusEntity, create_new_entities, determine_new_outputs + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class QbusWeatherDescription(SensorEntityDescription): + """Description for Qbus weather entities.""" + + property: str + + +_WEATHER_DESCRIPTIONS = ( + QbusWeatherDescription( + key="daylight", + property="dayLight", + translation_key="daylight", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light", + property="light", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_east", + property="lightEast", + translation_key="light_east", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_south", + property="lightSouth", + translation_key="light_south", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="light_west", + property="lightWest", + translation_key="light_west", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + QbusWeatherDescription( + key="temperature", + property="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + QbusWeatherDescription( + key="wind", + property="wind", + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + ), +) + +_GAUGE_VARIANT_DESCRIPTIONS = { + "AIRPRESSURE": SensorEntityDescription( + key="airpressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + "AIRQUALITY": SensorEntityDescription( + key="airquality", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + "CURRENT": SensorEntityDescription( + key="current", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + ), + "ENERGY": SensorEntityDescription( + key="energy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + ), + "GAS": SensorEntityDescription( + key="gas", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "GASFLOW": SensorEntityDescription( + key="gasflow", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "HUMIDITY": SensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "LIGHT": SensorEntityDescription( + key="light", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + "LOUDNESS": SensorEntityDescription( + key="loudness", + device_class=SensorDeviceClass.SOUND_PRESSURE, + native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, + state_class=SensorStateClass.MEASUREMENT, + ), + "POWER": SensorEntityDescription( + key="power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + "PRESSURE": SensorEntityDescription( + key="pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.KPA, + state_class=SensorStateClass.MEASUREMENT, + ), + "TEMPERATURE": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "VOLTAGE": SensorEntityDescription( + key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + "VOLUME": SensorEntityDescription( + key="volume", + device_class=SensorDeviceClass.VOLUME_STORAGE, + native_unit_of_measurement=UnitOfVolume.LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATER": SensorEntityDescription( + key="water", + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.LITERS, + state_class=SensorStateClass.TOTAL, + ), + "WATERFLOW": SensorEntityDescription( + key="waterflow", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATERLEVEL": SensorEntityDescription( + key="waterlevel", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "WATERPRESSURE": SensorEntityDescription( + key="waterpressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + "WIND": SensorEntityDescription( + key="wind", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _is_gauge_with_variant(output: QbusMqttOutput) -> bool: + return ( + output.type == "gauge" + and isinstance(output.variant, str) + and _GAUGE_VARIANT_DESCRIPTIONS.get(output.variant.upper()) is not None + ) + + +def _is_ventilation_with_co2(output: QbusMqttOutput) -> bool: + return output.type == "ventilation" and output.properties.get("co2") is not None + + +async def async_setup_entry( + hass: HomeAssistant, + entry: QbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up sensor entities.""" + + coordinator = entry.runtime_data + added_outputs: list[QbusMqttOutput] = [] + + def _create_weather_entities() -> list[QbusEntity]: + new_outputs = determine_new_outputs( + coordinator, added_outputs, lambda output: output.type == "weatherstation" + ) + + return [ + QbusWeatherSensor(output, description) + for output in new_outputs + for description in _WEATHER_DESCRIPTIONS + ] + + def _check_outputs() -> None: + entities: list[QbusEntity] = [ + *create_new_entities( + coordinator, + added_outputs, + _is_gauge_with_variant, + QbusGaugeVariantSensor, + ), + *create_new_entities( + coordinator, + added_outputs, + lambda output: output.type == "humidity", + QbusHumiditySensor, + ), + *create_new_entities( + coordinator, + added_outputs, + lambda output: output.type == "thermo", + QbusThermoSensor, + ), + *create_new_entities( + coordinator, + added_outputs, + _is_ventilation_with_co2, + QbusVentilationSensor, + ), + *_create_weather_entities(), + ] + + async_add_entities(entities) + + _check_outputs() + entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) + + +class QbusGaugeVariantSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for gauges with variant.""" + + _state_cls = QbusMqttGaugeState + + _attr_name = None + _attr_suggested_display_precision = 2 + + def __init__(self, mqtt_output: QbusMqttOutput) -> None: + """Initialize sensor entity.""" + + super().__init__(mqtt_output) + + variant = str(mqtt_output.variant) + self.entity_description = _GAUGE_VARIANT_DESCRIPTIONS[variant.upper()] + + async def _handle_state_received(self, state: QbusMqttGaugeState) -> None: + self._attr_native_value = state.read_value(GaugeStateProperty.CURRENT_VALUE) + + +class QbusHumiditySensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for humidity modules.""" + + _state_cls = QbusMqttHumidityState + + _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_name = None + _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT + + async def _handle_state_received(self, state: QbusMqttHumidityState) -> None: + self._attr_native_value = state.read_value() + + +class QbusThermoSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for thermostats.""" + + _state_cls = QbusMqttThermoState + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + + async def _handle_state_received(self, state: QbusMqttThermoState) -> None: + self._attr_native_value = state.read_current_temperature() + + +class QbusVentilationSensor(QbusEntity, SensorEntity): + """Representation of a Qbus sensor entity for ventilations.""" + + _state_cls = QbusMqttVentilationState + + _attr_device_class = SensorDeviceClass.CO2 + _attr_name = None + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_suggested_display_precision = 0 + + async def _handle_state_received(self, state: QbusMqttVentilationState) -> None: + self._attr_native_value = state.read_co2() + + +class QbusWeatherSensor(QbusEntity, SensorEntity): + """Representation of a Qbus weather sensor.""" + + _state_cls = QbusMqttWeatherState + + entity_description: QbusWeatherDescription + + def __init__( + self, mqtt_output: QbusMqttOutput, description: QbusWeatherDescription + ) -> None: + """Initialize sensor entity.""" + + super().__init__(mqtt_output, id_suffix=description.key) + + self.entity_description = description + + if description.key == "temperature": + self._attr_name = None + + async def _handle_state_received(self, state: QbusMqttWeatherState) -> None: + if value := state.read_property(self.entity_description.property, None): + self.native_value = value diff --git a/homeassistant/components/qbus/strings.json b/homeassistant/components/qbus/strings.json index f308c5b3519..f3a0d108476 100644 --- a/homeassistant/components/qbus/strings.json +++ b/homeassistant/components/qbus/strings.json @@ -16,6 +16,22 @@ "no_controller": "No controllers were found" } }, + "entity": { + "sensor": { + "daylight": { + "name": "Daylight" + }, + "light_east": { + "name": "Illuminance east" + }, + "light_south": { + "name": "Illuminance south" + }, + "light_west": { + "name": "Illuminance west" + } + } + }, "exceptions": { "invalid_preset": { "message": "Preset mode \"{preset}\" is not valid. Valid preset modes are: {options}." diff --git a/homeassistant/components/qbus/switch.py b/homeassistant/components/qbus/switch.py index 05283a44cfc..3c4d280fa30 100644 --- a/homeassistant/components/qbus/switch.py +++ b/homeassistant/components/qbus/switch.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import QbusConfigEntry -from .entity import QbusEntity, add_new_outputs +from .entity import QbusEntity, create_new_entities PARALLEL_UPDATES = 0 @@ -26,13 +26,13 @@ async def async_setup_entry( added_outputs: list[QbusMqttOutput] = [] def _check_outputs() -> None: - add_new_outputs( + entities = create_new_entities( coordinator, added_outputs, lambda output: output.type == "onoff", QbusSwitch, - async_add_entities, ) + async_add_entities(entities) _check_outputs() entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index d57f2dc8eec..450f78f9e83 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -82,6 +82,7 @@ }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", @@ -129,7 +130,7 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 26876b53224..7a7abe37b89 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -39,6 +39,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): _LOGGER, name=f"{DOMAIN}_{config_entry.title}", update_interval=SCAN_INTERVAL, + config_entry=config_entry, always_update=True, ) self._client = get_async_client(hass) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 39541476429..efd9f1121b6 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.14.4"] + "requirements": ["reolink-aio==0.14.5"] } diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index cc7e017699d..63da15b1ede 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -89,8 +89,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager): """ if result.get("type") != data_entry_flow.FlowResultType.ABORT: ir.async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) - if "result" not in result: - result["result"] = None return result diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 28e08372d68..8b9d7ddf37e 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.13.3", "lxml==5.3.0"] + "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.0"] } diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index d46f63c9516..91452287ce7 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,6 +139,7 @@ "selector": { "device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", @@ -155,6 +156,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -184,13 +186,14 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8a75baa69c6..67bf94c61ae 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -573,6 +573,7 @@ class SimpliSafe: self._hass, LOGGER, name=self.entry.title, + config_entry=self.entry, update_interval=DEFAULT_SCAN_INTERVAL, update_method=self.async_update, ) diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 337959e0316..095179d618a 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -74,6 +74,7 @@ class SmartTubController: self._hass, _LOGGER, name=DOMAIN, + config_entry=entry, update_method=self.async_update_data, update_interval=timedelta(seconds=SCAN_INTERVAL), ) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 6c7c5374f7d..78f7899a571 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -83,8 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not gateway: raise ConfigEntryNotReady(f"Cannot find device {device}") - signal_coordinator = SignalCoordinator(hass, gateway) - network_coordinator = NetworkCoordinator(hass, gateway) + signal_coordinator = SignalCoordinator(hass, entry, gateway) + network_coordinator = NetworkCoordinator(hass, entry, gateway) # Fetch initial data so we have data when entities subscribe await signal_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/sms/coordinator.py b/homeassistant/components/sms/coordinator.py index 7bc691afedf..858fc303805 100644 --- a/homeassistant/components/sms/coordinator.py +++ b/homeassistant/components/sms/coordinator.py @@ -16,13 +16,14 @@ _LOGGER = logging.getLogger(__name__) class SignalCoordinator(DataUpdateCoordinator): """Signal strength coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize signal strength coordinator.""" super().__init__( hass, _LOGGER, name="Device signal state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway @@ -38,13 +39,14 @@ class SignalCoordinator(DataUpdateCoordinator): class NetworkCoordinator(DataUpdateCoordinator): """Network info coordinator.""" - def __init__(self, hass, gateway): + def __init__(self, hass, entry, gateway): """Initialize network info coordinator.""" super().__init__( hass, _LOGGER, name="Device network state", update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + config_entry=entry, ) self._gateway = gateway diff --git a/homeassistant/components/snoo/__init__.py b/homeassistant/components/snoo/__init__.py index 54834bf58ce..20d94be7c03 100644 --- a/homeassistant/components/snoo/__init__.py +++ b/homeassistant/components/snoo/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnooConfigEntry) -> bool coordinators: dict[str, SnooCoordinator] = {} tasks = [] for device in devices: - coordinators[device.serialNumber] = SnooCoordinator(hass, device, snoo) + coordinators[device.serialNumber] = SnooCoordinator(hass, entry, device, snoo) tasks.append(coordinators[device.serialNumber].setup()) await asyncio.gather(*tasks) entry.runtime_data = coordinators diff --git a/homeassistant/components/snoo/coordinator.py b/homeassistant/components/snoo/coordinator.py index bc06d20955c..8ce0db34621 100644 --- a/homeassistant/components/snoo/coordinator.py +++ b/homeassistant/components/snoo/coordinator.py @@ -19,11 +19,18 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]): config_entry: SnooConfigEntry - def __init__(self, hass: HomeAssistant, device: SnooDevice, snoo: Snoo) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: SnooConfigEntry, + device: SnooDevice, + snoo: Snoo, + ) -> None: """Set up Snoo Coordinator.""" super().__init__( hass, name=device.name, + config_entry=entry, logger=_LOGGER, ) self.device_unique_id = device.serialNumber diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index f9b8044e992..cbc0deda96a 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,10 +71,13 @@ "selector": { "device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "area": "[%key:component::sensor::entity_component::area::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", + "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", @@ -85,6 +88,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -115,13 +119,14 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index bab4f90c6d1..4f2a1fa7aa5 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib from dataclasses import dataclass, field +import logging from typing import Any from pysqueezebox import Player @@ -21,6 +22,8 @@ from homeassistant.helpers.network import is_internal_request from .const import DOMAIN, UNPLAYABLE_TYPES +_LOGGER = logging.getLogger(__name__) + LIBRARY = [ "favorites", "artists", @@ -138,18 +141,42 @@ class BrowseData: self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE) self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX) + def add_new_command(self, cmd: str | MediaType, type: str) -> None: + """Add items to maps for new apps or radios.""" + self.known_apps_radios.add(cmd) + self.media_type_to_squeezebox[cmd] = cmd + self.squeezebox_id_by_type[cmd] = type + self.content_type_media_class[cmd] = { + "item": MediaClass.DIRECTORY, + "children": MediaClass.TRACK, + } + self.content_type_to_child_type[cmd] = MediaType.TRACK -def _add_new_command_to_browse_data( - browse_data: BrowseData, cmd: str | MediaType, type: str -) -> None: - """Add items to maps for new apps or radios.""" - browse_data.media_type_to_squeezebox[cmd] = cmd - browse_data.squeezebox_id_by_type[cmd] = type - browse_data.content_type_media_class[cmd] = { - "item": MediaClass.DIRECTORY, - "children": MediaClass.TRACK, - } - browse_data.content_type_to_child_type[cmd] = MediaType.TRACK + async def async_init(self, player: Player, browse_limit: int) -> None: + """Initialize known apps and radios from the player.""" + + cmd = ["apps", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) + cmd = ["radios", 0, browse_limit] + result = await player.async_query(*cmd) + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( @@ -292,8 +319,7 @@ async def build_item_response( app_cmd = "app-" + item["cmd"] if app_cmd not in browse_data.known_apps_radios: - browse_data.known_apps_radios.add(app_cmd) - _add_new_command_to_browse_data(browse_data, app_cmd, "item_id") + browse_data.add_new_command(app_cmd, "item_id") child_media = _build_response_apps_radios_category( browse_data=browse_data, cmd=app_cmd, item=item diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 0dbc1b96b0c..49aad4fd698 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -311,6 +311,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): ) return None + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + await super().async_added_to_hass() + await self._browse_data.async_init(self._player, self.browse_limit) + async def async_will_remove_from_hass(self) -> None: """Remove from list of known players when removed from hass.""" self.coordinator.config_entry.runtime_data.known_player_ids.remove( diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 482c5c4a9e6..ae3a32997ae 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -30,6 +30,7 @@ PLATFORMS: list[Platform] = [ Platform.BUTTON, Platform.CLIMATE, Platform.FAN, + Platform.LIGHT, Platform.LOCK, Platform.SENSOR, Platform.SWITCH, @@ -53,6 +54,7 @@ class SwitchbotDevices: vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) + lights: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) @dataclass @@ -142,12 +144,15 @@ async def make_device_data( hass, entry, api, device, coordinators_by_id ) devices_data.sensors.append((device, coordinator)) - if isinstance(device, Device) and device.device_type in [ "K10+", "K10+ Pro", "Robot Vacuum Cleaner S1", "Robot Vacuum Cleaner S1 Plus", + "K20+ Pro", + "Robot Vacuum Cleaner K10+ Pro Combo", + "Robot Vacuum Cleaner S10", + "S20", ]: coordinator = await coordinator_for_device( hass, entry, api, device, coordinators_by_id, True @@ -188,6 +193,17 @@ async def make_device_data( devices_data.fans.append((device, coordinator)) devices_data.sensors.append((device, coordinator)) + if isinstance(device, Device) and device.device_type in [ + "Strip Light", + "Strip Light 3", + "Floor Lamp", + "Color Bulb", + ]: + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.lights.append((device, coordinator)) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SwitchBot via API from a config entry.""" diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py index b849194537a..dcca5119a74 100644 --- a/homeassistant/components/switchbot_cloud/const.py +++ b/homeassistant/components/switchbot_cloud/const.py @@ -15,3 +15,5 @@ VACUUM_FAN_SPEED_QUIET = "quiet" VACUUM_FAN_SPEED_STANDARD = "standard" VACUUM_FAN_SPEED_STRONG = "strong" VACUUM_FAN_SPEED_MAX = "max" + +AFTER_COMMAND_REFRESH = 5 diff --git a/homeassistant/components/switchbot_cloud/light.py b/homeassistant/components/switchbot_cloud/light.py new file mode 100644 index 00000000000..645c6b4c62b --- /dev/null +++ b/homeassistant/components/switchbot_cloud/light.py @@ -0,0 +1,153 @@ +"""Support for the Switchbot Light.""" + +import asyncio +from typing import Any + +from switchbot_api import ( + CommonCommands, + Device, + Remote, + RGBWLightCommands, + RGBWWLightCommands, + SwitchBotAPI, +) + +from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import SwitchbotCloudData, SwitchBotCoordinator +from .const import AFTER_COMMAND_REFRESH, DOMAIN +from .entity import SwitchBotCloudEntity + + +def value_map_brightness(value: int) -> int: + """Return value for brightness map.""" + return int(value / 255 * 100) + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up SwitchBot Cloud entry.""" + data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] + async_add_entities( + _async_make_entity(data.api, device, coordinator) + for device, coordinator in data.devices.lights + ) + + +class SwitchBotCloudLight(SwitchBotCloudEntity, LightEntity): + """Base Class for SwitchBot Light.""" + + _attr_is_on: bool | None = None + _attr_name: str | None = None + + _attr_color_mode = ColorMode.UNKNOWN + + def _set_attributes(self) -> None: + """Set attributes from coordinator data.""" + if self.coordinator.data is None: + return + + power: str | None = self.coordinator.data.get("power") + brightness: int | None = self.coordinator.data.get("brightness") + color: str | None = self.coordinator.data.get("color") + color_temperature: int | None = self.coordinator.data.get("colorTemperature") + self._attr_is_on = power == "on" if power else None + self._attr_brightness: int | None = brightness if brightness else None + self._attr_rgb_color: tuple | None = ( + (tuple(int(i) for i in color.split(":"))) if color else None + ) + self._attr_color_temp_kelvin: int | None = ( + color_temperature if color_temperature else None + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness: int | None = kwargs.get("brightness") + rgb_color: tuple[int, int, int] | None = kwargs.get("rgb_color") + color_temp_kelvin: int | None = kwargs.get("color_temp_kelvin") + if brightness is not None: + self._attr_color_mode = ColorMode.RGB + await self._send_brightness_command(brightness) + elif rgb_color is not None: + self._attr_color_mode = ColorMode.RGB + await self._send_rgb_color_command(rgb_color) + elif color_temp_kelvin is not None: + self._attr_color_mode = ColorMode.COLOR_TEMP + await self._send_color_temperature_command(color_temp_kelvin) + else: + self._attr_color_mode = ColorMode.RGB + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def _send_brightness_command(self, brightness: int) -> None: + """Send a brightness command.""" + await self.send_api_command( + RGBWLightCommands.SET_BRIGHTNESS, + parameters=str(value_map_brightness(brightness)), + ) + + async def _send_rgb_color_command(self, rgb_color: tuple) -> None: + """Send an RGB command.""" + await self.send_api_command( + RGBWLightCommands.SET_COLOR, + parameters=f"{rgb_color[2]}:{rgb_color[1]}:{rgb_color[0]}", + ) + + async def _send_color_temperature_command(self, color_temp_kelvin: int) -> None: + """Send a color temperature command.""" + await self.send_api_command( + RGBWWLightCommands.SET_COLOR_TEMPERATURE, + parameters=str(color_temp_kelvin), + ) + + +class SwitchBotCloudStripLight(SwitchBotCloudLight): + """Representation of a SwitchBot Strip Light.""" + + _attr_supported_color_modes = {ColorMode.RGB} + + +class SwitchBotCloudRGBWWLight(SwitchBotCloudLight): + """Representation of SwitchBot |Strip Light|Floor Lamp|Color Bulb.""" + + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2700 + + _attr_supported_color_modes = {ColorMode.RGB, ColorMode.COLOR_TEMP} + + async def _send_brightness_command(self, brightness: int) -> None: + """Send a brightness command.""" + await self.send_api_command( + RGBWWLightCommands.SET_BRIGHTNESS, + parameters=str(value_map_brightness(brightness)), + ) + + async def _send_rgb_color_command(self, rgb_color: tuple) -> None: + """Send an RGB command.""" + await self.send_api_command( + RGBWWLightCommands.SET_COLOR, + parameters=f"{rgb_color[0]}:{rgb_color[1]}:{rgb_color[2]}", + ) + + +@callback +def _async_make_entity( + api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator +) -> SwitchBotCloudStripLight | SwitchBotCloudRGBWWLight: + """Make a SwitchBotCloudLight.""" + if device.device_type == "Strip Light": + return SwitchBotCloudStripLight(api, device, coordinator) + return SwitchBotCloudRGBWWLight(api, device, coordinator) diff --git a/homeassistant/components/switchbot_cloud/vacuum.py b/homeassistant/components/switchbot_cloud/vacuum.py index 9a9ad49626f..7bc4c7d0ea2 100644 --- a/homeassistant/components/switchbot_cloud/vacuum.py +++ b/homeassistant/components/switchbot_cloud/vacuum.py @@ -2,7 +2,15 @@ from typing import Any -from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands +from switchbot_api import ( + Device, + Remote, + SwitchBotAPI, + VacuumCleanerV2Commands, + VacuumCleanerV3Commands, + VacuumCleanMode, + VacuumCommands, +) from homeassistant.components.vacuum import ( StateVacuumEntity, @@ -63,6 +71,11 @@ VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = { class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): """Representation of a SwitchBot vacuum.""" + # "K10+" + # "K10+ Pro" + # "Robot Vacuum Cleaner S1" + # "Robot Vacuum Cleaner S1 Plus" + _attr_supported_features: VacuumEntityFeature = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.FAN_SPEED @@ -85,23 +98,26 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): VacuumCommands.POW_LEVEL, parameters=VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed], ) - self.async_write_ha_state() + await self.coordinator.async_request_refresh() async def async_pause(self) -> None: """Pause the cleaning task.""" await self.send_api_command(VacuumCommands.STOP) + self.async_write_ha_state() async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" await self.send_api_command(VacuumCommands.DOCK) + await self.coordinator.async_request_refresh() async def async_start(self) -> None: """Start or resume the cleaning task.""" await self.send_api_command(VacuumCommands.START) + await self.coordinator.async_request_refresh() def _set_attributes(self) -> None: """Set attributes from coordinator data.""" - if not self.coordinator.data: + if self.coordinator.data is None: return self._attr_battery_level = self.coordinator.data.get("battery") @@ -109,11 +125,127 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity): switchbot_state = str(self.coordinator.data.get("workingStatus")) self._attr_activity = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state) + if self._attr_fan_speed is None: + self._attr_fan_speed = VACUUM_FAN_SPEED_QUIET + + +class SwitchBotCloudVacuumK20PlusPro(SwitchBotCloudVacuum): + """Representation of a SwitchBot K20+ Pro.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + await self.send_api_command( + VacuumCleanerV2Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + 1, + "waterLevel": 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + async def async_pause(self) -> None: + """Pause the cleaning task.""" + await self.send_api_command(VacuumCleanerV2Commands.PAUSE) + await self.coordinator.async_request_refresh() + + async def async_return_to_base(self, **kwargs: Any) -> None: + """Set the vacuum cleaner to return to the dock.""" + await self.send_api_command(VacuumCleanerV2Commands.DOCK) + await self.coordinator.async_request_refresh() + + async def async_start(self) -> None: + """Start or resume the cleaning task.""" + fan_level = ( + VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED.get(self.fan_speed) + if self.fan_speed + else None + ) + await self.send_api_command( + VacuumCleanerV2Commands.START_CLEAN, + parameters={ + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": int(fan_level if fan_level else VACUUM_FAN_SPEED_QUIET) + + 1, + "times": 1, + }, + }, + ) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudVacuumK10PlusProCombo(SwitchBotCloudVacuumK20PlusPro): + """Representation of a SwitchBot vacuum K10+ Pro Combo.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + if fan_speed in VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: + await self.send_api_command( + VacuumCleanerV2Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + + 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudVacuumV3(SwitchBotCloudVacuumK20PlusPro): + """Representation of a SwitchBot vacuum Robot Vacuum Cleaner S10 & S20.""" + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._attr_fan_speed = fan_speed + await self.send_api_command( + VacuumCleanerV3Commands.CHANGE_PARAM, + parameters={ + "fanLevel": int(VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED[fan_speed]) + 1, + "waterLevel": 1, + "times": 1, + }, + ) + await self.coordinator.async_request_refresh() + + async def async_start(self) -> None: + """Start or resume the cleaning task.""" + fan_level = ( + VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED.get(self.fan_speed) + if self.fan_speed + else None + ) + await self.send_api_command( + VacuumCleanerV3Commands.START_CLEAN, + parameters={ + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": int(fan_level if fan_level else VACUUM_FAN_SPEED_QUIET), + "waterLevel": 1, + "times": 1, + }, + }, + ) + await self.coordinator.async_request_refresh() @callback def _async_make_entity( api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator -) -> SwitchBotCloudVacuum: +) -> ( + SwitchBotCloudVacuum + | SwitchBotCloudVacuumK20PlusPro + | SwitchBotCloudVacuumV3 + | SwitchBotCloudVacuumK10PlusProCombo +): """Make a SwitchBotCloudVacuum.""" + if device.device_type in VacuumCleanerV2Commands.get_supported_devices(): + if device.device_type == "K20+ Pro": + return SwitchBotCloudVacuumK20PlusPro(api, device, coordinator) + return SwitchBotCloudVacuumK10PlusProCombo(api, device, coordinator) + + if device.device_type in VacuumCleanerV3Commands.get_supported_devices(): + return SwitchBotCloudVacuumV3(api, device, coordinator) return SwitchBotCloudVacuum(api, device, coordinator) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index df33845437f..0513d63b893 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -41,7 +41,6 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.DEVICE_TRACKER, - Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.WATER_HEATER, diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index 79486ff998b..09c6ec40208 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -73,8 +73,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): "weather": {}, "geofence": {}, "zone": {}, - "zone_control": {}, - "heating_circuits": {}, } @property @@ -101,14 +99,11 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): self.home_name = tado_home["name"] 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() - heating_circuits = await self._async_update_heating_circuits() self.data["device"] = devices self.data["zone"] = zones - self.data["zone_control"] = zone_controls - self.data["heating_circuits"] = heating_circuits self.data["weather"] = home["weather"] self.data["geofence"] = home["geofence"] @@ -171,7 +166,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): 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.""" try: @@ -184,12 +179,10 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): raise UpdateFailed(f"Error updating Tado zones: {err}") from err mapped_zones: dict[int, dict] = {} - mapped_zone_controls: dict[int, dict] = {} for zone in zone_states: 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]: """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) 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]: """Update the home data from Tado.""" @@ -242,23 +217,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): 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: """Fetch the capabilities from Tado.""" @@ -406,20 +364,6 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): except RequestException as 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 to manage the mobile devices from Tado via PyTado.""" diff --git a/homeassistant/components/tado/select.py b/homeassistant/components/tado/select.py deleted file mode 100644 index 6db765128c2..00000000000 --- a/homeassistant/components/tado/select.py +++ /dev/null @@ -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" - ) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index ba1c9e95683..5d9c4237be8 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -59,14 +59,6 @@ } } }, - "select": { - "heating_circuit": { - "name": "Heating circuit", - "state": { - "no_heating_circuit": "No circuit" - } - } - }, "switch": { "child_lock": { "name": "Child lock" diff --git a/homeassistant/components/telegram_bot/bot.py b/homeassistant/components/telegram_bot/bot.py index c57648c9551..3145badbed7 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -101,13 +101,26 @@ _LOGGER = logging.getLogger(__name__) type TelegramBotConfigEntry = ConfigEntry[TelegramNotificationService] +def _get_bot_info(bot: Bot, config_entry: ConfigEntry) -> dict[str, Any]: + return { + "config_entry_id": config_entry.entry_id, + "id": bot.id, + "first_name": bot.first_name, + "last_name": bot.last_name, + "username": bot.username, + } + + class BaseTelegramBot: """The base class for the telegram bot.""" - def __init__(self, hass: HomeAssistant, config: TelegramBotConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, config: TelegramBotConfigEntry, bot: Bot + ) -> None: """Initialize the bot base class.""" self.hass = hass self.config = config + self._bot = bot @abstractmethod async def shutdown(self) -> None: @@ -134,6 +147,8 @@ class BaseTelegramBot: _LOGGER.warning("Unhandled update: %s", update) return True + event_data["bot"] = _get_bot_info(self._bot, self.config) + event_context = Context() _LOGGER.debug("Firing event %s: %s", event_type, event_data) @@ -442,6 +457,9 @@ class TelegramNotificationService: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] + + event_data["bot"] = _get_bot_info(self.bot, self.config) + self.hass.bus.async_fire( EVENT_TELEGRAM_SENT, event_data, context=context ) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 6c38a0e53b8..b8640c5c005 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -54,7 +54,7 @@ class PollBot(BaseTelegramBot): self, hass: HomeAssistant, bot: Bot, config: TelegramBotConfigEntry ) -> None: """Create Application to poll for updates.""" - super().__init__(hass, config) + super().__init__(hass, config, bot) self.bot = bot self.application = ApplicationBuilder().bot(self.bot).build() self.application.add_handler(TypeHandler(Update, self.handle_update)) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index ce7ebea2b66..0ebe7988642 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -18,7 +18,8 @@ send_message: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -43,7 +44,8 @@ send_message: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text @@ -98,10 +100,12 @@ send_photo: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -126,7 +130,8 @@ send_photo: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -180,10 +185,12 @@ send_sticker: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -199,7 +206,8 @@ send_sticker: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -253,10 +261,12 @@ send_animation: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -281,7 +291,8 @@ send_animation: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -335,10 +346,12 @@ send_video: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -363,7 +376,8 @@ send_video: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -417,10 +431,12 @@ send_voice: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -436,7 +452,8 @@ send_voice: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -490,10 +507,12 @@ send_document: example: myuser_pwd selector: text: + type: password target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true parse_mode: selector: select: @@ -518,7 +537,8 @@ send_document: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -563,7 +583,8 @@ send_location: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true disable_notification: selector: boolean: @@ -576,7 +597,8 @@ send_location: keyboard: example: '["/command1, /command2", "/command3"]' selector: - object: + text: + multiple: true inline_keyboard: example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], @@ -605,7 +627,8 @@ send_poll: target: example: "[12345, 67890] or 12345" selector: - object: + text: + multiple: true question: required: true selector: diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 29c3305858b..61843e6ffbf 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -77,7 +77,7 @@ class PushBot(BaseTelegramBot): # Dumb Application that just gets our updates to our handler callback (self.handle_update) self.application = ApplicationBuilder().bot(bot).updater(None).build() self.application.add_handler(TypeHandler(Update, self.handle_update)) - super().__init__(hass, config) + super().__init__(hass, config, bot) self.base_url = config.data.get(CONF_URL) or get_url( hass, require_ssl=True, allow_internal=False diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 7e06ef51a4b..2e581628da2 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_STATE_CLASSES, @@ -62,6 +63,33 @@ from .const import ( CONF_TURN_ON, DOMAIN, ) +from .cover import ( + CLOSE_ACTION, + CONF_OPEN_AND_CLOSE, + CONF_POSITION, + OPEN_ACTION, + POSITION_ACTION, + STOP_ACTION, + async_create_preview_cover, +) +from .fan import ( + CONF_OFF_ACTION, + CONF_ON_ACTION, + CONF_PERCENTAGE, + CONF_SET_PERCENTAGE_ACTION, + CONF_SPEED_COUNT, + async_create_preview_fan, +) +from .light import ( + CONF_HS, + CONF_HS_ACTION, + CONF_LEVEL, + CONF_LEVEL_ACTION, + CONF_TEMPERATURE, + CONF_TEMPERATURE_ACTION, + async_create_preview_light, +) +from .lock import CONF_LOCK, CONF_OPEN, CONF_UNLOCK, async_create_preview_lock from .number import ( CONF_MAX, CONF_MIN, @@ -76,6 +104,18 @@ from .select import CONF_OPTIONS, CONF_SELECT_OPTION, async_create_preview_selec from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch 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] = { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -143,12 +183,65 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: ) } + if domain == Platform.COVER: + schema |= _SCHEMA_STATE | { + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): selector.ActionSelector(), + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): selector.ActionSelector(), + vol.Optional(STOP_ACTION): selector.ActionSelector(), + vol.Optional(CONF_POSITION): selector.TemplateSelector(), + vol.Optional(POSITION_ACTION): selector.ActionSelector(), + } + if flow_type == "config": + schema |= { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[cls.value for cls in CoverDeviceClass], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="cover_device_class", + sort=True, + ), + ) + } + + if domain == Platform.FAN: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_ON_ACTION): selector.ActionSelector(), + vol.Required(CONF_OFF_ACTION): selector.ActionSelector(), + vol.Optional(CONF_PERCENTAGE): selector.TemplateSelector(), + vol.Optional(CONF_SET_PERCENTAGE_ACTION): selector.ActionSelector(), + vol.Optional(CONF_SPEED_COUNT): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=100, step=1, mode=selector.NumberSelectorMode.BOX + ), + ), + } + if domain == Platform.IMAGE: schema |= { vol.Required(CONF_URL): selector.TemplateSelector(), vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(), } + if domain == Platform.LIGHT: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_TURN_ON): selector.ActionSelector(), + vol.Required(CONF_TURN_OFF): selector.ActionSelector(), + vol.Optional(CONF_LEVEL): selector.TemplateSelector(), + vol.Optional(CONF_LEVEL_ACTION): selector.ActionSelector(), + vol.Optional(CONF_HS): selector.TemplateSelector(), + vol.Optional(CONF_HS_ACTION): selector.ActionSelector(), + vol.Optional(CONF_TEMPERATURE): selector.TemplateSelector(), + 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: schema |= { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -222,6 +315,26 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: 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 |= { vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), vol.Optional(CONF_ADVANCED_OPTIONS): section( @@ -327,11 +440,16 @@ TEMPLATE_TYPES = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, + Platform.FAN, Platform.IMAGE, + Platform.LIGHT, + Platform.LOCK, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.VACUUM, ] CONFIG_FLOW = { @@ -350,11 +468,31 @@ CONFIG_FLOW = { config_schema(Platform.BUTTON), validate_user_input=validate_user_input(Platform.BUTTON), ), + Platform.COVER: SchemaFlowFormStep( + config_schema(Platform.COVER), + preview="template", + validate_user_input=validate_user_input(Platform.COVER), + ), + Platform.FAN: SchemaFlowFormStep( + config_schema(Platform.FAN), + preview="template", + validate_user_input=validate_user_input(Platform.FAN), + ), Platform.IMAGE: SchemaFlowFormStep( config_schema(Platform.IMAGE), preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + config_schema(Platform.LIGHT), + preview="template", + 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( config_schema(Platform.NUMBER), preview="template", @@ -375,6 +513,11 @@ CONFIG_FLOW = { preview="template", 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), + ), } @@ -394,11 +537,31 @@ OPTIONS_FLOW = { options_schema(Platform.BUTTON), validate_user_input=validate_user_input(Platform.BUTTON), ), + Platform.COVER: SchemaFlowFormStep( + options_schema(Platform.COVER), + preview="template", + validate_user_input=validate_user_input(Platform.COVER), + ), + Platform.FAN: SchemaFlowFormStep( + options_schema(Platform.FAN), + preview="template", + validate_user_input=validate_user_input(Platform.FAN), + ), Platform.IMAGE: SchemaFlowFormStep( options_schema(Platform.IMAGE), preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + options_schema(Platform.LIGHT), + preview="template", + 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( options_schema(Platform.NUMBER), preview="template", @@ -419,6 +582,11 @@ OPTIONS_FLOW = { preview="template", 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[ @@ -427,10 +595,15 @@ CREATE_PREVIEW_ENTITY: dict[ ] = { Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, + Platform.COVER: async_create_preview_cover, + Platform.FAN: async_create_preview_fan, + Platform.LIGHT: async_create_preview_light, + Platform.LOCK: async_create_preview_lock, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, Platform.SWITCH: async_create_preview_switch, + Platform.VACUUM: async_create_preview_vacuum, } diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e8739fa8207..44981fcb08f 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_COVERS, CONF_DEVICE_CLASS, @@ -31,14 +32,22 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError 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 . import TriggerUpdateCoordinator from .const import DOMAIN 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 ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, @@ -91,23 +100,29 @@ LEGACY_FIELDS = { DEFAULT_NAME = "Template Cover" +COVER_COMMON_SCHEMA = vol.Schema( + { + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_POSITION): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_TILT): cv.template, + vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, + } +) + COVER_YAML_SCHEMA = vol.All( vol.Schema( { - vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, - vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_POSITION): cv.template, - vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_TILT): cv.template, - vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, } ) - .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) - .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA), + .extend(COVER_COMMON_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA) + .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) @@ -139,6 +154,11 @@ PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_LEGACY_YAML_SCHEMA)} ) +COVER_CONFIG_ENTRY_SCHEMA = vol.All( + COVER_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema), + cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), +) + async def async_setup_platform( hass: HomeAssistant, @@ -160,11 +180,43 @@ 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, + StateCoverEntity, + COVER_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_cover( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateCoverEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateCoverEntity, + COVER_CONFIG_ENTRY_SCHEMA, + True, + ) + + class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): """Representation of a template cover features.""" _entity_id_format = ENTITY_ID_FORMAT _optimistic_entity = True + _extra_optimistic_options = (CONF_POSITION,) # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. # This ensures that the __init__ on AbstractTemplateEntity is not called twice. diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index e9a630594d7..03a93f50ec3 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -20,6 +20,7 @@ class AbstractTemplateEntity(Entity): _entity_id_format: str _optimistic_entity: bool = False + _extra_optimistic_options: tuple[str, ...] | None = None _template: Template | None = None def __init__( @@ -35,9 +36,14 @@ class AbstractTemplateEntity(Entity): if self._optimistic_entity: self._template = config.get(CONF_STATE) - self._attr_assumed_state = self._template is None or config.get( - CONF_OPTIMISTIC, False - ) + optimistic = self._template is None + if self._extra_optimistic_options: + optimistic = optimistic and all( + config.get(option) is None + for option in self._extra_optimistic_options + ) + + self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 381d58a8a9c..9504ba45ab9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -20,6 +20,7 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, @@ -34,15 +35,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError 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 .const import DOMAIN from .coordinator import TriggerUpdateCoordinator 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 ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, @@ -132,6 +141,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)} ) +FAN_CONFIG_ENTRY_SCHEMA = FAN_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -153,6 +166,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, + StateFanEntity, + FAN_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_fan( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateFanEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateFanEntity, + FAN_CONFIG_ENTRY_SCHEMA, + ) + + class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): """Representation of a template fan features.""" diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index 25f7011c794..a26b7bb0df1 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -242,7 +242,7 @@ async def async_setup_template_entry( config_entry: ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, state_entity_cls: type[TemplateEntity], - config_schema: vol.Schema, + config_schema: vol.Schema | vol.All, replace_value_template: bool = False, ) -> None: """Setup the Template from a config entry.""" @@ -267,7 +267,7 @@ def async_setup_template_preview[T: TemplateEntity]( name: str, config: ConfigType, state_entity_cls: type[T], - schema: vol.Schema, + schema: vol.Schema | vol.All, replace_value_template: bool = False, ) -> T: """Setup the Template preview.""" diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 19eecaa7006..538d3f3aaaf 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -27,6 +27,7 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EFFECT, CONF_ENTITY_ID, @@ -43,15 +44,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError 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.util import color as color_util from . import TriggerUpdateCoordinator from .const import DOMAIN 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 ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, @@ -135,6 +144,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema( vol.Optional(CONF_MIN_MIREDS): cv.template, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB): cv.template, vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA, @@ -195,6 +206,10 @@ PLATFORM_SCHEMA = vol.All( ), ) +LIGHT_CONFIG_ENTRY_SCHEMA = LIGHT_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -216,6 +231,37 @@ 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, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_light( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateLightEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): """Representation of a template lights features.""" diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index e89f95734d1..04d26521ef1 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -15,6 +15,7 @@ from homeassistant.components.lock import ( LockEntityFeature, LockState, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, CONF_NAME, @@ -26,15 +27,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError 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 .const import DOMAIN from .coordinator import TriggerUpdateCoordinator 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 ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, @@ -82,6 +91,10 @@ PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( } ).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( 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): """Representation of a template lock features.""" diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 31a6338f594..362a7e9d5c5 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -17,14 +17,9 @@ from homeassistant.components.number import ( NumberEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - CONF_OPTIMISTIC, - CONF_STATE, - CONF_UNIT_OF_MEASUREMENT, -) +from homeassistant.const import CONF_NAME, CONF_STATE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, @@ -33,6 +28,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN +from .entity import AbstractTemplateEntity from .helpers import ( async_setup_template_entry, async_setup_template_platform, @@ -40,6 +36,7 @@ from .helpers import ( ) from .template_entity import ( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, ) @@ -57,21 +54,15 @@ NUMBER_COMMON_SCHEMA = vol.Schema( vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template, vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template, vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, - vol.Required(CONF_STATE): cv.template, - vol.Required(CONF_STEP): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } -) +).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) -NUMBER_YAML_SCHEMA = ( - vol.Schema( - { - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - } - ) - .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) - .extend(NUMBER_COMMON_SCHEMA.schema) -) +NUMBER_YAML_SCHEMA = NUMBER_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA +).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) NUMBER_CONFIG_ENTRY_SCHEMA = NUMBER_COMMON_SCHEMA.extend( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema @@ -121,69 +112,28 @@ def async_create_preview_number( ) -class StateNumberEntity(TemplateEntity, NumberEntity): - """Representation of a template number.""" +class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity): + """Representation of a template number features.""" - _attr_should_poll = False _entity_id_format = ENTITY_ID_FORMAT + _optimistic_entity = True - def __init__( - self, - hass: HomeAssistant, - config, - unique_id: str | None, - ) -> None: - """Initialize the number.""" - TemplateEntity.__init__(self, hass, config, unique_id) - if TYPE_CHECKING: - assert self._attr_name is not None - - self._value_template = config[CONF_STATE] - self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], self._attr_name, DOMAIN) - + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" self._step_template = config[CONF_STEP] - self._min_value_template = config[CONF_MIN] - self._max_value_template = config[CONF_MAX] - self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC) + self._min_template = config[CONF_MIN] + self._max_template = config[CONF_MAX] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_step = DEFAULT_STEP self._attr_native_min_value = DEFAULT_MIN_VALUE self._attr_native_max_value = DEFAULT_MAX_VALUE - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - self.add_template_attribute( - "_attr_native_value", - self._value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - self.add_template_attribute( - "_attr_native_step", - self._step_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - if self._min_value_template is not None: - self.add_template_attribute( - "_attr_native_min_value", - self._min_value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - if self._max_value_template is not None: - self.add_template_attribute( - "_attr_native_max_value", - self._max_value_template, - validator=vol.Coerce(float), - none_on_template_error=True, - ) - super()._async_setup_templates() - async def async_set_native_value(self, value: float) -> None: """Set value of the number.""" - if self._optimistic: + if self._attr_assumed_state: self._attr_native_value = value self.async_write_ha_state() if set_value := self._action_scripts.get(CONF_SET_VALUE): @@ -194,17 +144,65 @@ class StateNumberEntity(TemplateEntity, NumberEntity): ) -class TriggerNumberEntity(TriggerEntity, NumberEntity): +class StateNumberEntity(TemplateEntity, AbstractTemplateNumber): + """Representation of a template number.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: + """Initialize the number.""" + TemplateEntity.__init__(self, hass, config, unique_id) + AbstractTemplateNumber.__init__(self, config) + + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], name, DOMAIN) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template is not None: + self.add_template_attribute( + "_attr_native_value", + self._template, + vol.Coerce(float), + none_on_template_error=True, + ) + if self._step_template is not None: + self.add_template_attribute( + "_attr_native_step", + self._step_template, + vol.Coerce(float), + none_on_template_error=True, + ) + if self._min_template is not None: + self.add_template_attribute( + "_attr_native_min_value", + self._min_template, + validator=vol.Coerce(float), + none_on_template_error=True, + ) + if self._max_template is not None: + self.add_template_attribute( + "_attr_native_max_value", + self._max_template, + validator=vol.Coerce(float), + none_on_template_error=True, + ) + super()._async_setup_templates() + + +class TriggerNumberEntity(TriggerEntity, AbstractTemplateNumber): """Number entity based on trigger data.""" - _entity_id_format = ENTITY_ID_FORMAT domain = NUMBER_DOMAIN - extra_template_keys = ( - CONF_STATE, - CONF_STEP, - CONF_MIN, - CONF_MAX, - ) def __init__( self, @@ -213,47 +211,49 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): config: dict, ) -> None: """Initialize the entity.""" - super().__init__(hass, coordinator, config) + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateNumber.__init__(self, config) - name = self._rendered.get(CONF_NAME, DEFAULT_NAME) - self.add_script(CONF_SET_VALUE, config[CONF_SET_VALUE], name, DOMAIN) + for key in ( + CONF_STATE, + CONF_STEP, + CONF_MIN, + CONF_MAX, + ): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) - self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> float | None: - """Return the currently selected option.""" - return vol.Any(vol.Coerce(float), None)(self._rendered.get(CONF_STATE)) - - @property - def native_min_value(self) -> int: - """Return the minimum value.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_MIN, super().native_min_value) + self.add_script( + CONF_SET_VALUE, + config[CONF_SET_VALUE], + self._rendered.get(CONF_NAME, DEFAULT_NAME), + DOMAIN, ) - @property - def native_max_value(self) -> int: - """Return the maximum value.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_MAX, super().native_max_value) - ) + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" + self._process_data() - @property - def native_step(self) -> int: - """Return the increment/decrement step.""" - return vol.Any(vol.Coerce(float), None)( - self._rendered.get(CONF_STEP, super().native_step) - ) - - async def async_set_native_value(self, value: float) -> None: - """Set value of the number.""" - if self._config[CONF_OPTIMISTIC]: - self._attr_native_value = value + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, attr in ( + (CONF_STATE, "_attr_native_value"), + (CONF_STEP, "_attr_native_step"), + (CONF_MIN, "_attr_native_min_value"), + (CONF_MAX, "_attr_native_max_value"), + ): + if (rendered := self._rendered.get(key)) is not None: + setattr(self, attr, vol.Any(vol.Coerce(float), None)(rendered)) + write_ha_state = True + + if len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() - if set_value := self._action_scripts.get(CONF_SET_VALUE): - await self.async_run_script( - set_value, - run_variables={ATTR_VALUE: value}, - context=self._context, - ) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index be91b27e485..be5fb1866ea 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -2,6 +2,7 @@ "common": { "advanced_options": "Advanced options", "availability": "Availability template", + "availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.", "code_format": "Code format", "device_class": "Device class", "device_id_description": "Select a device to link to this entity.", @@ -28,13 +29,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.", + "disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.", + "arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.", + "arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.", + "arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.", + "arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.", + "arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.", + "trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.", + "code_arm_required": "If true, the code is required to arm the alarm.", + "code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -48,13 +62,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -68,18 +86,89 @@ "press": "Actions on press" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "Defines actions to run when button is pressed." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "title": "Template button" }, + "cover": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "open_cover": "Actions on open", + "close_cover": "Actions on close", + "stop_cover": "Actions on stop", + "position": "Position", + "set_cover_position": "Actions on set position" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the cover. Valid output values from the template are `open`, `opening`, `closing` and `closed` which are directly mapped to the corresponding states. If both a state and a position are specified, only `opening` and `closing` are set from the state template.", + "open_cover": "Defines actions to run when the cover is opened.", + "close_cover": "Defines actions to run when the cover is closed.", + "stop_cover": "Defines actions to run when the cover is stopped.", + "position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).", + "set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template cover" + }, + "fan": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "percentage": "Percentage", + "set_percentage": "Actions on set percentage", + "speed_count": "Speed count" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", + "turn_off": "Defines actions to run when the fan is turned off.", + "turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.", + "percentage": "Defines a template to get the speed percentage of the fan.", + "set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.", + "speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template fan" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -88,18 +177,93 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "Defines a template to get the URL on which the image is served.", + "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "title": "Template image" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "Brightness level", + "set_level": "Actions on set level", + "hs": "HS color", + "set_hs": "Actions on set HS color", + "temperature": "Color temperature", + "set_temperature": "Actions on set color temperature" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.", + "turn_off": "Defines actions to run when the light is turned off.", + "turn_on": "Defines actions to run when the light is turned on.", + "level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.", + "set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.", + "hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).", + "set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.", + "temperature": "Defines a template to get the color temperature of the light.", + "set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "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%]", + "state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.", + "lock": "Defines actions to run when the lock is locked.", + "unlock": "Defines actions to run when the lock is unlocked.", + "code_format": "Defines a template to get the `code_format` attribute of the lock.", + "open": "Defines actions to run when the lock is opened." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template lock" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -112,13 +276,22 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the number's current value.", + "step": "Defines the number's increment/decrement step.", + "set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.", + "max": "Defines the number's maximum value.", + "min": "Defines the number's minimum value.", + "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -133,13 +306,19 @@ "options": "Available options" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Template for the select’s current value.", + "select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.", + "options": "Template for the select’s available options." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -155,13 +334,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.", + "unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -173,11 +357,16 @@ "alarm_control_panel": "Template an alarm control panel", "binary_sensor": "Template a binary sensor", "button": "Template a button", + "cover": "Template a cover", + "fan": "Template a fan", "image": "Template an image", + "light": "Template a light", + "lock": "Template a lock", "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", - "switch": "Template a switch" + "switch": "Template a switch", + "vacuum": "Template a vacuum" }, "title": "Template helper" }, @@ -191,17 +380,63 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful." + "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.", + "turn_off": "Defines actions to run when the switch is turned off.", + "turn_on": "Defines actions to run when the switch is turned on." }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "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 start", + "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 dock", + "clean_spot": "Actions on clean spot", + "locate": "Actions on locate" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.", + "start": "Defines actions to run when the vacuum is started.", + "fan_speed": "Defines a template to get the fan speed of the vacuum.", + "fan_speeds": "List of fan speeds supported by the vacuum.", + "set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.", + "stop": "Defines actions to run when the vacuum is stopped.", + "pause": "Defines actions to run when the vacuum is paused.", + "return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.", + "clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.", + "locate": "Defines actions to run when the vacuum is given a 'Locate' command." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template vacuum" } } }, @@ -222,13 +457,26 @@ "code_format": "[%key:component::template::common::code_format%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]", + "disarm": "[%key:component::template::config::step::alarm_control_panel::data_description::disarm%]", + "arm_away": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_away%]", + "arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_custom_bypass%]", + "arm_home": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_home%]", + "arm_night": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_night%]", + "arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_vacation%]", + "trigger": "[%key:component::template::config::step::alarm_control_panel::data_description::trigger%]", + "code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data_description::code_arm_required%]", + "code_format": "[%key:component::template::config::step::alarm_control_panel::data_description::code_format%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -240,13 +488,17 @@ "state": "[%key:component::template::common::state%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::binary_sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -258,18 +510,87 @@ "press": "[%key:component::template::config::step::button::data::press%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "press": "[%key:component::template::config::step::button::data_description::press%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "title": "[%key:component::template::config::step::button::title%]" }, + + "cover": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "state": "[%key:component::template::common::state%]", + "open_cover": "[%key:component::template::config::step::cover::data::open_cover%]", + "close_cover": "[%key:component::template::config::step::cover::data::close_cover%]", + "stop_cover": "[%key:component::template::config::step::cover::data::stop_cover%]", + "position": "[%key:component::template::config::step::cover::data::position%]", + "set_cover_position": "[%key:component::template::config::step::cover::data::set_cover_position%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::cover::data_description::state%]", + "open_cover": "[%key:component::template::config::step::cover::data_description::open_cover%]", + "close_cover": "[%key:component::template::config::step::cover::data_description::close_cover%]", + "stop_cover": "[%key:component::template::config::step::cover::data_description::stop_cover%]", + "position": "[%key:component::template::config::step::cover::data_description::position%]", + "set_cover_position": "[%key:component::template::config::step::cover::data_description::set_cover_position%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "[%key:component::template::config::step::cover::title%]" + }, + "fan": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "percentage": "[%key:component::template::config::step::fan::data::percentage%]", + "set_percentage": "[%key:component::template::config::step::fan::data::set_percentage%]", + "speed_count": "[%key:component::template::config::step::fan::data::speed_count%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::fan::data_description::state%]", + "turn_off": "[%key:component::template::config::step::fan::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::fan::data_description::turn_on%]", + "percentage": "[%key:component::template::config::step::fan::data_description::percentage%]", + "set_percentage": "[%key:component::template::config::step::fan::data_description::set_percentage%]", + "speed_count": "[%key:component::template::config::step::fan::data_description::speed_count%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "[%key:component::template::config::step::fan::title%]" + }, "image": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -277,18 +598,92 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "url": "[%key:component::template::config::step::image::data_description::url%]", + "verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "title": "[%key:component::template::config::step::image::title%]" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "[%key:component::template::config::step::light::data::level%]", + "set_level": "[%key:component::template::config::step::light::data::set_level%]", + "hs": "[%key:component::template::config::step::light::data::hs%]", + "set_hs": "[%key:component::template::config::step::light::data::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::light::data_description::state%]", + "turn_off": "[%key:component::template::config::step::light::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]", + "level": "[%key:component::template::config::step::light::data_description::level%]", + "set_level": "[%key:component::template::config::step::light::data_description::set_level%]", + "hs": "[%key:component::template::config::step::light::data_description::hs%]", + "set_hs": "[%key:component::template::config::step::light::data_description::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data_description::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data_description::set_temperature%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "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%]", + "state": "[%key:component::template::config::step::lock::data_description::state%]", + "lock": "[%key:component::template::config::step::lock::data_description::lock%]", + "unlock": "[%key:component::template::config::step::lock::data_description::unlock%]", + "code_format": "[%key:component::template::config::step::lock::data_description::code_format%]", + "open": "[%key:component::template::config::step::lock::data_description::open%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "[%key:component::template::config::step::lock::title%]" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -300,13 +695,21 @@ "min": "[%key:component::template::config::step::number::data::min%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::number::data_description::state%]", + "step": "[%key:component::template::config::step::number::data_description::step%]", + "set_value": "[%key:component::template::config::step::number::data_description::set_value%]", + "max": "[%key:component::template::config::step::number::data_description::max%]", + "min": "[%key:component::template::config::step::number::data_description::min%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -321,13 +724,19 @@ "options": "[%key:component::template::config::step::select::data::options%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::select::data_description::state%]", + "select_option": "[%key:component::template::config::step::select::data_description::select_option%]", + "options": "[%key:component::template::config::step::select::data_description::options%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -342,13 +751,18 @@ "unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]" }, "data_description": { - "device_id": "[%key:component::template::common::device_id_description%]" + "device_id": "[%key:component::template::common::device_id_description%]", + "state": "[%key:component::template::config::step::sensor::data_description::state%]", + "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, @@ -364,17 +778,63 @@ }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]", - "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]" + "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]", + "turn_off": "[%key:component::template::config::step::switch::data_description::turn_off%]", + "turn_on": "[%key:component::template::config::step::switch::data_description::turn_on%]" }, "sections": { "advanced_options": { "name": "[%key:component::template::common::advanced_options%]", "data": { "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" } } }, "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%]", + "state": "[%key:component::template::config::step::vacuum::data_description::state%]", + "start": "[%key:component::template::config::step::vacuum::data_description::start%]", + "fan_speed": "[%key:component::template::config::step::vacuum::data_description::fan_speed%]", + "fan_speeds": "[%key:component::template::config::step::vacuum::data_description::fan_speeds%]", + "set_fan_speed": "[%key:component::template::config::step::vacuum::data_description::set_fan_speed%]", + "stop": "[%key:component::template::config::step::vacuum::data_description::stop%]", + "pause": "[%key:component::template::config::step::vacuum::data_description::pause%]", + "return_to_base": "[%key:component::template::config::step::vacuum::data_description::return_to_base%]", + "clean_spot": "[%key:component::template::config::step::vacuum::data_description::clean_spot%]", + "locate": "[%key:component::template::config::step::vacuum::data_description::locate%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "[%key:component::template::config::step::vacuum::title%]" } } }, @@ -425,8 +885,23 @@ "update": "[%key:component::button::entity_component::update::name%]" } }, + "cover_device_class": { + "options": { + "awning": "[%key:component::cover::entity_component::awning::name%]", + "blind": "[%key:component::cover::entity_component::blind::name%]", + "curtain": "[%key:component::cover::entity_component::curtain::name%]", + "damper": "[%key:component::cover::entity_component::damper::name%]", + "door": "[%key:component::cover::entity_component::door::name%]", + "garage": "[%key:component::cover::entity_component::garage::name%]", + "gate": "[%key:component::cover::entity_component::gate::name%]", + "shade": "[%key:component::cover::entity_component::shade::name%]", + "shutter": "[%key:component::cover::entity_component::shutter::name%]", + "window": "[%key:component::cover::entity_component::window::name%]" + } + }, "sensor_device_class": { "options": { + "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "area": "[%key:component::sensor::entity_component::area::name%]", @@ -474,7 +949,7 @@ "temperature": "[%key:component::sensor::entity_component::temperature::name%]", "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", - "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 67f0f780388..1abfdbd00da 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -22,6 +22,7 @@ from homeassistant.components.vacuum import ( VacuumActivity, VacuumEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, @@ -34,16 +35,24 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError 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 .const import DOMAIN from .coordinator import TriggerUpdateCoordinator 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 ( TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, 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)} ) +VACUUM_CONFIG_ENTRY_SCHEMA = VACUUM_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( 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): """Representation of a template vacuum features.""" diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 7f79adc2201..bddb55197c3 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -34,6 +34,7 @@ from homeassistant.components.weather import ( from homeassistant.const import ( CONF_NAME, CONF_TEMPERATURE_UNIT, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -151,6 +152,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index 20d2d70b5dc..e3a31a2c0dc 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -247,11 +247,15 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any raise UpdateFailed(e.message) from e self.updated_once = True + if not data or not isinstance(data.get("time_series"), list): + raise UpdateFailed("Received invalid data") + # Add all time periods together output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: - output[key] += period.get(key, 0) + if key in period: + output[key] += period[key] return output diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index 51eed97227e..6d12aa56470 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -28,7 +28,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] scopes: list[Scope] - stream: TeslemetryStream + stream: TeslemetryStream | None @dataclass diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 1ffe073cc5c..34ee2d4b8e9 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -45,7 +45,7 @@ from .entity import ( TeslemetryVehicleStreamEntity, TeslemetryWallConnectorEntity, ) -from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData +from .models import TeslemetryEnergyData, TeslemetryVehicleData PARALLEL_UPDATES = 0 @@ -1617,11 +1617,12 @@ async def async_setup_entry( if energysite.history_coordinator is not None ) - entities.append( - TeslemetryCreditBalanceSensor( - entry.unique_id or entry.entry_id, entry.runtime_data + if entry.runtime_data.stream is not None: + entities.append( + TeslemetryCreditBalanceSensor( + entry.unique_id or entry.entry_id, entry.runtime_data.stream + ) ) - ) async_add_entities(entities) @@ -1840,12 +1841,12 @@ class TeslemetryCreditBalanceSensor(RestoreSensor): _attr_state_class = SensorStateClass.MEASUREMENT _attr_suggested_display_precision = 0 - def __init__(self, uid: str, data: TeslemetryData) -> None: + def __init__(self, uid: str, stream: TeslemetryStream) -> None: """Initialize common aspects of a Teslemetry entity.""" self._attr_translation_key = "credit_balance" self._attr_unique_id = f"{uid}_credit_balance" - self.stream = data.stream + self.stream = stream async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index d4e47c31dd2..4bd4c6e81f7 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) KNOWN_BRANDS: dict[str | None, str] = { "Amazon": "amazon", + "Apple": "apple", "Apple Inc.": "apple", "Aqara": "aqara_gateway", "eero": "eero", diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 6c3aa146158..106075e9314 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -153,17 +153,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool # Register known device IDs device_registry = dr.async_get(hass) for device in manager.device_map.values(): - if not device.status and not device.status_range and not device.function: - # If the device has no status, status_range or function, - # it cannot be supported - LOGGER.info( - "Device %s (%s) has been ignored as it does not provide any" - " standard instructions (status, status_range and function are" - " all empty) - see %s", - device.product_name, - device.id, - "https://github.com/tuya/tuya-device-sharing-sdk/issues/11", - ) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.id)}, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index cb7555c38d8..7b73e825900 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ColorMode, LightEntity, LightEntityDescription, + color_supported, filter_supported_color_modes, ) from homeassistant.const import EntityCategory @@ -530,19 +531,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity): description.brightness_min, dptype=DPType.INTEGER ) - if int_type := self.find_dpcode( - description.color_temp, dptype=DPType.INTEGER, prefer_function=True - ): - self._color_temp = int_type - color_modes.add(ColorMode.COLOR_TEMP) - # If entity does not have color_temp, check if it has work_mode "white" - elif color_mode_enum := self.find_dpcode( - description.color_mode, dptype=DPType.ENUM, prefer_function=True - ): - if WorkMode.WHITE.value in color_mode_enum.range: - color_modes.add(ColorMode.WHITE) - self._white_color_mode = ColorMode.WHITE - if ( dpcode := self.find_dpcode(description.color_data, prefer_function=True) ) and self.get_dptype(dpcode) == DPType.JSON: @@ -568,6 +556,26 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ): self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 + # Check if the light has color temperature + if int_type := self.find_dpcode( + description.color_temp, dptype=DPType.INTEGER, prefer_function=True + ): + self._color_temp = int_type + color_modes.add(ColorMode.COLOR_TEMP) + # If light has color but does not have color_temp, check if it has + # work_mode "white" + elif ( + color_supported(color_modes) + and ( + color_mode_enum := self.find_dpcode( + description.color_mode, dptype=DPType.ENUM, prefer_function=True + ) + ) + and WorkMode.WHITE.value in color_mode_enum.range + ): + color_modes.add(ColorMode.WHITE) + self._white_color_mode = ColorMode.WHITE + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index b4afca83a85..43e4c04c518 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -99,8 +99,22 @@ class EnumTypeData: return cls(dpcode, **parsed) +class ComplexTypeData: + """Complex Type Data (for JSON/RAW parsing).""" + + @classmethod + def from_json(cls, data: str) -> Self: + """Load JSON string and return a ComplexTypeData object.""" + raise NotImplementedError("from_json is not implemented for this type") + + @classmethod + def from_raw(cls, data: str) -> Self: + """Decode base64 string and return a ComplexTypeData object.""" + raise NotImplementedError("from_raw is not implemented for this type") + + @dataclass -class ElectricityTypeData: +class ElectricityTypeData(ComplexTypeData): """Electricity Type Data.""" electriccurrent: str | None = None diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 6e8da29ef53..da7a57b1be2 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -40,13 +40,14 @@ from .const import ( UnitOfMeasurement, ) from .entity import TuyaEntity -from .models import ElectricityTypeData, EnumTypeData, IntegerTypeData +from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData @dataclass(frozen=True) class TuyaSensorEntityDescription(SensorEntityDescription): """Describes Tuya sensor entity.""" + complex_type: type[ComplexTypeData] | None = None subkey: str | None = None @@ -368,6 +369,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -376,6 +378,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -384,6 +387,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -392,6 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -400,6 +405,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -408,6 +414,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -416,6 +423,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -424,6 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -432,6 +441,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1247,6 +1257,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { translation_key="total_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1255,6 +1266,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1263,6 +1275,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1271,6 +1284,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1279,6 +1293,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1287,6 +1302,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1295,6 +1311,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1303,6 +1320,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, + complex_type=ElectricityTypeData, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1311,6 +1329,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, + complex_type=ElectricityTypeData, subkey="power", ), TuyaSensorEntityDescription( @@ -1319,6 +1338,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + complex_type=ElectricityTypeData, subkey="voltage", ), ), @@ -1417,7 +1437,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): _status_range: DeviceStatusRange | None = None _type: DPType | None = None - _type_data: IntegerTypeData | EnumTypeData | None = None + _type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None _uom: UnitOfMeasurement | None = None def __init__( @@ -1516,15 +1536,21 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # Get subkey value from Json string. if self._type is DPType.JSON: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_json(value) + values = self.entity_description.complex_type.from_json(value) return getattr(values, self.entity_description.subkey) if self._type is DPType.RAW: - if self.entity_description.subkey is None: + if ( + self.entity_description.complex_type is None + or self.entity_description.subkey is None + ): return None - values = ElectricityTypeData.from_raw(value) + values = self.entity_description.complex_type.from_raw(value) return getattr(values, self.entity_description.subkey) # Valid string or enum value diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index fd3a680ed3c..97d623d7c21 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -744,86 +744,26 @@ "switch": { "name": "Switch" }, + "indexed_switch": { + "name": "Switch {index}" + }, "socket": { "name": "Socket" }, + "indexed_socket": { + "name": "Socket {index}" + }, "radio": { "name": "Radio" }, - "alarm_1": { - "name": "Alarm 1" - }, - "alarm_2": { - "name": "Alarm 2" - }, - "alarm_3": { - "name": "Alarm 3" - }, - "alarm_4": { - "name": "Alarm 4" + "indexed_alarm": { + "name": "Alarm {index}" }, "sleep_aid": { "name": "Sleep aid" }, - "switch_1": { - "name": "Switch 1" - }, - "switch_2": { - "name": "Switch 2" - }, - "switch_3": { - "name": "Switch 3" - }, - "switch_4": { - "name": "Switch 4" - }, - "switch_5": { - "name": "Switch 5" - }, - "switch_6": { - "name": "Switch 6" - }, - "switch_7": { - "name": "Switch 7" - }, - "switch_8": { - "name": "Switch 8" - }, - "usb_1": { - "name": "USB 1" - }, - "usb_2": { - "name": "USB 2" - }, - "usb_3": { - "name": "USB 3" - }, - "usb_4": { - "name": "USB 4" - }, - "usb_5": { - "name": "USB 5" - }, - "usb_6": { - "name": "USB 6" - }, - "socket_1": { - "name": "Socket 1" - }, - "socket_2": { - "name": "Socket 2" - }, - "socket_3": { - "name": "Socket 3" - }, - "socket_4": { - "name": "Socket 4" - }, - "socket_5": { - "name": "Socket 5" - }, - "socket_6": { - "name": "Socket 6" + "indexed_usb": { + "name": "USB {index}" }, "ionizer": { "name": "Ionizer" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 67f3ba9cb81..f6d5df9af73 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -232,35 +232,43 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "ggq": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="switch_5", + translation_key="indexed_switch", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="switch_6", + translation_key="indexed_switch", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH_7, - translation_key="switch_7", + translation_key="indexed_switch", + translation_placeholders={"index": "7"}, ), SwitchEntityDescription( key=DPCode.SWITCH_8, - translation_key="switch_8", + translation_key="indexed_switch", + translation_placeholders={"index": "8"}, ), ), # Wake Up Light II @@ -272,22 +280,26 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="alarm_1", + translation_key="indexed_alarm", + translation_placeholders={"index": "1"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="alarm_2", + translation_key="indexed_alarm", + translation_placeholders={"index": "2"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="alarm_3", + translation_key="indexed_alarm", + translation_placeholders={"index": "3"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="alarm_4", + translation_key="indexed_alarm", + translation_placeholders={"index": "4"}, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -324,67 +336,81 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="switch_5", + translation_key="indexed_switch", + translation_placeholders={"index": "5"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="switch_6", + translation_key="indexed_switch", + translation_placeholders={"index": "6"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_7, - translation_key="switch_7", + translation_key="indexed_switch", + translation_placeholders={"index": "7"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_8, - translation_key="switch_8", + translation_key="indexed_switch", + translation_placeholders={"index": "8"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - translation_key="usb_1", + translation_key="indexed_usb", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - translation_key="usb_2", + translation_key="indexed_usb", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - translation_key="usb_3", + translation_key="indexed_usb", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - translation_key="usb_4", + translation_key="indexed_usb", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - translation_key="usb_5", + translation_key="indexed_usb", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - translation_key="usb_6", + translation_key="indexed_usb", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -487,57 +513,69 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="socket_1", + translation_key="indexed_socket", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="socket_2", + translation_key="indexed_socket", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="socket_3", + translation_key="indexed_socket", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="socket_4", + translation_key="indexed_socket", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - translation_key="socket_5", + translation_key="indexed_socket", + translation_placeholders={"index": "5"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - translation_key="socket_6", + translation_key="indexed_socket", + translation_placeholders={"index": "6"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - translation_key="usb_1", + translation_key="indexed_usb", + translation_placeholders={"index": "1"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - translation_key="usb_2", + translation_key="indexed_usb", + translation_placeholders={"index": "2"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - translation_key="usb_3", + translation_key="indexed_usb", + translation_placeholders={"index": "3"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - translation_key="usb_4", + translation_key="indexed_usb", + translation_placeholders={"index": "4"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - translation_key="usb_5", + translation_key="indexed_usb", + translation_placeholders={"index": "5"}, ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - translation_key="usb_6", + translation_key="indexed_usb", + translation_placeholders={"index": "6"}, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -698,22 +736,26 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "tdq": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - translation_key="switch_3", + translation_key="indexed_switch", + translation_placeholders={"index": "3"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - translation_key="switch_4", + translation_key="indexed_switch", + translation_placeholders={"index": "4"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( @@ -746,12 +788,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "wkcz": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - translation_key="switch_1", + translation_key="indexed_switch", + translation_placeholders={"index": "1"}, device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - translation_key="switch_2", + translation_key="indexed_switch", + translation_placeholders={"index": "2"}, device_class=SwitchDeviceClass.OUTLET, ), ), diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py index 84948a92e98..4fd3d34a51d 100644 --- a/homeassistant/components/unifi/hub/entity_loader.py +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -25,6 +25,7 @@ from ..const import LOGGER, UNIFI_WIRELESS_CLIENTS from ..entity import UnifiEntity, UnifiEntityDescription if TYPE_CHECKING: + from .. import UnifiConfigEntry from .hub import UnifiHub CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) @@ -34,7 +35,7 @@ POLL_INTERVAL = timedelta(seconds=10) class UnifiEntityLoader: """UniFi Network integration handling platforms for entity registration.""" - def __init__(self, hub: UnifiHub) -> None: + def __init__(self, hub: UnifiHub, config_entry: UnifiConfigEntry) -> None: """Initialize the UniFi entity loader.""" self.hub = hub self.api_updaters = ( @@ -57,15 +58,16 @@ class UnifiEntityLoader: ) self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS] - self._dataUpdateCoordinator = DataUpdateCoordinator( + self._data_update_coordinator = DataUpdateCoordinator( hub.hass, LOGGER, name="Unifi entity poller", + config_entry=config_entry, update_method=self._update_pollable_api_data, update_interval=POLL_INTERVAL, ) - self._update_listener = self._dataUpdateCoordinator.async_add_listener( + self._update_listener = self._data_update_coordinator.async_add_listener( update_callback=lambda: None ) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index 6cf8825a26c..9ea887bdb29 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -39,7 +39,7 @@ class UnifiHub: self.hass = hass self.api = api self.config = UnifiConfig.from_config_entry(config_entry) - self.entity_loader = UnifiEntityLoader(self) + self.entity_loader = UnifiEntityLoader(self, config_entry) self._entity_helper = UnifiEntityHelper(hass, api) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) diff --git a/homeassistant/components/uptime_kuma/__init__.py b/homeassistant/components/uptime_kuma/__init__.py index 68234077976..cdeae16cc5a 100644 --- a/homeassistant/components/uptime_kuma/__init__.py +++ b/homeassistant/components/uptime_kuma/__init__.py @@ -6,7 +6,7 @@ from pythonkuma.update import UpdateChecker from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.hass_dict import HassKey @@ -19,7 +19,6 @@ from .coordinator import ( _PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE] -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) UPTIME_KUMA_KEY: HassKey[UptimeKumaSoftwareUpdateCoordinator] = HassKey(DOMAIN) @@ -43,6 +42,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) - return True +async def async_remove_config_entry_device( + hass: HomeAssistant, + config_entry: UptimeKumaConfigEntry, + device_entry: dr.DeviceEntry, +) -> bool: + """Remove a stale device from a config entry.""" + + def normalize_key(id: str) -> int | str: + key = id.removeprefix(f"{config_entry.entry_id}_") + return int(key) if key.isnumeric() else key + + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + and ( + identifier[1] == config_entry.entry_id + or normalize_key(identifier[1]) in config_entry.runtime_data.data + ) + ) + + async def async_unload_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/homeassistant/components/uptime_kuma/config_flow.py b/homeassistant/components/uptime_kuma/config_flow.py index da71084d1bc..a6429ea7dfe 100644 --- a/homeassistant/components/uptime_kuma/config_flow.py +++ b/homeassistant/components/uptime_kuma/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .const import DOMAIN @@ -47,7 +48,7 @@ async def validate_connection( hass: HomeAssistant, url: URL | str, verify_ssl: bool, - api_key: str, + api_key: str | None, ) -> dict[str, str]: """Validate Uptime Kuma connectivity.""" errors: dict[str, str] = {} @@ -69,6 +70,8 @@ async def validate_connection( class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Uptime Kuma.""" + _hassio_discovery: HassioServiceInfo | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -168,3 +171,61 @@ class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: + """Prepare configuration for Uptime Kuma add-on. + + This flow is triggered by the discovery component. + """ + self._async_abort_entries_match({CONF_URL: discovery_info.config[CONF_URL]}) + await self.async_set_unique_id(discovery_info.uuid) + self._abort_if_unique_id_configured( + updates={CONF_URL: discovery_info.config[CONF_URL]} + ) + + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Supervisor discovery.""" + assert self._hassio_discovery + errors: dict[str, str] = {} + api_key = user_input[CONF_API_KEY] if user_input else None + + if not ( + errors := await validate_connection( + self.hass, + self._hassio_discovery.config[CONF_URL], + True, + api_key, + ) + ): + if user_input is None: + self._set_confirm_only() + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={ + "addon": self._hassio_discovery.config["addon"] + }, + ) + return self.async_create_entry( + title=self._hassio_discovery.slug, + data={ + CONF_URL: self._hassio_discovery.config[CONF_URL], + CONF_VERIFY_SSL: True, + CONF_API_KEY: api_key, + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input + ), + description_placeholders={"addon": self._hassio_discovery.config["addon"]}, + errors=errors if user_input is not None else None, + ) diff --git a/homeassistant/components/uptime_kuma/quality_scale.yaml b/homeassistant/components/uptime_kuma/quality_scale.yaml index 876318c8917..3c9b5a3af50 100644 --- a/homeassistant/components/uptime_kuma/quality_scale.yaml +++ b/homeassistant/components/uptime_kuma/quality_scale.yaml @@ -44,12 +44,10 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: - status: exempt - comment: is not locally discoverable + discovery-update-info: done discovery: - status: exempt - comment: is not locally discoverable + status: done + comment: hassio addon supports discovery, other installation methods are not discoverable docs-data-update: done docs-examples: todo docs-known-limitations: done diff --git a/homeassistant/components/uptime_kuma/sensor.py b/homeassistant/components/uptime_kuma/sensor.py index c76fbcae04c..b499c67da16 100644 --- a/homeassistant/components/uptime_kuma/sensor.py +++ b/homeassistant/components/uptime_kuma/sensor.py @@ -162,7 +162,11 @@ class UptimeKumaSensorEntity( name=coordinator.data[monitor].monitor_name, identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor!s}")}, manufacturer="Uptime Kuma", - configuration_url=coordinator.config_entry.data[CONF_URL], + configuration_url=( + None + if "127.0.0.1" in (url := coordinator.config_entry.data[CONF_URL]) + else url + ), sw_version=coordinator.api.version.version, ) diff --git a/homeassistant/components/uptime_kuma/strings.json b/homeassistant/components/uptime_kuma/strings.json index 62b1ccbdd9a..e84b68501f3 100644 --- a/homeassistant/components/uptime_kuma/strings.json +++ b/homeassistant/components/uptime_kuma/strings.json @@ -36,6 +36,16 @@ "verify_ssl": "[%key:component::uptime_kuma::config::step::user::data_description::verify_ssl%]", "api_key": "[%key:component::uptime_kuma::config::step::user::data_description::api_key%]" } + }, + "hassio_confirm": { + "title": "Uptime Kuma via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the Uptime Kuma service provided by the add-on: {addon}?", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::uptime_kuma::config::step::user::data_description::api_key%]" + } } }, "error": { diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 0b533795a2c..fe855159d55 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.3.3"] + "requirements": ["voip-utils==0.3.4"] } diff --git a/homeassistant/components/volvo/config_flow.py b/homeassistant/components/volvo/config_flow.py index 05d19fd1d26..f187d751a2d 100644 --- a/homeassistant/components/volvo/config_flow.py +++ b/homeassistant/components/volvo/config_flow.py @@ -9,6 +9,7 @@ from typing import Any import voluptuous as vol from volvocarsapi.api import VolvoCarsApi from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle +from volvocarsapi.scopes import DEFAULT_SCOPES from homeassistant.config_entries import ( SOURCE_REAUTH, @@ -54,6 +55,13 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): self._vehicles: list[VolvoCarsVehicle] = [] self._config_data: dict = {} + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return super().extra_authorize_data | { + "scope": " ".join(DEFAULT_SCOPES), + } + @property def logger(self) -> logging.Logger: """Return logger.""" diff --git a/homeassistant/components/volvo/icons.json b/homeassistant/components/volvo/icons.json index 8e2897c66ad..61f67bcfe04 100644 --- a/homeassistant/components/volvo/icons.json +++ b/homeassistant/components/volvo/icons.json @@ -20,7 +20,11 @@ "default": "mdi:gas-station" }, "charger_connection_status": { - "default": "mdi:ev-plug-ccs2" + "default": "mdi:power-plug-off", + "state": { + "connected": "mdi:power-plug", + "fault": "mdi:flash-alert" + } }, "charging_power": { "default": "mdi:gauge-empty", @@ -44,22 +48,22 @@ } }, "distance_to_empty_battery": { - "default": "mdi:gauge-empty" + "default": "mdi:battery-outline" }, "distance_to_empty_tank": { "default": "mdi:gauge-empty" }, "distance_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-check" }, "engine_time_to_service": { - "default": "mdi:wrench-clock" + "default": "mdi:wrench-cog" }, "estimated_charging_time": { "default": "mdi:battery-clock" }, "fuel_amount": { - "default": "mdi:gas-station" + "default": "mdi:fuel" }, "odometer": { "default": "mdi:counter" diff --git a/homeassistant/components/weatherflow_cloud/icons.json b/homeassistant/components/weatherflow_cloud/icons.json index 5b9cd9c6cf4..a5759d8b810 100644 --- a/homeassistant/components/weatherflow_cloud/icons.json +++ b/homeassistant/components/weatherflow_cloud/icons.json @@ -34,6 +34,60 @@ "lightning_strike_last_epoch": { "default": "mdi:lightning-bolt" }, + + "precip_accum_local_day": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_day_final": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_yesterday": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + "precip_accum_local_yesterday_final": { + "default": "mdi:umbrella-closed", + "range": { + "0.01": "mdi:umbrella" + } + }, + + "precip_minutes_local_day": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + "precip_minutes_local_yesterday": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + "precip_minutes_local_yesterday_final": { + "default": "mdi:umbrella-closed", + "range": { + "1": "mdi:umbrella" + } + }, + + "precip_analysis_type_yesterday": { + "default": "mdi:radar", + "state": { + "rain": "mdi:weather-rainy", + "snow": "mdi:weather-snowy", + "rain_snow": "mdi:weather-snoy-rainy", + "lightning": "mdi:weather-lightning-rainy" + } + }, "sea_level_pressure": { "default": "mdi:gauge" }, @@ -49,6 +103,7 @@ "wind_chill": { "default": "mdi:snowflake-thermometer" }, + "wind_direction": { "default": "mdi:compass", "range": { diff --git a/homeassistant/components/weatherflow_cloud/sensor.py b/homeassistant/components/weatherflow_cloud/sensor.py index 42357807d17..ec094448519 100644 --- a/homeassistant/components/weatherflow_cloud/sensor.py +++ b/homeassistant/components/weatherflow_cloud/sensor.py @@ -39,6 +39,14 @@ from .const import DOMAIN from .coordinator import WeatherFlowObservationCoordinator, WeatherFlowWindCoordinator from .entity import WeatherFlowCloudEntity +PRECIPITATION_TYPE = { + 0: "none", + 1: "rain", + 2: "snow", + 3: "sleet", + 4: "storm", +} + @dataclass(frozen=True, kw_only=True) class WeatherFlowCloudSensorEntityDescription( @@ -223,6 +231,81 @@ WF_SENSORS: tuple[WeatherFlowCloudSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=3, ), + # Rain Sensors + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_last_1hr", + translation_key="precip_accum_last_1hr", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_last_1hr, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_day", + translation_key="precip_accum_local_day", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_day, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_day_final", + translation_key="precip_accum_local_day_final", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_day_final, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_yesterday", + translation_key="precip_accum_local_yesterday", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_yesterday, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_accum_local_yesterday_final", + translation_key="precip_accum_local_yesterday_final", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.precip_accum_local_yesterday_final, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_analysis_type_yesterday", + translation_key="precip_analysis_type_yesterday", + device_class=SensorDeviceClass.ENUM, + options=["none", "rain", "snow", "sleet", "storm"], + suggested_display_precision=1, + value_fn=lambda data: PRECIPITATION_TYPE.get( + data.precip_analysis_type_yesterday + ), + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_day", + translation_key="precip_minutes_local_day", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_day, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_yesterday", + translation_key="precip_minutes_local_yesterday", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_yesterday, + ), + WeatherFlowCloudSensorEntityDescription( + key="precip_minutes_local_yesterday_final", + translation_key="precip_minutes_local_yesterday_final", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=1, + value_fn=lambda data: data.precip_minutes_local_yesterday_final, + ), # Lightning Sensors WeatherFlowCloudSensorEntityDescription( key="lightning_strike_count", diff --git a/homeassistant/components/weatherflow_cloud/strings.json b/homeassistant/components/weatherflow_cloud/strings.json index 6c6e6f122a4..5b628e9f5c8 100644 --- a/homeassistant/components/weatherflow_cloud/strings.json +++ b/homeassistant/components/weatherflow_cloud/strings.json @@ -56,6 +56,34 @@ "lightning_strike_last_epoch": { "name": "Lightning last strike" }, + "precip_accum_last_1hr": { + "name": "Rain last hour" + }, + + "precip_accum_local_day": { + "name": "Precipitation today" + }, + "precip_accum_local_day_final": { + "name": "Nearcast precipitation today" + }, + "precip_accum_local_yesterday": { + "name": "Precipitation yesterday" + }, + "precip_accum_local_yesterday_final": { + "name": "Nearcast precipitation yesterday" + }, + "precip_analysis_type_yesterday": { + "name": "Precipitation type yesterday" + }, + "precip_minutes_local_day": { + "name": "Precipitation duration today" + }, + "precip_minutes_local_yesterday": { + "name": "Precipitation duration yesterday" + }, + "precip_minutes_local_yesterday_final": { + "name": "Nearcast precipitation duration yesterday" + }, "sea_level_pressure": { "name": "Pressure sea level" }, diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2897fbbdb16..bd318c5e30b 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==1.1.0"] + "requirements": ["xiaomi-ble==1.2.0"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 5b45628ee64..aa68009ac72 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7a02afbc5d7..b1fad926f1d 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==3.1.0"] + "requirements": ["yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 084e1c882ac..f5b44eb8fc4 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -74,7 +74,12 @@ from zha.event import EventBase from zha.exceptions import ZHAException from zha.mixins import LogMixin from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent -from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent +from zha.zigbee.device import ( + ClusterHandlerConfigurationComplete, + Device, + DeviceFirmwareInfoUpdatedEvent, + ZHAEvent, +) from zha.zigbee.group import Group, GroupInfo, GroupMember from zigpy.config import ( CONF_DATABASE, @@ -843,8 +848,23 @@ class ZHAGatewayProxy(EventBase): name=zha_device.name, manufacturer=zha_device.manufacturer, model=zha_device.model, + sw_version=zha_device.firmware_version, ) zha_device_proxy.device_id = device_registry_device.id + + def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None: + """Update software version in device registry.""" + device_registry.async_update_device( + device_registry_device.id, + sw_version=event.new_firmware_version, + ) + + self._unsubs.append( + zha_device.on_event( + DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version + ) + ) + return zha_device_proxy def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 2cbc962a305..facde4ead3a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.62"], + "requirements": ["zha==0.0.65"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 23d17ea128f..1c9454ec0a0 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -616,6 +616,18 @@ }, "water_supply": { "name": "Water supply" + }, + "frient_in_1": { + "name": "IN1" + }, + "frient_in_2": { + "name": "IN2" + }, + "frient_in_3": { + "name": "IN3" + }, + "frient_in_4": { + "name": "IN4" } }, "button": { @@ -639,6 +651,9 @@ }, "frost_lock_reset": { "name": "Frost lock reset" + }, + "reset_alarm": { + "name": "Reset alarm" } }, "climate": { @@ -1472,6 +1487,9 @@ "tier6_summation_delivered": { "name": "Tier 6 summation delivered" }, + "total_active_power": { + "name": "Total power" + }, "summation_received": { "name": "Summation received" }, @@ -2006,6 +2024,18 @@ }, "auto_relock": { "name": "Autorelock" + }, + "distance_tracking": { + "name": "Distance tracking" + }, + "water_shortage_auto_close": { + "name": "Water shortage auto-close" + }, + "frient_com_1": { + "name": "COM 1" + }, + "frient_com_2": { + "name": "COM 2" } } } diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 062581fd259..867e4ff2dd3 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -58,7 +58,7 @@ async def async_setup_entry( zha_data = get_zha_data(hass) if zha_data.update_coordinator is None: zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator( - hass, get_zha_gateway(hass).application_controller + hass, config_entry, get_zha_gateway(hass).application_controller ) entities_to_create = zha_data.platforms[Platform.UPDATE] @@ -79,12 +79,16 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa """Firmware update coordinator that broadcasts updates network-wide.""" def __init__( - self, hass: HomeAssistant, controller_application: ControllerApplication + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + controller_application: ControllerApplication, ) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, + config_entry=config_entry, name="ZHA firmware update coordinator", update_method=self.async_update_data, ) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d754419c94c..923cd776f92 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,7 +105,6 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData from .services import async_setup_services CONNECT_TIMEOUT = 10 +DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { @@ -147,6 +147,7 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +MIN_CONTROLLER_FIRMWARE_SDK_VERSION = AwesomeVersion("6.50.0") PLATFORMS = [ Platform.BINARY_SENSOR, @@ -367,6 +368,16 @@ class DriverEvents: ) ) + # listen for driver ready event to reload the config entry + self.config_entry.async_on_unload( + driver.on( + "driver ready", + lambda _: self.hass.config_entries.async_schedule_reload( + self.config_entry.entry_id + ), + ) + ) + # listen for new nodes being added to the mesh self.config_entry.async_on_unload( controller.on( @@ -799,11 +810,19 @@ class NodeEvents: node.on("notification", self.async_on_notification) ) - # Create a firmware update entity for each non-controller device that + # Create a firmware update entity for each device that # supports firmware updates - if not node.is_controller_node and any( - cc.id == CommandClass.FIRMWARE_UPDATE_MD.value - for cc in node.command_classes + controller = self.controller_events.driver_events.driver.controller + if ( + not (is_controller_node := node.is_controller_node) + and any( + cc.id == CommandClass.FIRMWARE_UPDATE_MD.value + for cc in node.command_classes + ) + ) or ( + is_controller_node + and (sdk_version := controller.sdk_version) is not None + and sdk_version >= MIN_CONTROLLER_FIRMWARE_SDK_VERSION ): async_dispatcher_send( self.hass, @@ -1065,23 +1084,32 @@ async def client_listen( try: await client.listen(driver_ready) except BaseZwaveJSServerError as err: - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise LOGGER.error("Client listen failed: %s", err) except Exception as err: # We need to guard against unknown exceptions to not crash this task. LOGGER.exception("Unexpected exception: %s", err) - if entry.state is not ConfigEntryState.LOADED: + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: raise + if hass.is_stopping or entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS: + return + + if entry.state is ConfigEntryState.SETUP_IN_PROGRESS: + raise HomeAssistantError("Listen task ended unexpectedly") + # The entry needs to be reloaded since a new driver state # will be acquired on reconnect. # All model instances will be replaced when the new state is acquired. - if not hass.is_stopping: - if entry.state is not ConfigEntryState.LOADED: - raise HomeAssistantError("Listen task ended unexpectedly") + if entry.state.recoverable: LOGGER.debug("Disconnected from server. Reloading integration") hass.config_entries.async_schedule_reload(entry.entry_id) + else: + LOGGER.error( + "Disconnected from server. Cannot recover entry %s", + entry.title, + ) async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool: diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 0f75d8b4673..b392b1c95cd 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine from contextlib import suppress import dataclasses @@ -87,7 +86,6 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, DOMAIN, - DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, USER_AGENT, @@ -98,6 +96,7 @@ from .helpers import ( async_get_node_from_device_id, async_get_provisioning_entry_from_device_id, async_get_version_info, + async_wait_for_driver_ready_event, get_device_id, ) @@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller( connection.send_result(msg[ID], device.id) async_cleanup() - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - msg[DATA_UNSUBSCRIBE] = unsubs = [ async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added ), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When resetting the controller, the controller home id is also changed. # The controller state in the client is stale after resetting the controller, # so get the new home id with a new client using the helper function. @@ -2886,14 +2877,14 @@ async def websocket_hard_reset_controller( # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) + hass.config_entries.async_schedule_reload(entry.entry_id) @websocket_api.websocket_command( @@ -3100,27 +3091,19 @@ async def websocket_restore_nvm( ) ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - - wait_driver_ready = asyncio.Event() - # Set up subscription for progress events connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver) + await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False}) with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() - + await wait_for_driver_ready() # When restoring the NVM to the controller, the controller home id is also changed. # The controller state in the client is stale after restoring the NVM, # so get the new home id with a new client using the helper function. @@ -3133,14 +3116,13 @@ async def websocket_restore_nvm( # The stale unique id needs to be handled by a repair flow, # after the config entry has been reloaded. LOGGER.error( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) else: hass.config_entries.async_update_entry( entry, unique_id=str(version_info.home_id) ) - await hass.config_entries.async_reload(entry.entry_id) connection.send_message( @@ -3152,3 +3134,4 @@ async def websocket_restore_nvm( ) ) connection.send_result(msg[ID]) + async_cleanup() diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index d98dcf3dac8..6121bd00508 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -62,9 +62,12 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, - DRIVER_READY_TIMEOUT, ) -from .helpers import CannotConnect, async_get_version_info +from .helpers import ( + CannotConnect, + async_get_version_info, + async_wait_for_driver_ready_event, +) from .models import ZwaveJSConfigEntry _LOGGER = logging.getLogger(__name__) @@ -90,6 +93,10 @@ MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") NETWORK_TYPE_NEW = "new" NETWORK_TYPE_EXISTING = "existing" +ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( + "https://www.home-assistant.io/integrations/zwave_js/" + "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" +) def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: @@ -443,7 +450,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): None, ) if not self._reconfigure_config_entry: - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) vid = discovery_info.vid pid = discovery_info.pid @@ -887,7 +899,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): config_entry = self._reconfigure_config_entry assert config_entry is not None if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON): - return self.async_abort(reason="addon_required") + return self.async_abort( + reason="addon_required", + description_placeholders={ + "zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS, + }, + ) try: driver = self._get_driver() @@ -1396,19 +1413,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): event["bytesWritten"] / event["total"] * 0.5 + 0.5 ) - @callback - def set_driver_ready(event: dict) -> None: - "Set the driver ready event." - wait_driver_ready.set() - driver = self._get_driver() controller = driver.controller - wait_driver_ready = asyncio.Event() unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), - driver.once("driver ready", set_driver_ready), ] + + wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver) + try: await controller.async_restore_nvm( self.backup_data, {"preserveRoutes": False} @@ -1417,8 +1430,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(DRIVER_READY_TIMEOUT): - await wait_driver_ready.wait() + await wait_for_driver_ready() try: version_info = await async_get_version_info( self.hass, config_entry.data[CONF_URL] @@ -1435,10 +1447,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( config_entry, unique_id=str(version_info.home_id) ) - await self.hass.config_entries.async_reload(config_entry.entry_id) - # Reload the config entry two times to clean up - # the stale device entry. + # The config entry will be also be reloaded when the driver is ready, + # by the listener in the package module, + # and two reloads are needed to clean up the stale controller device entry. # Since both the old and the new controller have the same node id, # but different hardware identifiers, the integration # will create a new device for the new controller, on the first reload, diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6dc76ebd05d..0ccf51539d6 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -201,7 +201,3 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, } - -# Other constants - -DRIVER_READY_TIMEOUT = 60 diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 5694be5482b..17f4909662c 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import astuple, dataclass import logging from typing import Any, cast @@ -56,6 +56,7 @@ from .const import ( ) from .models import ZwaveJSConfigEntry +DRIVER_READY_EVENT_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 @@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info +@callback +def async_wait_for_driver_ready_event( + config_entry: ZwaveJSConfigEntry, + driver: Driver, +) -> Callable[[], Coroutine[Any, Any, None]]: + """Wait for the driver ready event and the config entry reload. + + When the driver ready event is received + the config entry will be reloaded by the integration. + This function helps wait for that to happen + before proceeding with further actions. + + If the config entry is reloaded for another reason, + this function will not wait for it to be reloaded again. + + Raises TimeoutError if the driver ready event and reload + is not received within the specified timeout. + """ + driver_ready_event_received = asyncio.Event() + config_entry_reloaded = asyncio.Event() + unsubscribers: list[Callable[[], None]] = [] + + @callback + def driver_ready_received(event: dict) -> None: + """Receive the driver ready event.""" + driver_ready_event_received.set() + + unsubscribers.append(driver.once("driver ready", driver_ready_received)) + + @callback + def on_config_entry_state_change() -> None: + """Check config entry was loaded after driver ready event.""" + if config_entry.state is ConfigEntryState.LOADED: + config_entry_reloaded.set() + + unsubscribers.append( + config_entry.async_on_state_change(on_config_entry_state_change) + ) + + async def wait_for_events() -> None: + try: + async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT): + await asyncio.gather( + driver_ready_event_received.wait(), config_entry_reloaded.wait() + ) + finally: + for unsubscribe in unsubscribers: + unsubscribe() + + return wait_for_events + + class CannotConnect(HomeAssistantError): """Indicate connection error.""" diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 2cad8df3805..153e8e6a7fe 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.1"], "usb": [ { "vid": "0658", diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0288fbd7131..8ac356a40b0 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -4,7 +4,7 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave add-on info.", "addon_install_failed": "Failed to install the Z-Wave add-on.", - "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. You can still use the Backup and Restore buttons to migrate your network manually.", + "addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).", "addon_set_config_failed": "Failed to set Z-Wave configuration.", "addon_start_failed": "Failed to start the Z-Wave add-on.", "addon_stop_failed": "Failed to stop the Z-Wave add-on.", diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 89fb4dd4aba..42a4b4cf6dd 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,26 +4,28 @@ from __future__ import annotations import asyncio from collections import Counter -from collections.abc import Callable +from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Any, Final +from typing import Any, Final, cast from awesomeversion import AwesomeVersion from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.node.firmware import ( - NodeFirmwareUpdateInfo, - NodeFirmwareUpdateProgress, - NodeFirmwareUpdateResult, +from zwave_js_server.model.firmware import ( + FirmwareUpdateInfo, + FirmwareUpdateProgress, + FirmwareUpdateResult, ) +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.node.firmware import NodeFirmwareUpdateInfo from homeassistant.components.update import ( ATTR_LATEST_VERSION, UpdateDeviceClass, UpdateEntity, + UpdateEntityDescription, UpdateEntityFeature, ) from homeassistant.const import EntityCategory @@ -45,11 +47,54 @@ UPDATE_DELAY_INTERVAL = 5 # In minutes ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" +@dataclass(frozen=True, kw_only=True) +class ZWaveUpdateEntityDescription(UpdateEntityDescription): + """Class describing Z-Wave update entity.""" + + install_method: Callable[ + [ZWaveFirmwareUpdateEntity, FirmwareUpdateInfo], + Awaitable[FirmwareUpdateResult], + ] + progress_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]] + finished_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]] + + +CONTROLLER_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription( + key="controller_firmware_update", + install_method=( + lambda entity, firmware_update_info: entity.driver.async_firmware_update_otw( + update_info=firmware_update_info + ) + ), + progress_method=lambda entity: entity.driver.on( + "firmware update progress", entity.update_progress + ), + finished_method=lambda entity: entity.driver.on( + "firmware update finished", entity.update_finished + ), +) +NODE_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription( + key="node_firmware_update", + install_method=( + lambda entity, + firmware_update_info: entity.driver.controller.async_firmware_update_ota( + entity.node, cast(NodeFirmwareUpdateInfo, firmware_update_info) + ) + ), + progress_method=lambda entity: entity.node.on( + "firmware update progress", entity.update_progress + ), + finished_method=lambda entity: entity.node.on( + "firmware update finished", entity.update_finished + ), +) + + @dataclass -class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): +class ZWaveFirmwareUpdateExtraStoredData(ExtraStoredData): """Extra stored data for Z-Wave node firmware update entity.""" - latest_version_firmware: NodeFirmwareUpdateInfo | None + latest_version_firmware: FirmwareUpdateInfo | None def as_dict(self) -> dict[str, Any]: """Return a dict representation of the extra data.""" @@ -60,7 +105,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): } @classmethod - def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData: + def from_dict(cls, data: dict[str, Any]) -> ZWaveFirmwareUpdateExtraStoredData: """Initialize the extra data from a dict.""" # If there was no firmware info stored, or if it's stale info, we don't restore # anything. @@ -70,7 +115,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): ): return cls(None) - return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict)) + return cls(FirmwareUpdateInfo.from_dict(firmware_dict)) async def async_setup_entry( @@ -92,7 +137,23 @@ async def async_setup_entry( delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, delay)]) + if node.is_controller_node: + # If the node is a controller, we create a controller firmware update entity + entity = ZWaveFirmwareUpdateEntity( + driver, + node, + delay=delay, + entity_description=CONTROLLER_UPDATE_ENTITY_DESCRIPTION, + ) + else: + # If the node is not a controller, we create a node firmware update entity + entity = ZWaveFirmwareUpdateEntity( + driver, + node, + delay=delay, + entity_description=NODE_UPDATE_ENTITY_DESCRIPTION, + ) + async_add_entities([entity]) config_entry.async_on_unload( async_dispatcher_connect( @@ -103,9 +164,12 @@ async def async_setup_entry( ) -class ZWaveNodeFirmwareUpdate(UpdateEntity): +class ZWaveFirmwareUpdateEntity(UpdateEntity): """Representation of a firmware update entity.""" + driver: Driver + entity_description: ZWaveUpdateEntityDescription + node: ZwaveNode _attr_entity_category = EntityCategory.CONFIG _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = ( @@ -116,17 +180,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, driver: Driver, node: ZwaveNode, delay: timedelta) -> None: + def __init__( + self, + driver: Driver, + node: ZwaveNode, + delay: timedelta, + entity_description: ZWaveUpdateEntityDescription, + ) -> None: """Initialize a Z-Wave device firmware update entity.""" self.driver = driver + self.entity_description = entity_description self.node = node - self._latest_version_firmware: NodeFirmwareUpdateInfo | None = None + self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None self._finished_unsub: Callable[[], None] | None = None self._finished_event = asyncio.Event() - self._result: NodeFirmwareUpdateResult | None = None + self._result: FirmwareUpdateResult | None = None self._delay: Final[timedelta] = delay # Entity class attributes @@ -138,9 +209,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_device_info = get_device_info(driver, node) @property - def extra_restore_state_data(self) -> ZWaveNodeFirmwareUpdateExtraStoredData: + def extra_restore_state_data(self) -> ZWaveFirmwareUpdateExtraStoredData: """Return ZWave Node Firmware Update specific state data to be restored.""" - return ZWaveNodeFirmwareUpdateExtraStoredData(self._latest_version_firmware) + return ZWaveFirmwareUpdateExtraStoredData(self._latest_version_firmware) @callback def _update_on_status_change(self, _: dict[str, Any]) -> None: @@ -149,9 +220,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.hass.async_create_task(self._async_update()) @callback - def _update_progress(self, event: dict[str, Any]) -> None: + def update_progress(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - progress: NodeFirmwareUpdateProgress = event["firmware_update_progress"] + progress: FirmwareUpdateProgress = event["firmware_update_progress"] if not self._latest_version_firmware: return self._attr_in_progress = True @@ -159,9 +230,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.async_write_ha_state() @callback - def _update_finished(self, event: dict[str, Any]) -> None: + def update_finished(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - result: NodeFirmwareUpdateResult = event["firmware_update_finished"] + result: FirmwareUpdateResult = event["firmware_update_finished"] self._result = result self._finished_event.set() @@ -266,15 +337,11 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_update_percentage = None self.async_write_ha_state() - self._progress_unsub = self.node.on( - "firmware update progress", self._update_progress - ) - self._finished_unsub = self.node.on( - "firmware update finished", self._update_finished - ) + self._progress_unsub = self.entity_description.progress_method(self) + self._finished_unsub = self.entity_description.finished_method(self) try: - await self.driver.controller.async_firmware_update_ota(self.node, firmware) + await self.entity_description.install_method(self, firmware) except BaseZwaveJSServerError as err: self._unsub_firmware_events_and_reset_progress() raise HomeAssistantError(err) from err @@ -342,8 +409,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): is not None and (extra_data := await self.async_get_last_extra_data()) and ( - latest_version_firmware - := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( + latest_version_firmware := ZWaveFirmwareUpdateExtraStoredData.from_dict( extra_data.as_dict() ).latest_version_firmware ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1c4f2b51ac7..da8e73d9566 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -298,8 +298,10 @@ class ConfigFlowContext(FlowContext, total=False): class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False): """Typed result dict for config flow.""" + # Extra keys, only present if type is CREATE_ENTRY minor_version: int options: Mapping[str, Any] + result: ConfigEntry subentries: Iterable[ConfigSubentryData] version: int @@ -3345,7 +3347,6 @@ class ConfigSubentryFlowManager( ), ) - result["result"] = True return result @@ -3508,7 +3509,6 @@ class OptionsFlowManager( ): self.hass.config_entries.async_schedule_reload(entry.entry_id) - result["result"] = True return result async def _async_setup_preview( diff --git a/homeassistant/const.py b/homeassistant/const.py index 2daa6d91db2..1983932813e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ce1c0806b14..5023d291ad5 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -142,7 +142,6 @@ class FlowResult(TypedDict, Generic[_FlowContextT, _HandlerT], total=False): progress_task: asyncio.Task[Any] | None reason: str required: bool - result: Any step_id: str title: str translation_domain: str @@ -677,9 +676,10 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): and key in suggested_values ): new_section_key = copy.copy(key) - schema[new_section_key] = val - val.schema = self.add_suggested_values_to_schema( - val.schema, suggested_values[key] + new_val = copy.copy(val) + schema[new_section_key] = new_val + new_val.schema = self.add_suggested_values_to_schema( + new_val.schema, suggested_values[key] ) continue @@ -706,10 +706,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): last_step: bool | None = None, preview: str | None = None, ) -> _FlowResultT: - """Return the definition of a form to gather user input. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Return the definition of a form to gather user input.""" flow_result = self._flow_result( type=FlowResultType.FORM, flow_id=self.flow_id, @@ -771,10 +768,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): url: str, description_placeholders: Mapping[str, str] | None = None, ) -> _FlowResultT: - """Return the definition of an external step for the user to take. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Return the definition of an external step for the user to take.""" flow_result = self._flow_result( type=FlowResultType.EXTERNAL_STEP, flow_id=self.flow_id, @@ -805,10 +799,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): description_placeholders: Mapping[str, str] | None = None, progress_task: asyncio.Task[Any] | None = None, ) -> _FlowResultT: - """Show a progress message to the user, without user input allowed. - - The step_id parameter is deprecated and will be removed in a future release. - """ + """Show a progress message to the user, without user input allowed.""" if progress_task is None and not self.__no_progress_task_reported: self.__no_progress_task_reported = True cls = self.__class__ @@ -868,7 +859,6 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]): """Show a navigation menu to the user. Options dict maps step_id => i18n label - The step_id parameter is deprecated and will be removed in a future release. """ flow_result = self._flow_result( type=FlowResultType.MENU, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5d468fd1dc9..5816a0ddbd9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ FLOWS = { "airgradient", "airly", "airnow", + "airos", "airq", "airthings", "airthings_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a673b05218d..c606d79f2c5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2137,6 +2137,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "frient": { + "name": "Frient", + "iot_standards": [ + "zigbee" + ] + }, "fritzbox": { "name": "FRITZ!Box", "integrations": { @@ -6722,6 +6728,7 @@ "third_reality": { "name": "Third Reality", "iot_standards": [ + "matter", "zigbee" ] }, @@ -7002,6 +7009,12 @@ "ubiquiti": { "name": "Ubiquiti", "integrations": { + "airos": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling", + "name": "Ubiquiti airOS" + }, "unifi": { "integration_type": "hub", "config_flow": true, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index a3668acee8d..742840fa849 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -890,6 +890,10 @@ ZEROCONF = { }, ], "_ssh._tcp.local.": [ + { + "domain": "homee", + "name": "homee-*", + }, { "domain": "smappee", "name": "smappee1*", diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 65eb2786aaf..22074fb90a7 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -35,7 +35,7 @@ class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]): """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() - data.pop("result") + assert "result" not in result data.pop("data") data.pop("context") return data diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index bc6e7c810bf..c7f7d4c369d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,6 +32,7 @@ from homeassistant.util.json import format_unserializable_data from . import storage, translation from .debounce import Debouncer +from .deprecation import deprecated_function from .frame import ReportBehavior, report_usage from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType @@ -67,6 +68,7 @@ CONNECTION_ZIGBEE = "zigbee" ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +# Can be removed when suggested_area is removed from DeviceEntry RUNTIME_ONLY_ATTRS = {"suggested_area"} CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"} @@ -156,7 +158,7 @@ class _EventDeviceRegistryUpdatedData_Remove(TypedDict): action: Literal["remove"] device_id: str - device: DeviceEntry + device: dict[str, Any] class _EventDeviceRegistryUpdatedData_Update(TypedDict): @@ -343,7 +345,8 @@ class DeviceEntry: name: str | None = attr.ib(default=None) primary_config_entry: str | None = attr.ib(default=None) serial_number: str | None = attr.ib(default=None) - suggested_area: str | None = attr.ib(default=None) + # Suggested area is deprecated and will be removed from DeviceEntry in 2026.9. + _suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) # This value is not stored, just used to keep track of events to fire. @@ -442,6 +445,14 @@ class DeviceEntry: ) ) + @property + @deprecated_function( + "code which ignores suggested_area", breaks_in_ha_version="2026.9" + ) + def suggested_area(self) -> str | None: + """Return the suggested area for this device entry.""" + return self._suggested_area + @attr.s(frozen=True, slots=True) class DeletedDeviceEntry: @@ -895,7 +906,19 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if device is None: deleted_device = self.deleted_devices.get_entry(identifiers, connections) if deleted_device is None: - device = DeviceEntry(is_new=True) + area_id: str | None = None + if ( + suggested_area is not None + and suggested_area is not UNDEFINED + and suggested_area != "" + ): + # Circular dep + from . import area_registry as ar # noqa: PLC0415 + + area = ar.async_get(self.hass).async_get_or_create(suggested_area) + area_id = area.id + device = DeviceEntry(is_new=True, area_id=area_id) + else: self.deleted_devices.pop(deleted_device.id) device = deleted_device.to_device_entry( @@ -950,7 +973,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): model_id=model_id, name=name, serial_number=serial_number, - suggested_area=suggested_area, + _suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, ) @@ -989,6 +1012,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): remove_config_entry_id: str | UndefinedType = UNDEFINED, remove_config_subentry_id: str | None | UndefinedType = UNDEFINED, serial_number: str | None | UndefinedType = UNDEFINED, + # _suggested_area is used internally by the device registry and must + # not be set by integrations. + _suggested_area: str | None | UndefinedType = UNDEFINED, + # suggested_area is deprecated and will be removed in 2026.9 suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, @@ -1054,19 +1081,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): "Cannot define both merge_identifiers and new_identifiers" ) - if ( - suggested_area is not None - and suggested_area is not UNDEFINED - and suggested_area != "" - and area_id is UNDEFINED - and old.area_id is None - ): - # Circular dep - from . import area_registry as ar # noqa: PLC0415 - - area = ar.async_get(self.hass).async_get_or_create(suggested_area) - area_id = area.id - if add_config_entry_id is not UNDEFINED: if add_config_subentry_id is UNDEFINED: # Interpret not specifying a subentry as None (the main entry) @@ -1144,6 +1158,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values["config_entries_subentries"] = config_entries_subentries old_values["config_entries_subentries"] = old.config_entries_subentries + if suggested_area is not UNDEFINED: + report_usage( + "passes a suggested_area to device_registry.async_update device", + core_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2026.9.0", + ) + + if _suggested_area is not UNDEFINED: + suggested_area = _suggested_area + added_connections: set[tuple[str, str]] | None = None added_identifiers: set[tuple[str, str]] | None = None @@ -1197,7 +1221,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ("name", name), ("name_by_user", name_by_user), ("serial_number", serial_number), - ("suggested_area", suggested_area), ("sw_version", sw_version), ("via_device_id", via_device_id), ): @@ -1205,12 +1228,18 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + # Can be removed when suggested_area is removed from DeviceEntry + if suggested_area is not UNDEFINED and suggested_area != old._suggested_area: # noqa: SLF001 + new_values["suggested_area"] = suggested_area + old_values["suggested_area"] = old._suggested_area # noqa: SLF001 + if old.is_new: new_values["is_new"] = False if not new_values: return old + # This condition can be removed when suggested_area is removed from DeviceEntry if not RUNTIME_ONLY_ATTRS.issuperset(new_values): # Change modified_at if we are changing something that we store new_values["modified_at"] = utcnow() @@ -1233,6 +1262,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): # firing events for data we have nothing to compare # against since its never saved on disk if RUNTIME_ONLY_ATTRS.issuperset(new_values): + # This can be removed when suggested_area is removed from DeviceEntry return new self.async_schedule_save() @@ -1319,7 +1349,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self.hass.bus.async_fire_internal( EVENT_DEVICE_REGISTRY_UPDATED, _EventDeviceRegistryUpdatedData_Remove( - action="remove", device_id=device_id, device=device + action="remove", device_id=device_id, device=device.dict_repr ), ) self.async_schedule_save() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7051521b805..d972b421fc4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1103,13 +1103,13 @@ class EntityRegistry(BaseRegistry): entities = async_entries_for_device( self, event.data["device_id"], include_disabled_entities=True ) - removed_device = event.data["device"] + removed_device_dict = event.data["device"] for entity in entities: config_entry_id = entity.config_entry_id if ( - config_entry_id in removed_device.config_entries + config_entry_id in removed_device_dict["config_entries"] and entity.config_subentry_id - in removed_device.config_entries_subentries[config_entry_id] + in removed_device_dict["config_entries_subentries"][config_entry_id] ): self.async_remove(entity.entity_id) else: diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 46b3d883865..de3f71c4834 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_ENABLED, CONF_ID, CONF_PLATFORM, + CONF_SELECTOR, CONF_VARIABLES, ) from homeassistant.core import ( @@ -41,8 +42,9 @@ from homeassistant.util.hass_dict import HassKey from homeassistant.util.yaml import load_yaml_dict from homeassistant.util.yaml.loader import JSON_TYPE -from . import config_validation as cv +from . import config_validation as cv, selector from .integration_platform import async_process_integration_platforms +from .selector import TargetSelector from .template import Template from .typing import ConfigType, TemplateVarsType @@ -73,12 +75,15 @@ TRIGGERS: HassKey[dict[str, str]] = HassKey("triggers") # Basic schemas to sanity check the trigger descriptions, # full validation is done by hassfest.triggers _FIELD_SCHEMA = vol.Schema( - {}, + { + vol.Optional(CONF_SELECTOR): selector.validate_selector, + }, extra=vol.ALLOW_EXTRA, ) _TRIGGER_SCHEMA = vol.Schema( { + vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), }, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a43eadce0de..f8a57ba61bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,12 +1,12 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.2.0 -aiodiscover==2.7.0 +aiodiscover==2.7.1 aiodns==3.5.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.14 +aiohttp==3.12.15 aiohttp_cors==0.8.1 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 @@ -30,16 +30,16 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 cryptography==45.0.3 -dbus-fast==2.44.2 +dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==4.0.1 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250702.3 -home-assistant-intents==2025.6.23 +home-assistant-frontend==20250731.0 +home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 @@ -209,7 +209,6 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py > 0.25.0 requires cargo 1.84.0 -# Stable Alpine current only ships cargo 1.83.0 +# rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 -rpds-py==0.24.0 +rpds-py==0.26.0 diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 59775655854..abcf32f2659 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -17,6 +17,7 @@ from . import bootstrap from .core import callback from .helpers.frame import warn_use from .util.executor import InterruptibleThreadPoolExecutor +from .util.resource import set_open_file_descriptor_limit from .util.thread import deadlock_safe_shutdown # @@ -146,6 +147,7 @@ def _enable_posix_spawn() -> None: def run(runtime_config: RuntimeConfig) -> int: """Run Home Assistant.""" _enable_posix_spawn() + set_open_file_descriptor_limit() asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) # Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out loop = asyncio.new_event_loop() diff --git a/homeassistant/util/resource.py b/homeassistant/util/resource.py new file mode 100644 index 00000000000..41982df9e50 --- /dev/null +++ b/homeassistant/util/resource.py @@ -0,0 +1,65 @@ +"""Resource management utilities for Home Assistant.""" + +from __future__ import annotations + +import logging +import os +import resource +from typing import Final + +_LOGGER = logging.getLogger(__name__) + +# Default soft file descriptor limit to set +DEFAULT_SOFT_FILE_LIMIT: Final = 2048 + + +def set_open_file_descriptor_limit() -> None: + """Set the maximum open file descriptor soft limit.""" + try: + # Check environment variable first, then use default + soft_limit = int(os.environ.get("SOFT_FILE_LIMIT", DEFAULT_SOFT_FILE_LIMIT)) + + # Get current limits + current_soft, current_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + + _LOGGER.debug( + "Current file descriptor limits: soft=%d, hard=%d", + current_soft, + current_hard, + ) + + # Don't increase if already at or above the desired limit + if current_soft >= soft_limit: + _LOGGER.debug( + "Current soft limit (%d) is already >= desired limit (%d), skipping", + current_soft, + soft_limit, + ) + return + + # Don't set soft limit higher than hard limit + if soft_limit > current_hard: + _LOGGER.warning( + "Requested soft limit (%d) exceeds hard limit (%d), " + "setting to hard limit", + soft_limit, + current_hard, + ) + soft_limit = current_hard + + # Set the new soft limit + resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, current_hard)) + + # Verify the change + new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + _LOGGER.info( + "File descriptor limits updated: soft=%d->%d, hard=%d", + current_soft, + new_soft, + new_hard, + ) + + except OSError as err: + _LOGGER.error("Failed to set file descriptor limit: %s", err) + except ValueError as err: + _LOGGER.error("Invalid file descriptor limit value: %s", err) diff --git a/mypy.ini b/mypy.ini index ba5ac08d3c9..8482138cc45 100644 --- a/mypy.ini +++ b/mypy.ini @@ -285,6 +285,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airos.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airq.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pyproject.toml b/pyproject.toml index d15a93fd8bd..a32e9308fe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.0.dev0" +version = "2025.9.0.dev0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.14", + "aiohttp==3.12.15", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.110.0", + "hass-nabucasa==0.110.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", @@ -487,19 +487,10 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast", # -- fixed, waiting for release / update - # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 - "ignore:.*invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", - # https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0 - "ignore:pkg_resources is deprecated as an API:UserWarning:datadog.util.compat", # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", - # https://github.com/vacanza/python-holidays/discussions/1800 - >1.0.0 - "ignore::DeprecationWarning:holidays", # https://github.com/ReactiveX/RxPY/pull/716 - >4.0.4 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:reactivex.internal.constants", - # https://github.com/postlund/pyatv/issues/2645 - >0.16.0 - # https://github.com/postlund/pyatv/pull/2664 - "ignore:Protobuf gencode .* exactly one major version older than the runtime version 6.* at pyatv:UserWarning:google.protobuf.runtime_version", # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", @@ -526,6 +517,9 @@ filterwarnings = [ "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", # https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12 + # https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390 + "ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device", # https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15 # https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api", @@ -542,8 +536,6 @@ filterwarnings = [ "ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api", # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", - # New in aiohttp - v3.9.0 - "ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)", # - SyntaxWarnings # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 "ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", @@ -589,7 +581,7 @@ filterwarnings = [ # -- Websockets 14.1 # https://websockets.readthedocs.io/en/stable/howto/upgrade.html "ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", - # https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0 + # https://github.com/graphql-python/gql/pull/543 - >=4.0.0b0 "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base", # -- unmaintained projects, last release about 2+ years diff --git a/requirements.txt b/requirements.txt index 6110854f5f6..ba08a72e324 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.5.0 aiohasupervisor==0.3.1 -aiohttp==3.12.14 +aiohttp==3.12.15 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1359413cd3a..1c7280547c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.1 +aioamazondevices==4.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -223,7 +223,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.2 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 @@ -310,7 +310,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -452,6 +452,9 @@ airgradient==0.9.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airos +airos==0.2.1 + # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -557,7 +560,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.1 +automower-ble==0.2.7 # homeassistant.components.generic # homeassistant.components.stream @@ -756,13 +759,13 @@ crownstone-sse==2.0.5 crownstone-uart==2.1.0 # homeassistant.components.datadog -datadog==0.15.0 +datadog==0.52.0 # homeassistant.components.metoffice datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 @@ -788,7 +791,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 @@ -1097,7 +1100,7 @@ greenwavereality==0.5.1 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.6.0 +growattServer==1.7.1 # homeassistant.components.google_sheets gspread==5.5.0 @@ -1130,7 +1133,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.splunk hass-splunk==0.1.1 @@ -1171,10 +1174,10 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250702.3 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 @@ -1237,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1388,7 +1391,7 @@ lupupy==0.3.2 lw12==0.9.2 # homeassistant.components.scrape -lxml==5.3.0 +lxml==6.0.0 # homeassistant.components.matrix matrix-nio==0.25.2 @@ -1455,7 +1458,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 @@ -1867,7 +1870,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==2.0.1 +pyblu==2.0.4 # homeassistant.components.neato pybotvac==0.0.28 @@ -1960,10 +1963,10 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms -pyemoncms==0.1.1 +pyemoncms==0.1.2 # homeassistant.components.enphase_envoy -pyenphase==2.2.2 +pyenphase==2.2.3 # homeassistant.components.envisalink pyenvisalink==4.7 @@ -2119,7 +2122,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 @@ -2161,7 +2164,7 @@ pymonoprice==0.4 pymsteams==0.1.12 # homeassistant.components.mysensors -pymysensors==0.25.0 +pymysensors==0.26.0 # homeassistant.components.iron_os pynecil==4.1.1 @@ -2478,7 +2481,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 @@ -2663,7 +2666,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -3054,7 +3057,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volkszaehler volkszaehler==0.4.0 @@ -3136,7 +3139,7 @@ wyoming==1.7.1 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.1.0 +xiaomi-ble==1.2.0 # homeassistant.components.knx xknx==3.8.0 @@ -3160,7 +3163,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale @@ -3182,7 +3185,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zabbix zabbix-utils==2.0.2 @@ -3200,7 +3203,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.65 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 @@ -3212,7 +3215,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31004789f97..4aa7a5063e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.5.1 +aioamazondevices==4.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -211,7 +211,7 @@ aiocomelit==0.12.3 aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp -aiodiscover==2.7.0 +aiodiscover==2.7.1 # homeassistant.components.dnsip aiodns==3.5.0 @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.1.2 +aioesphomeapi==37.2.2 # homeassistant.components.flo aioflo==2021.11.0 @@ -292,7 +292,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.10.0 +aiomealie==0.10.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -434,6 +434,9 @@ airgradient==0.9.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airos +airos==0.2.1 + # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -512,7 +515,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.1 +automower-ble==0.2.7 # homeassistant.components.generic # homeassistant.components.stream @@ -659,13 +662,13 @@ crownstone-sse==2.0.5 crownstone-uart==2.1.0 # homeassistant.components.datadog -datadog==0.15.0 +datadog==0.52.0 # homeassistant.components.metoffice datapoint==0.12.1 # homeassistant.components.bluetooth -dbus-fast==2.44.2 +dbus-fast==2.44.3 # homeassistant.components.debugpy debugpy==1.8.14 @@ -688,7 +691,7 @@ deluge-client==1.10.2 demetriek==1.3.0 # homeassistant.components.denonavr -denonavr==1.1.1 +denonavr==1.1.2 # homeassistant.components.devialet devialet==1.5.7 @@ -958,7 +961,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.1 # homeassistant.components.growatt_server -growattServer==1.6.0 +growattServer==1.7.1 # homeassistant.components.google_sheets gspread==5.5.0 @@ -991,7 +994,7 @@ habiticalib==0.4.1 habluetooth==4.0.1 # homeassistant.components.cloud -hass-nabucasa==0.110.0 +hass-nabucasa==0.110.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation @@ -1020,10 +1023,10 @@ hole==0.9.0 holidays==0.77 # homeassistant.components.frontend -home-assistant-frontend==20250702.3 +home-assistant-frontend==20250731.0 # homeassistant.components.conversation -home-assistant-intents==2025.6.23 +home-assistant-intents==2025.7.30 # homeassistant.components.homematicip_cloud homematicip==2.2.0 @@ -1071,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.1 +imgw_pib==1.5.2 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1186,7 +1189,7 @@ luftdaten==0.7.4 lupupy==0.3.2 # homeassistant.components.scrape -lxml==5.3.0 +lxml==6.0.0 # homeassistant.components.matrix matrix-nio==0.25.2 @@ -1247,7 +1250,7 @@ monzopy==1.5.1 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.29 +motionblinds==0.6.30 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 @@ -1569,7 +1572,7 @@ pybalboa==1.1.3 pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==2.0.1 +pyblu==2.0.4 # homeassistant.components.neato pybotvac==0.0.28 @@ -1635,10 +1638,10 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms -pyemoncms==0.1.1 +pyemoncms==0.1.2 # homeassistant.components.enphase_envoy -pyenphase==2.2.2 +pyenphase==2.2.3 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -1764,7 +1767,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.3 # homeassistant.components.litterrobot -pylitterbot==2024.2.2 +pylitterbot==2024.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.24.0 @@ -1797,7 +1800,7 @@ pymodbus==3.9.2 pymonoprice==0.4 # homeassistant.components.mysensors -pymysensors==0.25.0 +pymysensors==0.26.0 # homeassistant.components.iron_os pynecil==4.1.1 @@ -2051,7 +2054,7 @@ python-mpd2==3.1.1 python-mystrom==2.4.0 # homeassistant.components.open_router -python-open-router==0.3.0 +python-open-router==0.3.1 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 @@ -2209,7 +2212,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.4 +reolink-aio==0.14.5 # homeassistant.components.rflink rflink==0.0.67 @@ -2522,7 +2525,7 @@ venstarcolortouch==0.21 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.3 +voip-utils==0.3.4 # homeassistant.components.volvo volvocarsapi==0.4.1 @@ -2589,7 +2592,7 @@ wyoming==1.7.1 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.1.0 +xiaomi-ble==1.2.0 # homeassistant.components.knx xknx==3.8.0 @@ -2610,7 +2613,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==3.1.0 +yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale @@ -2629,7 +2632,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.06.09 +yt-dlp[default]==2025.07.21 # homeassistant.components.zamg zamg==0.3.6 @@ -2644,10 +2647,10 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.62 +zha==0.0.65 # homeassistant.components.zwave_js -zwave-js-server-python==0.67.0 +zwave-js-server-python==0.67.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/script/bootstrap b/script/bootstrap index e60342563ac..725cb856bbf 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,7 +4,7 @@ # Stop on errors set -e -cd "$(dirname "$0")/.." +cd "$(realpath "$(dirname "$0")/..")" echo "Installing development dependencies..." uv pip install wheel --constraint homeassistant/package_constraints.txt --upgrade diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 13bb3384258..b13f586439d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -235,10 +235,9 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py > 0.25.0 requires cargo 1.84.0 -# Stable Alpine current only ships cargo 1.83.0 +# rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 -rpds-py==0.24.0 +rpds-py==0.26.0 """ GENERATED_MESSAGE = ( diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5168388c934..5776f6dfe12 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==2.2.3 \ - home-assistant-intents==2025.6.23 \ + home-assistant-intents==2025.7.30 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 9c3f60a827c..9b5334823b9 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -30,6 +30,7 @@ PACKAGE_CHECK_VERSION_RANGE = { "bleak": "SemVer", "grpcio": "SemVer", "httpx": "SemVer", + "lxml": "SemVer", "mashumaro": "SemVer", "numpy": "SemVer", "pandas": "SemVer", @@ -42,6 +43,13 @@ PACKAGE_CHECK_VERSION_RANGE = { "urllib3": "SemVer", "yarl": "SemVer", } +PACKAGE_CHECK_PREPARE_UPDATE: dict[str, int] = { + # In the form dict("dependencyX": n+1) + # - dependencyX should be the name of the referenced dependency + # - current major version +1 + # Pandas will only fully support Python 3.14 in v3. + "pandas": 3, +} PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # In the form dict("domain": {"package": {"dependency1", "dependency2"}}) # - domain is the integration domain @@ -52,6 +60,10 @@ PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # geocachingapi > reverse_geocode > scipy > numpy "scipy": {"numpy"} }, + "noaa_tides": { + # https://github.com/GClunies/noaa_coops/pull/69 + "noaa-coops": {"pandas"} + }, } PACKAGE_REGEX = re.compile( @@ -567,7 +579,7 @@ def check_dependency_version_range( version == "Any" or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None or all( - _is_dependency_version_range_valid(version_part, convention) + _is_dependency_version_range_valid(version_part, convention, pkg) for version_part in version.split(";", 1)[0].split(",") ) ): @@ -581,22 +593,35 @@ def check_dependency_version_range( return False -def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool: +def _is_dependency_version_range_valid( + version_part: str, convention: str, pkg: str | None = None +) -> bool: + prepare_update = PACKAGE_CHECK_PREPARE_UPDATE.get(pkg) if pkg else None version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part.strip()) operator = version_match.group(1) version = version_match.group(2) + awesome = AwesomeVersion(version) if operator in (">", ">=", "!="): # Lower version binding and version exclusion are fine return True + if prepare_update is not None: + if operator in ("==", "~="): + # Only current major version allowed which prevents updates to the next one + return False + # Allow upper constraints for major version + 1 + if operator == "<" and awesome.section(0) < prepare_update + 1: + return False + if operator == "<=" and awesome.section(0) < prepare_update: + return False + if convention == "SemVer": if operator == "==": # Explicit version with wildcard is allowed only on major version # e.g. ==1.* is allowed, but ==1.2.* is not return version.endswith(".*") and version.count(".") == 1 - awesome = AwesomeVersion(version) if operator in ("<", "<="): # Upper version binding only allowed on major version # e.g. <=3 is allowed, but <=3.1 is not diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr index c7a11cb58df..9e311260693 100644 --- a/tests/components/acaia/snapshots/test_init.ambr +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'aa:bb:cc:dd:ee:ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Acaia', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Kitchen', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index b3181fddfeb..2a1e3dcc7fd 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '84fce612f5b8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'AirGradient', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) @@ -58,7 +56,6 @@ '84fce612f5b8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'AirGradient', @@ -68,7 +65,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '84fce612f5b8', - 'suggested_area': None, 'sw_version': '3.1.1', 'via_device_id': None, }) diff --git a/tests/components/airos/__init__.py b/tests/components/airos/__init__.py new file mode 100644 index 00000000000..8c6182a8650 --- /dev/null +++ b/tests/components/airos/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Ubiquity airOS integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py new file mode 100644 index 00000000000..b17908e801a --- /dev/null +++ b/tests/components/airos/conftest.py @@ -0,0 +1,61 @@ +"""Common fixtures for the Ubiquiti airOS tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from airos.airos8 import AirOSData +import pytest + +from homeassistant.components.airos.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def ap_fixture(): + """Load fixture data for AP mode.""" + json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN) + return AirOSData.from_dict(json_data) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.airos.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_airos_client( + request: pytest.FixtureRequest, ap_fixture: AirOSData +) -> Generator[AsyncMock]: + """Fixture to mock the AirOS API client.""" + with ( + patch( + "homeassistant.components.airos.config_flow.AirOS", autospec=True + ) as mock_airos, + patch("homeassistant.components.airos.coordinator.AirOS", new=mock_airos), + patch("homeassistant.components.airos.AirOS", new=mock_airos), + ): + client = mock_airos.return_value + client.status.return_value = ap_fixture + client.login.return_value = True + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the AirOS mocked config entry.""" + return MockConfigEntry( + title="NanoStation", + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_USERNAME: "ubnt", + }, + unique_id="01:23:45:67:89:AB", + ) diff --git a/tests/components/airos/fixtures/airos_ap-ptp.json b/tests/components/airos/fixtures/airos_ap-ptp.json new file mode 100644 index 00000000000..06d13ba1101 --- /dev/null +++ b/tests/components/airos/fixtures/airos_ap-ptp.json @@ -0,0 +1,300 @@ +{ + "chain_names": [ + { "number": 1, "name": "Chain 0" }, + { "number": 2, "name": "Chain 1" } + ], + "host": { + "hostname": "NanoStation 5AC ap name", + "device_id": "03aa0d0b40fed0a47088293584ef5432", + "uptime": 264888, + "power_time": 268683, + "time": "2025-06-23 23:06:42", + "timestamp": 2668313184, + "fwversion": "v8.7.17", + "devmodel": "NanoStation 5AC loco", + "netrole": "bridge", + "loadavg": 0.412598, + "totalram": 63447040, + "freeram": 16564224, + "temperature": 0, + "cpuload": 10.10101, + "height": 3 + }, + "genuine": "/images/genuine.png", + "services": { + "dhcpc": false, + "dhcpd": false, + "dhcp6d_stateful": false, + "pppoe": false, + "airview": 2 + }, + "firewall": { + "iptables": false, + "ebtables": false, + "ip6tables": false, + "eb6tables": false + }, + "portfw": false, + "wireless": { + "essid": "DemoSSID", + "mode": "ap-ptp", + "ieeemode": "11ACVHT80", + "band": 2, + "compat_11n": 0, + "hide_essid": 0, + "apmac": "01:23:45:67:89:AB", + "antenna_gain": 13, + "frequency": 5500, + "center1_freq": 5530, + "dfs": 1, + "distance": 0, + "security": "WPA2", + "noisef": -89, + "txpower": -3, + "aprepeater": false, + "rstatus": 5, + "chanbw": 80, + "rx_chainmask": 3, + "tx_chainmask": 3, + "nol_state": 0, + "nol_timeout": 0, + "cac_state": 0, + "cac_timeout": 0, + "rx_idx": 8, + "rx_nss": 2, + "tx_idx": 9, + "tx_nss": 2, + "throughput": { "tx": 222, "rx": 9907 }, + "service": { "time": 267181, "link": 266003 }, + "polling": { + "cb_capacity": 593970, + "dl_capacity": 647400, + "ul_capacity": 540540, + "use": 48, + "tx_use": 6, + "rx_use": 42, + "atpc_status": 2, + "fixed_frame": false, + "gps_sync": false, + "ff_cap_rep": false + }, + "count": 1, + "sta": [ + { + "mac": "01:23:45:67:89:AB", + "lastip": "192.168.1.2", + "signal": -59, + "rssi": 37, + "noisefloor": -89, + "chainrssi": [35, 32, 0], + "tx_idx": 9, + "rx_idx": 8, + "tx_nss": 2, + "rx_nss": 2, + "tx_latency": 0, + "distance": 1, + "tx_packets": 0, + "tx_lretries": 0, + "tx_sretries": 0, + "uptime": 170281, + "dl_signal_expect": -80, + "ul_signal_expect": -55, + "cb_capacity_expect": 416000, + "dl_capacity_expect": 208000, + "ul_capacity_expect": 624000, + "dl_rate_expect": 3, + "ul_rate_expect": 8, + "dl_linkscore": 100, + "ul_linkscore": 86, + "dl_avg_linkscore": 100, + "ul_avg_linkscore": 88, + "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], + "stats": { + "rx_bytes": 206938324814, + "rx_packets": 149767200, + "rx_pps": 846, + "tx_bytes": 5265602739, + "tx_packets": 52980390, + "tx_pps": 0 + }, + "airmax": { + "actual_priority": 0, + "beam": 0, + "desired_priority": 0, + "cb_capacity": 593970, + "dl_capacity": 647400, + "ul_capacity": 540540, + "atpc_status": 2, + "rx": { + "usage": 42, + "cinr": 31, + "evm": [ + [ + 31, 28, 33, 32, 32, 32, 31, 31, 31, 29, 30, 32, 30, 27, 34, 31, + 31, 30, 32, 29, 31, 29, 31, 33, 31, 31, 32, 30, 31, 34, 33, 31, + 30, 31, 30, 31, 31, 32, 31, 30, 33, 31, 30, 31, 27, 31, 30, 30, + 30, 30, 30, 29, 32, 34, 31, 30, 28, 30, 29, 35, 31, 33, 32, 29 + ], + [ + 34, 34, 35, 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, + 34, 34, 35, 34, 33, 33, 35, 34, 34, 35, 34, 35, 34, 34, 35, 34, + 34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34, + 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35 + ] + ] + }, + "tx": { + "usage": 6, + "cinr": 31, + "evm": [ + [ + 32, 34, 28, 33, 35, 30, 31, 33, 30, 30, 32, 30, 29, 33, 31, 29, + 33, 31, 31, 30, 33, 34, 33, 31, 33, 32, 32, 31, 29, 31, 30, 32, + 31, 30, 29, 32, 31, 32, 31, 31, 32, 29, 31, 29, 30, 32, 32, 31, + 32, 32, 33, 31, 28, 29, 31, 31, 33, 32, 33, 32, 32, 32, 31, 33 + ], + [ + 37, 37, 37, 38, 38, 37, 36, 38, 38, 37, 37, 37, 37, 37, 39, 37, + 37, 37, 37, 37, 37, 36, 37, 37, 37, 37, 37, 37, 37, 38, 37, 37, + 38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37, + 37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37 + ] + ] + } + }, + "last_disc": 1, + "remote": { + "age": 1, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "hostname": "NanoStation 5AC sta name", + "platform": "NanoStation 5AC loco", + "version": "WA.ar934x.v8.7.17.48152.250620.2132", + "time": "2025-06-23 23:13:54", + "cpuload": 43.564301, + "temperature": 0, + "totalram": 63447040, + "freeram": 14290944, + "netrole": "bridge", + "mode": "sta-ptp", + "sys_id": "0xe7fa", + "tx_throughput": 16023, + "rx_throughput": 251, + "uptime": 265320, + "power_time": 268512, + "compat_11n": 0, + "signal": -58, + "rssi": 38, + "noisefloor": -90, + "tx_power": -4, + "distance": 1, + "rx_chainmask": 3, + "chainrssi": [33, 37, 0], + "tx_ratedata": [ + 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 + ], + "tx_bytes": 212308148210, + "rx_bytes": 3624206478, + "antenna_gain": 13, + "cable_loss": 0, + "height": 2, + "ethlist": [ + { + "ifname": "eth0", + "enabled": true, + "plugged": true, + "duplex": true, + "speed": 1000, + "snr": [30, 30, 29, 30], + "cable_len": 14 + } + ], + "ipaddr": ["192.168.1.2"], + "ip6addr": ["fe80::eea:14ff:fea4:89ab"], + "gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 }, + "oob": false, + "unms": { "status": 0, "timestamp": null }, + "airview": 2, + "service": { "time": 267195, "link": 265996 } + }, + "airos_connected": true + } + ], + "sta_disconnected": [] + }, + "interfaces": [ + { + "ifname": "eth0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 209900085624, + "rx_bytes": 3984971949, + "tx_packets": 185866883, + "rx_packets": 73564835, + "tx_errors": 0, + "rx_errors": 4, + "tx_dropped": 10, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 1000, + "duplex": true, + "snr": [30, 30, 30, 30], + "cable_len": 18, + "ip6addr": null + } + }, + { + "ifname": "ath0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": false, + "tx_bytes": 5265602738, + "rx_bytes": 206938324766, + "tx_packets": 52980390, + "rx_packets": 149767200, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 2005, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 0, + "duplex": false, + "snr": null, + "cable_len": null, + "ip6addr": null + } + }, + { + "ifname": "br0", + "hwaddr": "01:23:45:67:89:AB", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 236295176, + "rx_bytes": 204802727, + "tx_packets": 298119, + "rx_packets": 1791592, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "192.168.1.2", + "speed": 0, + "duplex": false, + "snr": null, + "cable_len": null, + "ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }] + } + } + ], + "provmode": {}, + "ntpclient": {}, + "unms": { "status": 0, "timestamp": null }, + "gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 }, + "derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" } +} diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..bc2dedc905a --- /dev/null +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -0,0 +1,623 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'chain_names': list([ + dict({ + 'name': 'Chain 0', + 'number': 1, + }), + dict({ + 'name': 'Chain 1', + 'number': 2, + }), + ]), + 'derived': dict({ + 'mac': '**REDACTED**', + 'mac_interface': 'br0', + }), + 'firewall': dict({ + 'eb6tables': False, + 'ebtables': False, + 'ip6tables': False, + 'iptables': False, + }), + 'genuine': '/images/genuine.png', + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'host': dict({ + 'cpuload': 10.10101, + 'device_id': '03aa0d0b40fed0a47088293584ef5432', + 'devmodel': 'NanoStation 5AC loco', + 'freeram': 16564224, + 'fwversion': 'v8.7.17', + 'height': 3, + 'hostname': '**REDACTED**', + 'loadavg': 0.412598, + 'netrole': 'bridge', + 'power_time': 268683, + 'temperature': 0, + 'time': '2025-06-23 23:06:42', + 'timestamp': 2668313184, + 'totalram': 63447040, + 'uptime': 264888, + }), + 'interfaces': list([ + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'eth0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': 18, + 'duplex': True, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 3984971949, + 'rx_dropped': 0, + 'rx_errors': 4, + 'rx_packets': 73564835, + 'snr': list([ + 30, + 30, + 30, + 30, + ]), + 'speed': 1000, + 'tx_bytes': 209900085624, + 'tx_dropped': 10, + 'tx_errors': 0, + 'tx_packets': 185866883, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'ath0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': None, + 'ipaddr': '**REDACTED**', + 'plugged': False, + 'rx_bytes': 206938324766, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 149767200, + 'snr': None, + 'speed': 0, + 'tx_bytes': 5265602738, + 'tx_dropped': 2005, + 'tx_errors': 0, + 'tx_packets': 52980390, + }), + }), + dict({ + 'enabled': True, + 'hwaddr': '**REDACTED**', + 'ifname': 'br0', + 'mtu': 1500, + 'status': dict({ + 'cable_len': None, + 'duplex': False, + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'plugged': True, + 'rx_bytes': 204802727, + 'rx_dropped': 0, + 'rx_errors': 0, + 'rx_packets': 1791592, + 'snr': None, + 'speed': 0, + 'tx_bytes': 236295176, + 'tx_dropped': 0, + 'tx_errors': 0, + 'tx_packets': 298119, + }), + }), + ]), + 'ntpclient': dict({ + }), + 'portfw': False, + 'provmode': dict({ + }), + 'services': dict({ + 'airview': 2, + 'dhcp6d_stateful': False, + 'dhcpc': False, + 'dhcpd': False, + 'pppoe': False, + }), + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'wireless': dict({ + 'antenna_gain': 13, + 'apmac': '**REDACTED**', + 'aprepeater': False, + 'band': 2, + 'cac_state': 0, + 'cac_timeout': 0, + 'center1_freq': 5530, + 'chanbw': 80, + 'compat_11n': 0, + 'count': 1, + 'dfs': 1, + 'distance': 0, + 'essid': '**REDACTED**', + 'frequency': 5500, + 'hide_essid': 0, + 'ieeemode': '11ACVHT80', + 'mode': 'ap-ptp', + 'noisef': -89, + 'nol_state': 0, + 'nol_timeout': 0, + 'polling': dict({ + 'atpc_status': 2, + 'cb_capacity': 593970, + 'dl_capacity': 647400, + 'ff_cap_rep': False, + 'fixed_frame': False, + 'gps_sync': False, + 'rx_use': 42, + 'tx_use': 6, + 'ul_capacity': 540540, + 'use': 48, + }), + 'rstatus': 5, + 'rx_chainmask': 3, + 'rx_idx': 8, + 'rx_nss': 2, + 'security': 'WPA2', + 'service': dict({ + 'link': 266003, + 'time': 267181, + }), + 'sta': list([ + dict({ + 'airmax': dict({ + 'actual_priority': 0, + 'atpc_status': 2, + 'beam': 0, + 'cb_capacity': 593970, + 'desired_priority': 0, + 'dl_capacity': 647400, + 'rx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 31, + 28, + 33, + 32, + 32, + 32, + 31, + 31, + 31, + 29, + 30, + 32, + 30, + 27, + 34, + 31, + 31, + 30, + 32, + 29, + 31, + 29, + 31, + 33, + 31, + 31, + 32, + 30, + 31, + 34, + 33, + 31, + 30, + 31, + 30, + 31, + 31, + 32, + 31, + 30, + 33, + 31, + 30, + 31, + 27, + 31, + 30, + 30, + 30, + 30, + 30, + 29, + 32, + 34, + 31, + 30, + 28, + 30, + 29, + 35, + 31, + 33, + 32, + 29, + ]), + list([ + 34, + 34, + 35, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 35, + 34, + 33, + 33, + 35, + 34, + 34, + 35, + 34, + 35, + 34, + 34, + 35, + 34, + 34, + 33, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + 35, + 34, + 35, + 33, + 34, + 34, + 34, + 34, + 35, + 35, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 34, + 35, + 35, + ]), + ]), + 'usage': 42, + }), + 'tx': dict({ + 'cinr': 31, + 'evm': list([ + list([ + 32, + 34, + 28, + 33, + 35, + 30, + 31, + 33, + 30, + 30, + 32, + 30, + 29, + 33, + 31, + 29, + 33, + 31, + 31, + 30, + 33, + 34, + 33, + 31, + 33, + 32, + 32, + 31, + 29, + 31, + 30, + 32, + 31, + 30, + 29, + 32, + 31, + 32, + 31, + 31, + 32, + 29, + 31, + 29, + 30, + 32, + 32, + 31, + 32, + 32, + 33, + 31, + 28, + 29, + 31, + 31, + 33, + 32, + 33, + 32, + 32, + 32, + 31, + 33, + ]), + list([ + 37, + 37, + 37, + 38, + 38, + 37, + 36, + 38, + 38, + 37, + 37, + 37, + 37, + 37, + 39, + 37, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 37, + 37, + 38, + 37, + 38, + 37, + 37, + 37, + 37, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 38, + 37, + 37, + 38, + 37, + 36, + 37, + 37, + 37, + 37, + 37, + 37, + 37, + ]), + ]), + 'usage': 6, + }), + 'ul_capacity': 540540, + }), + 'airos_connected': True, + 'cb_capacity_expect': 416000, + 'chainrssi': list([ + 35, + 32, + 0, + ]), + 'distance': 1, + 'dl_avg_linkscore': 100, + 'dl_capacity_expect': 208000, + 'dl_linkscore': 100, + 'dl_rate_expect': 3, + 'dl_signal_expect': -80, + 'last_disc': 1, + 'lastip': '**REDACTED**', + 'mac': '**REDACTED**', + 'noisefloor': -89, + 'remote': dict({ + 'age': 1, + 'airview': 2, + 'antenna_gain': 13, + 'cable_loss': 0, + 'chainrssi': list([ + 33, + 37, + 0, + ]), + 'compat_11n': 0, + 'cpuload': 43.564301, + 'device_id': 'd4f4cdf82961e619328a8f72f8d7653b', + 'distance': 1, + 'ethlist': list([ + dict({ + 'cable_len': 14, + 'duplex': True, + 'enabled': True, + 'ifname': 'eth0', + 'plugged': True, + 'snr': list([ + 30, + 30, + 29, + 30, + ]), + 'speed': 1000, + }), + ]), + 'freeram': 14290944, + 'gps': dict({ + 'fix': 0, + 'lat': '**REDACTED**', + 'lon': '**REDACTED**', + }), + 'height': 2, + 'hostname': '**REDACTED**', + 'ip6addr': '**REDACTED**', + 'ipaddr': '**REDACTED**', + 'mode': 'sta-ptp', + 'netrole': 'bridge', + 'noisefloor': -90, + 'oob': False, + 'platform': 'NanoStation 5AC loco', + 'power_time': 268512, + 'rssi': 38, + 'rx_bytes': 3624206478, + 'rx_chainmask': 3, + 'rx_throughput': 251, + 'service': dict({ + 'link': 265996, + 'time': 267195, + }), + 'signal': -58, + 'sys_id': '0xe7fa', + 'temperature': 0, + 'time': '2025-06-23 23:13:54', + 'totalram': 63447040, + 'tx_bytes': 212308148210, + 'tx_power': -4, + 'tx_ratedata': list([ + 14, + 4, + 372, + 2223, + 4708, + 4037, + 8142, + 485763, + 29420892, + 24748154, + ]), + 'tx_throughput': 16023, + 'unms': dict({ + 'status': 0, + 'timestamp': None, + }), + 'uptime': 265320, + 'version': 'WA.ar934x.v8.7.17.48152.250620.2132', + }), + 'rssi': 37, + 'rx_idx': 8, + 'rx_nss': 2, + 'signal': -59, + 'stats': dict({ + 'rx_bytes': 206938324814, + 'rx_packets': 149767200, + 'rx_pps': 846, + 'tx_bytes': 5265602739, + 'tx_packets': 52980390, + 'tx_pps': 0, + }), + 'tx_idx': 9, + 'tx_latency': 0, + 'tx_lretries': 0, + 'tx_nss': 2, + 'tx_packets': 0, + 'tx_ratedata': list([ + 175, + 4, + 47, + 200, + 673, + 158, + 163, + 138, + 68895, + 19577430, + ]), + 'tx_sretries': 0, + 'ul_avg_linkscore': 88, + 'ul_capacity_expect': 624000, + 'ul_linkscore': 86, + 'ul_rate_expect': 8, + 'ul_signal_expect': -55, + 'uptime': 170281, + }), + ]), + 'sta_disconnected': list([ + ]), + 'throughput': dict({ + 'rx': 9907, + 'tx': 222, + }), + 'tx_chainmask': 3, + 'tx_idx': 9, + 'tx_nss': 2, + 'txpower': -3, + }), + }), + 'entry_data': dict({ + 'host': '**REDACTED**', + 'password': '**REDACTED**', + 'username': 'ubnt', + }), + }) +# --- diff --git a/tests/components/airos/snapshots/test_sensor.ambr b/tests/components/airos/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a92d2dc35a2 --- /dev/null +++ b/tests/components/airos/snapshots/test_sensor.ambr @@ -0,0 +1,547 @@ +# serializer version: 1 +# name: test_all_entities[sensor.nanostation_5ac_ap_name_antenna_gain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_antenna_gain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Antenna gain', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_antenna_gain', + 'unique_id': '01:23:45:67:89:AB_wireless_antenna_gain', + 'unit_of_measurement': 'dB', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_antenna_gain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'NanoStation 5AC ap name Antenna gain', + 'state_class': , + 'unit_of_measurement': 'dB', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_antenna_gain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_cpu_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_cpu_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU load', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'host_cpuload', + 'unique_id': '01:23:45:67:89:AB_host_cpuload', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_cpu_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name CPU load', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_cpu_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.10101', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_download_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_download_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Download capacity', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_polling_dl_capacity', + 'unique_id': '01:23:45:67:89:AB_wireless_polling_dl_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_download_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Download capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_download_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '647400', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_network_role-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'bridge', + 'router', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_network_role', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Network role', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'host_netrole', + 'unique_id': '01:23:45:67:89:AB_host_netrole', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_network_role-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Network role', + 'options': list([ + 'bridge', + 'router', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_network_role', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bridge', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_receive_actual-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_receive_actual', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Throughput receive (actual)', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_throughput_rx', + 'unique_id': '01:23:45:67:89:AB_wireless_throughput_rx', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_receive_actual-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Throughput receive (actual)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_receive_actual', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9907', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_transmit_actual', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Throughput transmit (actual)', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_throughput_tx', + 'unique_id': '01:23:45:67:89:AB_wireless_throughput_tx', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Throughput transmit (actual)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_transmit_actual', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '222', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_upload_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upload capacity', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_polling_ul_capacity', + 'unique_id': '01:23:45:67:89:AB_wireless_polling_ul_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'NanoStation 5AC ap name Upload capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_upload_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '540540', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless frequency', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_frequency', + 'unique_id': '01:23:45:67:89:AB_wireless_frequency', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'NanoStation 5AC ap name Wireless frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5500', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ap_ptp', + 'sta_ptp', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless mode', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_mode', + 'unique_id': '01:23:45:67:89:AB_wireless_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Wireless mode', + 'options': list([ + 'ap_ptp', + 'sta_ptp', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ap_ptp', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wireless SSID', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_essid', + 'unique_id': '01:23:45:67:89:AB_wireless_essid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name Wireless SSID', + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'DemoSSID', + }) +# --- diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py new file mode 100644 index 00000000000..9d2a6376732 --- /dev/null +++ b/tests/components/airos/test_config_flow.py @@ -0,0 +1,119 @@ +"""Test the Ubiquiti airOS config flow.""" + +from typing import Any +from unittest.mock import AsyncMock + +from airos.exceptions import ( + ConnectionAuthenticationError, + DeviceConnectionError, + KeyDataMissingError, +) +import pytest + +from homeassistant.components.airos.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "test-password", +} + + +async def test_form_creates_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airos_client: AsyncMock, + ap_fixture: dict[str, Any], +) -> None: + """Test we get the form and create the appropriate entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "NanoStation 5AC ap name" + assert result["result"].unique_id == "01:23:45:67:89:AB" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicate_entry( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test the form does not allow duplicate entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (ConnectionAuthenticationError, "invalid_auth"), + (DeviceConnectionError, "cannot_connect"), + (KeyDataMissingError, "key_data_missing"), + (Exception, "unknown"), + ], +) +async def test_form_exception_handling( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airos_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test we handle exceptions.""" + mock_airos_client.login.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_airos_client.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "NanoStation 5AC ap name" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/airos/test_diagnostics.py b/tests/components/airos/test_diagnostics.py new file mode 100644 index 00000000000..453e8ff1f03 --- /dev/null +++ b/tests/components/airos/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Diagnostic tests for airOS.""" + +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.airos.coordinator import AirOSData +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_airos_client: MagicMock, + mock_config_entry: MockConfigEntry, + ap_fixture: AirOSData, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + await setup_integration(hass, mock_config_entry) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py new file mode 100644 index 00000000000..561741b1a2b --- /dev/null +++ b/tests/components/airos/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Ubiquiti airOS sensors.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from airos.exceptions import ( + ConnectionAuthenticationError, + DataMissingError, + DeviceConnectionError, +) +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.airos.const import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("exception"), + [ + ConnectionAuthenticationError, + TimeoutError, + DeviceConnectionError, + DataMissingError, + ], +) +async def test_sensor_update_exception_handling( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + freezer: FrozenDateTimeFactory, +) -> None: + """Test entity update data handles exceptions.""" + await setup_integration(hass, mock_config_entry) + + expected_entity_id = "sensor.nanostation_5ac_ap_name_antenna_gain" + signal_state = hass.states.get(expected_entity_id) + + assert signal_state.state == "13", f"Expected state 13, got {signal_state.state}" + assert signal_state.attributes.get("unit_of_measurement") == "dB", ( + f"Expected unit 'dB', got {signal_state.attributes.get('unit_of_measurement')}" + ) + + mock_airos_client.login.side_effect = exception + + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds() + 1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + signal_state = hass.states.get(expected_entity_id) + + assert signal_state.state == STATE_UNAVAILABLE, ( + f"Expected state {STATE_UNAVAILABLE}, got {signal_state.state}" + ) + + mock_airos_client.login.side_effect = None + + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds())) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + signal_state = hass.states.get(expected_entity_id) + assert signal_state.state == "13", f"Expected state 13, got {signal_state.state}" diff --git a/tests/components/airq/common.py b/tests/components/airq/common.py new file mode 100644 index 00000000000..275da60e3a2 --- /dev/null +++ b/tests/components/airq/common.py @@ -0,0 +1,18 @@ +"""Common methods used across tests for air-Q.""" + +from aioairq import DeviceInfo + +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD + +TEST_USER_DATA = { + CONF_IP_ADDRESS: "192.168.0.0", + CONF_PASSWORD: "password", +} +TEST_DEVICE_INFO = DeviceInfo( + id="id", + name="name", + model="model", + sw_version="sw", + hw_version="hw", +) +TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"} diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 09da6343e05..95c22cb12c8 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -3,7 +3,7 @@ import logging from unittest.mock import patch -from aioairq import DeviceInfo, InvalidAuth +from aioairq import InvalidAuth from aiohttp.client_exceptions import ClientConnectionError import pytest @@ -13,25 +13,16 @@ from homeassistant.components.airq.const import ( CONF_RETURN_AVERAGE, DOMAIN, ) -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .common import TEST_DEVICE_INFO, TEST_USER_DATA + from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") -TEST_USER_DATA = { - CONF_IP_ADDRESS: "192.168.0.0", - CONF_PASSWORD: "password", -} -TEST_DEVICE_INFO = DeviceInfo( - id="id", - name="name", - model="model", - sw_version="sw", - hw_version="hw", -) DEFAULT_OPTIONS = { CONF_CLIP_NEGATIVE: True, CONF_RETURN_AVERAGE: True, diff --git a/tests/components/airq/test_coordinator.py b/tests/components/airq/test_coordinator.py index 69f7c9dee17..6512d60ddbe 100644 --- a/tests/components/airq/test_coordinator.py +++ b/tests/components/airq/test_coordinator.py @@ -3,7 +3,6 @@ import logging from unittest.mock import patch -from aioairq import DeviceInfo as AirQDeviceInfo import pytest from homeassistant.components.airq import AirQCoordinator @@ -12,9 +11,10 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo +from .common import TEST_DEVICE_DATA, TEST_DEVICE_INFO + from tests.common import MockConfigEntry -pytestmark = pytest.mark.usefixtures("mock_setup_entry") MOCKED_ENTRY = MockConfigEntry( domain=DOMAIN, data={ @@ -24,14 +24,6 @@ MOCKED_ENTRY = MockConfigEntry( unique_id="123-456", ) -TEST_DEVICE_INFO = AirQDeviceInfo( - id="id", - name="name", - model="model", - sw_version="sw", - hw_version="hw", -) -TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"} STATUS_WARMUP = { "co": "co sensor still in warm up phase; waiting time = 18 s", "tvoc": "tvoc sensor still in warm up phase; waiting time = 18 s", diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index a5a49a343a9..22596706862 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -69,6 +69,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( device.device_type ) + client.send_sound_notification = AsyncMock() yield client diff --git a/tests/components/alexa_devices/const.py b/tests/components/alexa_devices/const.py index 8a2f5b6b158..6a4dff1c38d 100644 --- a/tests/components/alexa_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -5,3 +5,5 @@ TEST_COUNTRY = "IT" TEST_PASSWORD = "fake_password" TEST_SERIAL_NUMBER = "echo_test_serial_number" TEST_USERNAME = "fake_email@gmail.com" + +TEST_DEVICE_ID = "echo_test_device_id" diff --git a/tests/components/alexa_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr index e0460c4c173..bf28f8fb1a1 100644 --- a/tests/components/alexa_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'echo_test_serial_number', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Amazon', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'echo_test_serial_number', - 'suggested_area': None, 'sw_version': 'echo_test_software_version', 'via_device_id': None, }) diff --git a/tests/components/alexa_devices/snapshots/test_services.ambr b/tests/components/alexa_devices/snapshots/test_services.ambr new file mode 100644 index 00000000000..b95108b0d03 --- /dev/null +++ b/tests/components/alexa_devices/snapshots/test_services.ambr @@ -0,0 +1,77 @@ +# serializer version: 1 +# name: test_send_sound_service + _Call( + tuple( + dict({ + 'account_name': 'Echo Test', + 'appliance_id': 'G1234567890123456789012345678A', + 'bluetooth_state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device_cluster_members': list([ + 'echo_test_serial_number', + ]), + 'device_family': 'mine', + 'device_locale': 'en-US', + 'device_owner_customer_id': 'amazon_ower_id', + 'device_type': 'echo', + 'do_not_disturb': False, + 'entity_id': '11111111-2222-3333-4444-555555555555', + 'online': True, + 'response_style': None, + 'sensors': dict({ + 'temperature': dict({ + 'name': 'temperature', + 'scale': 'CELSIUS', + 'value': '22.5', + }), + }), + 'serial_number': 'echo_test_serial_number', + 'software_version': 'echo_test_software_version', + }), + 'chimes_bells_01', + ), + dict({ + }), + ) +# --- +# name: test_send_text_service + _Call( + tuple( + dict({ + 'account_name': 'Echo Test', + 'appliance_id': 'G1234567890123456789012345678A', + 'bluetooth_state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device_cluster_members': list([ + 'echo_test_serial_number', + ]), + 'device_family': 'mine', + 'device_locale': 'en-US', + 'device_owner_customer_id': 'amazon_ower_id', + 'device_type': 'echo', + 'do_not_disturb': False, + 'entity_id': '11111111-2222-3333-4444-555555555555', + 'online': True, + 'response_style': None, + 'sensors': dict({ + 'temperature': dict({ + 'name': 'temperature', + 'scale': 'CELSIUS', + 'value': '22.5', + }), + }), + 'serial_number': 'echo_test_serial_number', + 'software_version': 'echo_test_software_version', + }), + 'Play B.B.C. radio on TuneIn', + ), + dict({ + }), + ) +# --- diff --git a/tests/components/alexa_devices/test_services.py b/tests/components/alexa_devices/test_services.py new file mode 100644 index 00000000000..914664199c2 --- /dev/null +++ b/tests/components/alexa_devices/test_services.py @@ -0,0 +1,195 @@ +"""Tests for Alexa Devices services.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.alexa_devices.const import DOMAIN +from homeassistant.components.alexa_devices.services import ( + ATTR_SOUND, + ATTR_SOUND_VARIANT, + ATTR_TEXT_COMMAND, + SERVICE_SOUND_NOTIFICATION, + SERVICE_TEXT_COMMAND, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import TEST_DEVICE_ID, TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, mock_device_registry + + +async def test_setup_services( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setup of Alexa Devices services.""" + await setup_integration(hass, mock_config_entry) + + assert (services := hass.services.async_services_for_domain(DOMAIN)) + assert SERVICE_TEXT_COMMAND in services + assert SERVICE_SOUND_NOTIFICATION in services + + +async def test_send_sound_service( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test send sound service.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: "chimes_bells", + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert mock_amazon_devices_client.call_alexa_sound.call_count == 1 + assert mock_amazon_devices_client.call_alexa_sound.call_args == snapshot + + +async def test_send_text_service( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test send text service.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.services.async_call( + DOMAIN, + SERVICE_TEXT_COMMAND, + { + ATTR_TEXT_COMMAND: "Play B.B.C. radio on TuneIn", + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert mock_amazon_devices_client.call_alexa_text_command.call_count == 1 + assert mock_amazon_devices_client.call_alexa_text_command.call_args == snapshot + + +@pytest.mark.parametrize( + ("sound", "device_id", "translation_key", "translation_placeholders"), + [ + ( + "chimes_bells", + "fake_device_id", + "invalid_device_id", + {"device_id": "fake_device_id"}, + ), + ( + "wrong_sound_name", + TEST_DEVICE_ID, + "invalid_sound_value", + { + "sound": "wrong_sound_name", + "variant": "1", + }, + ), + ], +) +async def test_invalid_parameters( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sound: str, + device_id: str, + translation_key: str, + translation_placeholders: dict[str, str], +) -> None: + """Test invalid service parameters.""" + + device_entry = dr.DeviceEntry( + id=TEST_DEVICE_ID, identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + mock_device_registry( + hass, + {device_entry.id: device_entry}, + ) + await setup_integration(hass, mock_config_entry) + + # Call Service + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: sound, + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_id, + }, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == translation_key + assert exc_info.value.translation_placeholders == translation_placeholders + + +async def test_config_entry_not_loaded( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry not loaded.""" + + await setup_integration(hass, mock_config_entry) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + # Call Service + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + DOMAIN, + SERVICE_SOUND_NOTIFICATION, + { + ATTR_SOUND: "chimes_bells", + ATTR_SOUND_VARIANT: 1, + ATTR_DEVICE_ID: device_entry.id, + }, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "entry_not_loaded" + assert exc_info.value.translation_placeholders == {"entry": mock_config_entry.title} diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 90f3049d8fd..0e14d556620 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1051,7 +1051,6 @@ async def test_devices_payload( "hw_version": "test-hw-version", "integration": "hue", "is_custom_integration": False, - "has_suggested_area": True, "has_configuration_url": True, "via_device": None, }, @@ -1063,7 +1062,6 @@ async def test_devices_payload( "hw_version": None, "integration": "hue", "is_custom_integration": False, - "has_suggested_area": False, "has_configuration_url": False, "via_device": 0, }, diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index e647b7fa6a5..c4c1b0b1b93 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -17,7 +17,6 @@ 'junctionId', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'A. O. Smith', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'serial', - 'suggested_area': 'Basement', 'sw_version': '2.14', 'via_device_id': None, }) diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr index 39f28b528fc..414c3e451fd 100644 --- a/tests/components/apcupsd/snapshots/test_init.ambr +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'XXXXXXXXXXXX', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.14.14 (31 May 2016) unknown', 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'XXXX', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -83,7 +79,6 @@ 'mocked-config-entry-id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -116,7 +110,6 @@ 'mocked-config-entry-id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'APC', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 5bc7b86c38c..0cb67302700 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -375,7 +375,7 @@ async def test_get_pipelines(hass: HomeAssistant) -> None: ("en", "us", "en", "en"), ("en", "uk", "en", "en"), ("pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt"), + ("pt", "br", "pt-BR", "pt"), ], ) async def test_default_pipeline_no_stt_tts( @@ -428,7 +428,7 @@ async def test_default_pipeline_no_stt_tts( ("en", "us", "en", "en", "en", "en"), ("en", "uk", "en", "en", "en", "en"), ("pt", "pt", "pt", "pt", "pt", "pt"), - ("pt", "br", "pt-br", "pt", "pt-br", "pt-br"), + ("pt", "br", "pt-BR", "pt", "pt-br", "pt-br"), ], ) @pytest.mark.usefixtures("init_supporting_components") diff --git a/tests/components/august/snapshots/test_binary_sensor.ambr b/tests/components/august/snapshots/test_binary_sensor.ambr index be5947372f5..9d94ae9ffdc 100644 --- a/tests/components/august/snapshots/test_binary_sensor.ambr +++ b/tests/components/august/snapshots/test_binary_sensor.ambr @@ -17,7 +17,6 @@ 'tmt100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'August Home Inc.', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/august/snapshots/test_lock.ambr b/tests/components/august/snapshots/test_lock.ambr index 0a594fed1ee..8af45cae68c 100644 --- a/tests/components/august/snapshots/test_lock.ambr +++ b/tests/components/august/snapshots/test_lock.ambr @@ -21,7 +21,6 @@ 'online_with_doorsense', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'August Home Inc.', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/components/axis/snapshots/test_hub.ambr b/tests/components/axis/snapshots/test_hub.ambr index 9e407bfef0b..663c52dd36c 100644 --- a/tests/components/axis/snapshots/test_hub.ambr +++ b/tests/components/axis/snapshots/test_hub.ambr @@ -21,7 +21,6 @@ '00:40:8c:12:34:56', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Axis Communications AB', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.10.1', 'via_device_id': None, }) @@ -58,7 +56,6 @@ '00:40:8c:12:34:56', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Axis Communications AB', @@ -68,7 +65,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '00:40:8c:12:34:56', - 'suggested_area': None, 'sw_version': '9.80.1', 'via_device_id': None, }) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 0aaff0edfe7..c8ced85c933 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -11,7 +11,11 @@ from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from .common import ( @@ -202,7 +206,9 @@ async def test_old_identifiers_are_removed( async def test_smart_by_bond_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a smart by bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -241,11 +247,13 @@ async def test_smart_by_bond_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None - assert device.suggested_area == "Den" + assert device.area_id == area_registry.async_get_area_by_name("Den").id async def test_bridge_device_suggested_area( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we can setup a bridge bond device and get the suggested area.""" config_entry = MockConfigEntry( @@ -289,7 +297,7 @@ async def test_bridge_device_suggested_area( device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id async def test_device_remove_devices( diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 72360ece687..a06131f7216 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -3,11 +3,11 @@ from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock -from bsblan import BSBLANConnectionError +from bsblan import BSBLANAuthError, BSBLANConnectionError, BSBLANError import pytest from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -129,7 +129,7 @@ async def test_full_user_flow_implementation( result = await _init_user_flow(hass) _assert_form_result(result, "user") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -142,7 +142,7 @@ async def test_full_user_flow_implementation( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "127.0.0.1", @@ -185,6 +185,94 @@ async def test_connection_error( _assert_form_result(result, "user", {"base": "cannot_connect"}) +async def test_authentication_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test we show user form on BSBLan authentication error with field preservation.""" + mock_bsblan.device.side_effect = BSBLANAuthError + + user_input = { + CONF_HOST: "192.168.1.100", + CONF_PORT: 8080, + CONF_PASSKEY: "secret", + CONF_USERNAME: "testuser", + CONF_PASSWORD: "wrongpassword", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": "invalid_auth"} + assert result.get("step_id") == "user" + + # Verify that user input is preserved in the form + data_schema = result.get("data_schema") + assert data_schema is not None + + # Check that the form fields contain the previously entered values + host_field = next( + field for field in data_schema.schema if field.schema == CONF_HOST + ) + port_field = next( + field for field in data_schema.schema if field.schema == CONF_PORT + ) + passkey_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSKEY + ) + username_field = next( + field for field in data_schema.schema if field.schema == CONF_USERNAME + ) + password_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSWORD + ) + + # The defaults are callable functions, so we need to call them + assert host_field.default() == "192.168.1.100" + assert port_field.default() == 8080 + assert passkey_field.default() == "secret" + assert username_field.default() == "testuser" + assert password_field.default() == "wrongpassword" + + +async def test_authentication_error_vs_connection_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test that authentication and connection errors are handled differently.""" + # Test connection error first + mock_bsblan.device.side_effect = BSBLANConnectionError + + result = await _init_user_flow( + hass, + { + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + }, + ) + + _assert_form_result(result, "user", {"base": "cannot_connect"}) + + # Reset and test authentication error + mock_bsblan.device.side_effect = BSBLANAuthError + + result = await _init_user_flow( + hass, + { + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_USERNAME: "admin", + CONF_PASSWORD: "wrongpass", + }, + ) + + _assert_form_result(result, "user", {"base": "invalid_auth"}) + + async def test_user_device_exists_abort( hass: HomeAssistant, mock_bsblan: MagicMock, @@ -217,7 +305,7 @@ async def test_zeroconf_discovery( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -228,7 +316,7 @@ async def test_zeroconf_discovery( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "10.0.2.60", @@ -285,7 +373,7 @@ async def test_zeroconf_discovery_no_mac_requires_auth( # Reset side_effect for the second call to succeed mock_bsblan.device.side_effect = None - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -295,7 +383,7 @@ async def test_zeroconf_discovery_no_mac_requires_auth( ) _assert_create_entry_result( - result2, + result, "00:80:41:19:69:90", # MAC from fixture file { CONF_HOST: "10.0.2.60", @@ -324,10 +412,10 @@ async def test_zeroconf_discovery_no_mac_no_auth_required( _assert_form_result(result, "discovery_confirm") # User confirms the discovery - result2 = await _configure_flow(hass, result["flow_id"], {}) + result = await _configure_flow(hass, result["flow_id"], {}) _assert_create_entry_result( - result2, + result, "00:80:41:19:69:90", # MAC from fixture file { CONF_HOST: "10.0.2.60", @@ -355,7 +443,7 @@ async def test_zeroconf_discovery_connection_error( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -365,7 +453,7 @@ async def test_zeroconf_discovery_connection_error( }, ) - _assert_form_result(result2, "discovery_confirm", {"base": "cannot_connect"}) + _assert_form_result(result, "discovery_confirm", {"base": "cannot_connect"}) async def test_zeroconf_discovery_updates_host_port_on_existing_entry( @@ -445,7 +533,7 @@ async def test_zeroconf_discovery_connection_error_recovery( result = await _init_zeroconf_flow(hass, zeroconf_discovery_info) _assert_form_result(result, "discovery_confirm") - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -455,12 +543,12 @@ async def test_zeroconf_discovery_connection_error_recovery( }, ) - _assert_form_result(result2, "discovery_confirm", {"base": "cannot_connect"}) + _assert_form_result(result, "discovery_confirm", {"base": "cannot_connect"}) # Second attempt succeeds (connection is fixed) mock_bsblan.device.side_effect = None - result3 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -471,7 +559,7 @@ async def test_zeroconf_discovery_connection_error_recovery( ) _assert_create_entry_result( - result3, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "10.0.2.60", @@ -513,7 +601,7 @@ async def test_connection_error_recovery( # Second attempt succeeds (connection is fixed) mock_bsblan.device.side_effect = None - result2 = await _configure_flow( + result = await _configure_flow( hass, result["flow_id"], { @@ -526,7 +614,7 @@ async def test_connection_error_recovery( ) _assert_create_entry_result( - result2, + result, format_mac("00:80:41:19:69:90"), { CONF_HOST: "127.0.0.1", @@ -567,3 +655,407 @@ async def test_zeroconf_discovery_no_mac_duplicate_host_port( # Should not call device API since we abort early assert len(mock_bsblan.device.mock_calls) == 0 + + +async def test_reauth_flow_success( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful reauth flow.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Check that the form has the correct description placeholder + assert result.get("description_placeholders") == {"name": "BSBLAN Setup"} + + # Check that existing values are preserved as defaults + data_schema = result.get("data_schema") + assert data_schema is not None + + # Complete reauth with new credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "new_passkey", + CONF_USERNAME: "new_admin", + CONF_PASSWORD: "new_password", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify config entry was updated with new credentials + assert mock_config_entry.data[CONF_PASSKEY] == "new_passkey" + assert mock_config_entry.data[CONF_USERNAME] == "new_admin" + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" + # Verify host and port remain unchanged + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_PORT] == 80 + + +async def test_reauth_flow_auth_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with authentication error.""" + mock_config_entry.add_to_hass(hass) + + # Mock authentication error + mock_bsblan.device.side_effect = BSBLANAuthError + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit with wrong credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "wrong_passkey", + CONF_USERNAME: "wrong_admin", + CONF_PASSWORD: "wrong_password", + }, + ) + + _assert_form_result(result, "reauth_confirm", {"base": "invalid_auth"}) + + # Verify that user input is preserved in the form after error + data_schema = result.get("data_schema") + assert data_schema is not None + + # Check that the form fields contain the previously entered values + passkey_field = next( + field for field in data_schema.schema if field.schema == CONF_PASSKEY + ) + username_field = next( + field for field in data_schema.schema if field.schema == CONF_USERNAME + ) + + assert passkey_field.default() == "wrong_passkey" + assert username_field.default() == "wrong_admin" + + +async def test_reauth_flow_connection_error( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with connection error.""" + mock_config_entry.add_to_hass(hass) + + # Mock connection error + mock_bsblan.device.side_effect = BSBLANConnectionError + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit credentials but get connection error + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "1234", + CONF_USERNAME: "admin", + CONF_PASSWORD: "admin1234", + }, + ) + + _assert_form_result(result, "reauth_confirm", {"base": "cannot_connect"}) + + +async def test_reauth_flow_preserves_existing_values( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that reauth flow preserves existing values when user doesn't change them.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + _assert_form_result(result, "reauth_confirm") + + # Submit without changing any credentials (only password is provided) + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSWORD: "new_password_only", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that existing passkey and username are preserved + assert mock_config_entry.data[CONF_PASSKEY] == "1234" # Original value + assert mock_config_entry.data[CONF_USERNAME] == "admin" # Original value + assert mock_config_entry.data[CONF_PASSWORD] == "new_password_only" # New value + + +async def test_reauth_flow_partial_credentials_update( + hass: HomeAssistant, + mock_bsblan: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with partial credential updates.""" + mock_config_entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + # Submit with only username and password changes + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_USERNAME: "new_admin", + CONF_PASSWORD: "new_password", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify partial update: passkey preserved, username and password updated + assert mock_config_entry.data[CONF_PASSKEY] == "1234" # Original preserved + assert mock_config_entry.data[CONF_USERNAME] == "new_admin" # Updated + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" # Updated + # Host and port should remain unchanged + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_PORT] == 80 + + +async def test_reauth_flow_preserves_non_credential_fields( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow preserves non-credential fields using data_updates.""" + # Create a config entry with additional custom fields that should be preserved + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "old_key", + CONF_USERNAME: "old_user", + CONF_PASSWORD: "old_pass", + # Add some custom fields that should be preserved + "custom_field": "should_be_preserved", + "another_field": 42, + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with only new credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "new_key", + CONF_USERNAME: "new_user", + CONF_PASSWORD: "new_pass", + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that only the provided fields were updated, others preserved + assert entry.data[CONF_PASSKEY] == "new_key" # Updated + assert entry.data[CONF_USERNAME] == "new_user" # Updated + assert entry.data[CONF_PASSWORD] == "new_pass" # Updated + + # These fields should remain unchanged (preserved by data_updates) + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + assert entry.data["custom_field"] == "should_be_preserved" + assert entry.data["another_field"] == 42 + + +async def test_reauth_flow_clears_credentials_with_empty_strings( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow can clear credentials by providing empty strings.""" + # Create a config entry with existing credentials + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "existing_key", + CONF_USERNAME: "existing_user", + CONF_PASSWORD: "existing_pass", + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with empty strings to clear credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "", # Clear passkey + CONF_USERNAME: "", # Clear username + CONF_PASSWORD: "", # Clear password + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify that credentials were cleared (set to empty strings) + assert entry.data[CONF_PASSKEY] == "" + assert entry.data[CONF_USERNAME] == "" + assert entry.data[CONF_PASSWORD] == "" + + # Host and port should remain unchanged + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + + +async def test_reauth_flow_partial_clear_credentials( + hass: HomeAssistant, + mock_bsblan: MagicMock, +) -> None: + """Test reauth flow can partially clear some credentials while updating others.""" + # Create a config entry with existing credentials + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 80, + CONF_PASSKEY: "existing_key", + CONF_USERNAME: "existing_user", + CONF_PASSWORD: "existing_pass", + }, + unique_id="00:80:41:19:69:90", + ) + entry.add_to_hass(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + ) + + # Submit with mix of clearing and updating credentials + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "", # Clear passkey + CONF_USERNAME: "new_user", # Update username + CONF_PASSWORD: "", # Clear password + }, + ) + + _assert_abort_result(result, "reauth_successful") + + # Verify mixed update: some cleared, some updated, some preserved + assert entry.data[CONF_PASSKEY] == "" # Cleared + assert entry.data[CONF_USERNAME] == "new_user" # Updated + assert entry.data[CONF_PASSWORD] == "" # Cleared + + # Host and port should remain unchanged + assert entry.data[CONF_HOST] == "127.0.0.1" + assert entry.data[CONF_PORT] == 80 + + +async def test_zeroconf_discovery_auth_error_during_confirm( + hass: HomeAssistant, + mock_bsblan: MagicMock, + zeroconf_discovery_info: ZeroconfServiceInfo, +) -> None: + """Test authentication error during discovery_confirm step.""" + # Remove MAC from discovery to force discovery_confirm step + zeroconf_discovery_info.properties.pop("mac", None) + + # Setup device to require authentication during initial discovery + mock_bsblan.device.side_effect = BSBLANError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf_discovery_info, + ) + + _assert_form_result(result, "discovery_confirm") + + # Now setup auth error for the confirmation step + mock_bsblan.device.side_effect = BSBLANAuthError + + result = await _configure_flow( + hass, + result["flow_id"], + { + CONF_PASSKEY: "wrong_key", + CONF_USERNAME: "admin", + CONF_PASSWORD: "wrong_password", + }, + ) + + # Should show the discovery_confirm form again with auth error + _assert_form_result(result, "discovery_confirm", {"base": "invalid_auth"}) diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index a9c3605f67f..cc52799d28b 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -2,13 +2,14 @@ from unittest.mock import MagicMock -from bsblan import BSBLANConnectionError +from bsblan import BSBLANAuthError, BSBLANConnectionError +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.bsblan.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_load_unload_config_entry( @@ -45,3 +46,32 @@ async def test_config_entry_not_ready( assert len(mock_bsblan.state.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_auth_failed_triggers_reauth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_bsblan: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that BSBLANAuthError during coordinator update triggers reauth flow.""" + # First, set up the integration successfully + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Mock BSBLANAuthError during next update + mock_bsblan.initialize.side_effect = BSBLANAuthError("Authentication failed") + + # Advance time by the coordinator's update interval to trigger update + freezer.tick(delta=20) # Advance beyond the 12 second scan interval + random offset + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check that a reauth flow has been started + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id diff --git a/tests/components/cambridge_audio/snapshots/test_init.ambr b/tests/components/cambridge_audio/snapshots/test_init.ambr index 7f4bbed36f7..22642635375 100644 --- a/tests/components/cambridge_audio/snapshots/test_init.ambr +++ b/tests/components/cambridge_audio/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0020c2d8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Cambridge Audio', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0020c2d8', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index e63af0ced09..a4625fcce92 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -74,6 +74,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]: mock_cloud.payments = MagicMock( spec=payments_api.PaymentsApi, subscription_info=AsyncMock(), + migrate_paypal_agreement=AsyncMock(), ) mock_cloud.ice_servers = MagicMock( spec=IceServers, diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index d131d211e2f..0377ee81dba 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -4,6 +4,7 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from hass_nabucasa.payments_api import PaymentsApiError import pytest from homeassistant.components.cloud.const import DOMAIN @@ -210,7 +211,13 @@ async def test_legacy_subscription_repair_flow_timeout( "preview": None, } - with patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0): + with ( + patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0), + patch( + "hass_nabucasa.payments_api.PaymentsApi.migrate_paypal_agreement", + side_effect=PaymentsApiError("some error", status=403), + ), + ): resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") assert resp.status == HTTPStatus.OK data = await resp.json() @@ -236,7 +243,6 @@ async def test_legacy_subscription_repair_flow_timeout( "handler": "cloud", "reason": "operation_took_too_long", "description_placeholders": None, - "result": None, } assert issue_registry.async_get_issue( diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index c34ca1bc871..ba45e6bca57 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -25,6 +25,7 @@ async def mocked_cloud_object(hass: HomeAssistant) -> Cloud: payments=Mock( spec=payments_api.PaymentsApi, subscription_info=AsyncMock(), + migrate_paypal_agreement=AsyncMock(), ), ) @@ -52,10 +53,7 @@ async def test_migrate_paypal_agreement_with_timeout_error( mocked_cloud: Cloud, ) -> None: """Test that we handle timeout error.""" - aioclient_mock.post( - "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", - exc=TimeoutError(), - ) + mocked_cloud.payments.migrate_paypal_agreement.side_effect = TimeoutError() assert await async_migrate_paypal_agreement(mocked_cloud) is None assert ( diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 391fb609d65..8f68274d37f 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -45,7 +45,7 @@ 'nl', 'pl', 'pt', - 'pt-br', + 'pt-BR', 'ro', 'ru', 'sk', @@ -60,9 +60,9 @@ 'uk', 'ur', 'vi', - 'zh-cn', - 'zh-hk', - 'zh-tw', + 'zh-CN', + 'zh-HK', + 'zh-TW', ]), }), dict({ diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 3c22aaeee8f..7ab9e0cb97a 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -46,7 +46,7 @@ async def test_datadog_setup_full(hass: HomeAssistant) -> None: assert mock_dogstatsd.call_count == 1 assert mock_dogstatsd.call_args == mock.call( - host="host", port=123, namespace="foo" + host="host", port=123, namespace="foo", disable_telemetry=True ) @@ -65,7 +65,7 @@ async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: assert mock_dogstatsd.call_count == 1 assert mock_dogstatsd.call_args == mock.call( - host="localhost", port=8125, namespace="hass" + host="localhost", port=8125, namespace="hass", disable_telemetry=True ) diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr index 06067b69c17..59e77c4fb12 100644 --- a/tests/components/deconz/snapshots/test_hub.ambr +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -17,7 +17,6 @@ '01234E56789A', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Dresden Elektronik', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index 27ffd981b1e..13603beb8b4 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -58,7 +56,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', @@ -68,7 +65,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) @@ -91,7 +87,6 @@ '1234567890', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'devolo', @@ -101,7 +96,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': '5.6.1', 'via_device_id': None, }) diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index e403c937394..0e847da73ad 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'E1234567890000000001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ecovacs', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'E1234567890000000001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 2f1c2107b52..85f9fadd2a0 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -70,7 +70,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -80,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -156,7 +154,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -166,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 16f20224079..5dbc21f62df 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -102,7 +102,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -112,7 +111,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -222,7 +220,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -232,7 +229,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) @@ -342,7 +338,6 @@ 'CN11A1A00001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -352,7 +347,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'CN11A1A00001', - 'suggested_area': None, 'sw_version': '1.0.3 (192)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 3592e88f975..f53f8d223bd 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -77,7 +77,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -87,7 +86,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -173,7 +171,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -183,7 +180,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -269,7 +265,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -279,7 +274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -362,7 +356,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -372,7 +365,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -458,7 +450,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -468,7 +459,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index f29c16d0cae..61235f17ece 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -69,7 +69,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -79,7 +78,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) @@ -154,7 +152,6 @@ 'GW24L1A02987', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Elgato', @@ -164,7 +161,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GW24L1A02987', - 'suggested_area': None, 'sw_version': '1.0.4 (229)', 'via_device_id': None, }) diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 3a7f4e4fb9f..ca6c502d3be 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -50,7 +50,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -60,7 +59,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -298,7 +296,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -308,7 +305,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -929,7 +925,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -939,7 +934,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -1177,7 +1171,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -1187,7 +1180,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -1852,7 +1844,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -1862,7 +1853,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ @@ -2100,7 +2090,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -2110,7 +2099,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -2796,7 +2784,6 @@ '1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -2806,7 +2793,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '1', - 'suggested_area': None, 'sw_version': None, }), 'entities': list([ @@ -3346,7 +3332,6 @@ '<>', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Enphase', @@ -3356,7 +3341,6 @@ 'name_by_user': None, 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', - 'suggested_area': None, 'sw_version': '7.6.175', }), 'entities': list([ diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 3f0148262e4..0fda7714dd0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -27,6 +27,9 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.encryption_key_storage import ( + ENCRYPTION_KEY_STORAGE_KEY, +) from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -41,6 +44,118 @@ from .conftest import MockGenericDeviceEntryType from tests.common import MockConfigEntry + +async def test_retrieve_encryption_key_from_storage_with_device_mac( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], +) -> None: + """Test key successfully retrieved from storage.""" + + # Mock the encryption key storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {"11:22:33:44:55:aa": VALID_NOISE_PSK}}, + } + + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test", "11:22:33:44:55:AA"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_reauth_fixed_from_from_storage( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], +) -> None: + """Test reauth fixed automatically via storage.""" + + # Mock the encryption key storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {"11:22:33:44:55:aa": VALID_NOISE_PSK}}, + } + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + + result = await entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_retrieve_encryption_key_from_storage_no_key_found( + hass: HomeAssistant, + mock_client: APIClient, +) -> None: + """Test _retrieve_encryption_key_from_storage when no key is found.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + + result = await entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM, result + assert result["step_id"] == "reauth_confirm" + assert CONF_NOISE_PSK not in entry.data + + INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" WRONG_NOISE_PSK = "GP+ciK+nVfTQ/gcz6uOdS+oKEdJgesU+jeu8Ssj2how=" @@ -2370,3 +2485,36 @@ async def test_reconfig_name_conflict_overwrite( ) is None ) + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_discovery_dhcp_no_probe_same_host_port_none( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test dhcp discovery does not probe when host matches and port is None.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + # DHCP discovery with same MAC and host (WiFi device) + service_info = DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="11:22:33:44:55:aa", # Same MAC as configured + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Verify device_info was NOT called (no probing) + mock_client.device_info.assert_not_called() + + # Host should remain unchanged + assert entry.data[CONF_HOST] == "192.168.43.183" diff --git a/tests/components/esphome/test_dynamic_encryption.py b/tests/components/esphome/test_dynamic_encryption.py new file mode 100644 index 00000000000..cbdcc35aea2 --- /dev/null +++ b/tests/components/esphome/test_dynamic_encryption.py @@ -0,0 +1,102 @@ +"""Tests for ESPHome dynamic encryption key generation.""" + +from __future__ import annotations + +import base64 + +from homeassistant.components.esphome.encryption_key_storage import ( + ESPHomeEncryptionKeyStorage, + async_get_encryption_key_storage, +) +from homeassistant.core import HomeAssistant + + +async def test_dynamic_encryption_key_generation_mock(hass: HomeAssistant) -> None: + """Test that encryption key generation works with mocked storage.""" + storage = await async_get_encryption_key_storage(hass) + + # Store a key + mac_address = "11:22:33:44:55:aa" + test_key = base64.b64encode(b"test_key_32_bytes_long_exactly!").decode() + + await storage.async_store_key(mac_address, test_key) + + # Retrieve a key + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == test_key + + +async def test_encryption_key_storage_remove_key(hass: HomeAssistant) -> None: + """Test ESPHomeEncryptionKeyStorage async_remove_key method.""" + # Create storage instance + storage = ESPHomeEncryptionKeyStorage(hass) + + # Test removing a key that exists + mac_address = "11:22:33:44:55:aa" + test_key = "test_encryption_key_32_bytes_long" + + # First store a key + await storage.async_store_key(mac_address, test_key) + + # Verify key exists + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == test_key + + # Remove the key + await storage.async_remove_key(mac_address) + + # Verify key no longer exists + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key is None + + # Test removing a key that doesn't exist (should not raise an error) + non_existent_mac = "aa:bb:cc:dd:ee:ff" + await storage.async_remove_key(non_existent_mac) # Should not raise + + # Test case insensitive removal + upper_mac = "22:33:44:55:66:77" + await storage.async_store_key(upper_mac, test_key) + + # Remove using lowercase MAC address + await storage.async_remove_key(upper_mac.lower()) + + # Verify key was removed + retrieved_key = await storage.async_get_key(upper_mac) + assert retrieved_key is None + + +async def test_encryption_key_basic_storage( + hass: HomeAssistant, +) -> None: + """Test basic encryption key storage functionality.""" + storage = await async_get_encryption_key_storage(hass) + mac_address = "11:22:33:44:55:aa" + key = "test_encryption_key_32_bytes_long" + + # Store key + await storage.async_store_key(mac_address, key) + + # Retrieve key + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == key + + +async def test_retrieve_key_from_storage( + hass: HomeAssistant, +) -> None: + """Test config flow can retrieve encryption key from storage for new device.""" + # Test that the encryption key storage integration works with config flow + storage = await async_get_encryption_key_storage(hass) + mac_address = "11:22:33:44:55:aa" + stored_key = "test_encryption_key_32_bytes_long" + + # Store encryption key for a device + await storage.async_store_key(mac_address, stored_key) + + # Verify the key can be retrieved (simulating config flow behavior) + retrieved_key = await storage.async_get_key(mac_address) + assert retrieved_key == stored_key + + # Test case insensitive retrieval (since config flows might use different case) + retrieved_key_upper = await storage.async_get_key(mac_address.upper()) + assert retrieved_key_upper == stored_key diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 318ccde221f..fec957a9560 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1,8 +1,10 @@ """Test ESPHome manager.""" import asyncio +import base64 import logging -from unittest.mock import AsyncMock, Mock, call +from typing import Any +from unittest.mock import AsyncMock, Mock, call, patch from aioesphomeapi import ( APIClient, @@ -27,11 +29,15 @@ from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_BLUETOOTH_MAC_ADDRESS, CONF_DEVICE_NAME, + CONF_NOISE_PSK, CONF_SUBSCRIBE_LOGS, DOMAIN, STABLE_BLE_URL_VERSION, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.encryption_key_storage import ( + ENCRYPTION_KEY_STORAGE_KEY, +) from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT from homeassistant.components.tag import DOMAIN as TAG_DOMAIN from homeassistant.const import ( @@ -44,6 +50,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( + area_registry as ar, device_registry as dr, entity_registry as er, issue_registry as ir, @@ -1164,6 +1171,7 @@ async def test_esphome_user_services_changes( async def test_esphome_device_with_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1178,11 +1186,12 @@ async def test_esphome_device_with_suggested_area( dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) - assert dev.suggested_area == "kitchen" + assert dev.area_id == area_registry.async_get_area_by_name("kitchen").id async def test_esphome_device_area_priority( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, @@ -1201,7 +1210,7 @@ async def test_esphome_device_area_priority( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) # Should use device_info.area.name instead of suggested_area - assert dev.suggested_area == "Living Room" + assert dev.area_id == area_registry.async_get_area_by_name("Living Room").id async def test_esphome_device_with_project( @@ -1529,6 +1538,7 @@ async def test_assist_in_progress_issue_deleted( async def test_sub_device_creation( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1565,7 +1575,7 @@ async def test_sub_device_creation( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub" + assert main_device.area_id == area_registry.async_get_area_by_name("Main Hub").id # Check sub devices are created sub_device_1 = device_registry.async_get_device( @@ -1573,7 +1583,9 @@ async def test_sub_device_creation( ) assert sub_device_1 is not None assert sub_device_1.name == "Motion Sensor" - assert sub_device_1.suggested_area == "Living Room" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_1.via_device_id == main_device.id sub_device_2 = device_registry.async_get_device( @@ -1581,7 +1593,9 @@ async def test_sub_device_creation( ) assert sub_device_2 is not None assert sub_device_2.name == "Light Switch" - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) assert sub_device_2.via_device_id == main_device.id sub_device_3 = device_registry.async_get_device( @@ -1589,7 +1603,7 @@ async def test_sub_device_creation( ) assert sub_device_3 is not None assert sub_device_3.name == "Temperature Sensor" - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id assert sub_device_3.via_device_id == main_device.id @@ -1725,6 +1739,7 @@ async def test_sub_device_with_empty_name( async def test_sub_device_references_main_device_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, mock_client: APIClient, mock_esphome_device: MockESPHomeDeviceType, ) -> None: @@ -1766,25 +1781,507 @@ async def test_sub_device_references_main_device_area( connections={(dr.CONNECTION_NETWORK_MAC, device.device_info.mac_address)} ) assert main_device is not None - assert main_device.suggested_area == "Main Hub Area" + assert ( + main_device.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 1 uses main device's area sub_device_1 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_11111111")} ) assert sub_device_1 is not None - assert sub_device_1.suggested_area == "Main Hub Area" + assert ( + sub_device_1.area_id == area_registry.async_get_area_by_name("Main Hub Area").id + ) # Check sub device 2 uses Living Room sub_device_2 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_22222222")} ) assert sub_device_2 is not None - assert sub_device_2.suggested_area == "Living Room" + assert ( + sub_device_2.area_id == area_registry.async_get_area_by_name("Living Room").id + ) # Check sub device 3 uses Bedroom sub_device_3 = device_registry.async_get_device( identifiers={(DOMAIN, f"{device.device_info.mac_address}_33333333")} ) assert sub_device_3 is not None - assert sub_device_3.suggested_area == "Bedroom" + assert sub_device_3.area_id == area_registry.async_get_area_by_name("Bedroom").id + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_dynamic_encryption_key_generation( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test that a device without a key in storage gets a new one generated.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify the key was generated and set + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + + # Verify config entry was updated + assert entry.data[CONF_NOISE_PSK] == expected_key + + +async def test_manager_retrieves_key_from_storage_on_reconnect( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test that manager retrieves encryption key from storage during reconnect.""" + mac_address = "11:22:33:44:55:aa" + test_key = base64.b64encode(b"existing_key_32_bytes_long!!!").decode() + + # Set up storage with existing key + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": {"keys": {mac_address: test_key}}, + } + + # Create entry without noise PSK (will be loaded from storage) + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key retrieval from storage + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify noise_encryption_set_key was called with the stored key + mock_client.noise_encryption_set_key.assert_called_once_with(test_key.encode()) + + # Verify config entry was updated with key from storage + assert entry.data[CONF_NOISE_PSK] == test_key + + +async def test_manager_handle_dynamic_encryption_key_guard_clauses( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test _handle_dynamic_encryption_key guard clauses and early returns.""" + # Test guard clause - no unique_id + entry_no_id = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=None, # No unique ID - should not generate key + ) + entry_no_id.add_to_hass(hass) + + # Set up device without unique ID + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry_no_id, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": "11:22:33:44:55:aa", + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # noise_encryption_set_key should not be called when no unique_id + mock_client.noise_encryption_set_key = AsyncMock() + await device.mock_disconnect(True) + await device.mock_connect() + + mock_client.noise_encryption_set_key.assert_not_called() + + +async def test_manager_handle_dynamic_encryption_key_edge_cases( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test _handle_dynamic_encryption_key edge cases for better coverage.""" + mac_address = "11:22:33:44:55:aa" + + # Test device without encryption support + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Set up device without encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": False, # No encryption support + }, + ) + + # noise_encryption_set_key should not be called when encryption not supported + mock_client.noise_encryption_set_key = AsyncMock() + await device.mock_disconnect(True) + await device.mock_connect() + + mock_client.noise_encryption_set_key.assert_not_called() + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_dynamic_encryption_key_generation_flow( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test the complete dynamic encryption key generation flow.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify the complete flow + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + assert entry.data[CONF_NOISE_PSK] == expected_key + + # Verify key was stored in hass_storage + assert ( + hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"][mac_address] + == expected_key + ) + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_no_existing_key( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when no existing key is found.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + expected_key = base64.b64encode(test_key_bytes).decode() + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods + mock_client.noise_encryption_set_key = AsyncMock(return_value=True) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation flow + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once() + + # Verify config entry was updated + assert entry.data[CONF_NOISE_PSK] == expected_key + + # Verify key was stored + assert ( + hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"][mac_address] + == expected_key + ) + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_device_set_key_fails( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when noise_encryption_set_key returns False.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods - set_key returns False + mock_client.noise_encryption_set_key = AsyncMock(return_value=False) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Reset mocks since initial connection already happened + mock_token_bytes.reset_mock() + mock_client.noise_encryption_set_key.reset_mock() + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation was attempted with the expected key + mock_token_bytes.assert_called_once_with(32) + mock_client.noise_encryption_set_key.assert_called_once_with( + base64.b64encode(test_key_bytes) + ) + + # Verify config entry was NOT updated since set_key failed + assert CONF_NOISE_PSK not in entry.data + + +@patch("homeassistant.components.esphome.manager.secrets.token_bytes") +async def test_manager_handle_dynamic_encryption_key_connection_error( + mock_token_bytes: Mock, + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_storage: dict[str, Any], +) -> None: + """Test _handle_dynamic_encryption_key when noise_encryption_set_key raises APIConnectionError.""" + mac_address = "11:22:33:44:55:aa" + test_key_bytes = b"test_key_32_bytes_long_exactly!" + mock_token_bytes.return_value = test_key_bytes + + # Initialize empty storage + hass_storage[ENCRYPTION_KEY_STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": ENCRYPTION_KEY_STORAGE_KEY, + "data": { + "keys": {} # No existing keys + }, + } + + # Create entry without noise PSK + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test-device", + }, + unique_id=mac_address, + ) + entry.add_to_hass(hass) + + # Mock the client methods - set_key raises APIConnectionError + mock_client.noise_encryption_set_key = AsyncMock( + side_effect=APIConnectionError("Connection failed") + ) + + # Set up device with encryption support + device = await mock_esphome_device( + mock_client=mock_client, + entry=entry, + device_info={ + "uses_password": False, + "name": "test-device", + "mac_address": mac_address, + "esphome_version": "2023.12.0", + "api_encryption_supported": True, + }, + ) + + # Force reconnect to trigger key generation + await device.mock_disconnect(True) + await device.mock_connect() + + # Verify key generation was attempted twice (once during setup, once during reconnect) + # This is expected because the first attempt failed with connection error + assert mock_token_bytes.call_count == 2 + mock_token_bytes.assert_called_with(32) + assert mock_client.noise_encryption_set_key.call_count == 2 + + # Verify config entry was NOT updated since connection error occurred + assert CONF_NOISE_PSK not in entry.data + + # Verify key was NOT stored due to connection error + assert mac_address not in hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"] diff --git a/tests/components/flo/snapshots/test_init.ambr b/tests/components/flo/snapshots/test_init.ambr index edba0ebe162..6a242c4d2ce 100644 --- a/tests/components/flo/snapshots/test_init.ambr +++ b/tests/components/flo/snapshots/test_init.ambr @@ -22,7 +22,6 @@ '98765', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Flo by Moen', @@ -32,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '6.1.1', 'via_device_id': None, }), @@ -57,7 +55,6 @@ '32839', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Flo by Moen', @@ -67,7 +64,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '1.1.15', 'via_device_id': None, }), diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index d2af92b3f8f..e11d42d970e 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00000000-0000-0000-0000-000000000001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) diff --git a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr index 3527596c9b9..859c0eeb1fe 100644 --- a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr +++ b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr @@ -15,7 +15,6 @@ # --- # name: test_options[create_entry] FlowResultSnapshot({ - 'result': True, 'type': , }) # --- diff --git a/tests/components/generic_thermostat/snapshots/test_config_flow.ambr b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr index ed757d1c2ae..e69e51e19cd 100644 --- a/tests/components/generic_thermostat/snapshots/test_config_flow.ambr +++ b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr @@ -39,7 +39,6 @@ # --- # name: test_options[create_entry] FlowResultSnapshot({ - 'result': True, 'type': , }) # --- diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index 0c57935589b..d0b92a7e88d 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -18,7 +18,6 @@ 'ulid-conversation', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -49,7 +47,6 @@ 'ulid-stt', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -59,7 +56,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -80,7 +76,6 @@ 'ulid-ai-task', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -90,7 +85,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -111,7 +105,6 @@ 'ulid-tts', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Google', @@ -121,7 +114,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 30adae2fd2a..322e6ebdad0 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -315,11 +315,11 @@ async def test_options( ("group_type", "extra_options", "extra_options_after", "advanced"), [ ("light", {"all": False}, {"all": False}, False), - ("light", {"all": True}, {"all": True}, False), + ("light", {"all": True}, {"all": False}, False), ("light", {"all": False}, {"all": False}, True), ("light", {"all": True}, {"all": False}, True), ("switch", {"all": False}, {"all": False}, False), - ("switch", {"all": True}, {"all": True}, False), + ("switch", {"all": True}, {"all": False}, False), ("switch", {"all": False}, {"all": False}, True), ("switch", {"all": True}, {"all": False}, True), ], diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 4c4f0e24dcc..39f9d4580bd 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -471,7 +471,6 @@ async def test_mount_failed_repair_flow_error( "flow_id": flow_id, "handler": "hassio", "reason": "apply_suggestion_fail", - "result": None, "description_placeholders": None, } diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index c4c2b861e6e..4839486810a 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -55,6 +55,10 @@ async def test_hassio_system_health( hass.data["hassio_network_info"] = { "host_internet": True, "supervisor_internet": True, + "interfaces": [ + {"primary": False, "ipv4": {"nameservers": ["9.9.9.9"]}}, + {"primary": True, "ipv4": {"nameservers": ["1.1.1.1"]}}, + ], } with patch.dict(os.environ, MOCK_ENVIRON): @@ -76,6 +80,7 @@ async def test_hassio_system_health( "host_os": "Home Assistant OS 5.9", "installed_addons": "Awesome Addon (1.0.0)", "ntp_synchronized": True, + "nameservers": "1.1.1.1", "supervisor_api": "ok", "supervisor_version": "supervisor-2020.11.1", "supported": True, diff --git a/tests/components/homeassistant_hardware/test_coordinator.py b/tests/components/homeassistant_hardware/test_coordinator.py index 9c57aac6811..39fef3366ad 100644 --- a/tests/components/homeassistant_hardware/test_coordinator.py +++ b/tests/components/homeassistant_hardware/test_coordinator.py @@ -13,6 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + async def test_firmware_update_coordinator_fetching( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -20,6 +22,8 @@ async def test_firmware_update_coordinator_fetching( """Test the firmware update coordinator loads manifests.""" session = async_get_clientsession(hass) + mock_config_entry = MockConfigEntry() + manifest = FirmwareManifest( url=URL("https://example.org/firmware"), html_url=URL("https://example.org/release_notes"), @@ -35,7 +39,7 @@ async def test_firmware_update_coordinator_fetching( return_value=mock_client, ): coordinator = FirmwareUpdateCoordinator( - hass, session, "https://example.org/firmware" + hass, mock_config_entry, session, "https://example.org/firmware" ) listener = Mock() diff --git a/tests/components/homeassistant_hardware/test_update.py b/tests/components/homeassistant_hardware/test_update.py index aacc064e4f2..3103e5cfc6a 100644 --- a/tests/components/homeassistant_hardware/test_update.py +++ b/tests/components/homeassistant_hardware/test_update.py @@ -143,6 +143,7 @@ def _mock_async_create_update_entity( config_entry=config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), @@ -593,6 +594,7 @@ async def test_update_entity_graceful_firmware_type_callback_errors( config_entry=update_config_entry, update_coordinator=FirmwareUpdateCoordinator( hass, + update_config_entry, session, TEST_FIRMWARE_RELEASES_URL, ), diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 4df3efab360..bdde5e09ea6 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -211,7 +211,6 @@ async def test_options_flow( ) assert create_result["type"] is FlowResultType.CREATE_ENTRY - assert create_result["result"] is True assert config_entry.data == { "firmware": "ezsp", diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index d5f1c380971..6e2120aa961 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -406,7 +406,6 @@ async def test_firmware_options_flow( ) assert create_result["type"] is FlowResultType.CREATE_ENTRY - assert create_result["result"] is True assert config_entry.data == { "firmware": fw_type.value, diff --git a/tests/components/homee/snapshots/test_init.ambr b/tests/components/homee/snapshots/test_init.ambr index 664740dbeac..8f20bb10454 100644 --- a/tests/components/homee/snapshots/test_init.ambr +++ b/tests/components/homee/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00055511EECC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'homee', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.2.3', 'via_device_id': None, }) @@ -54,7 +52,6 @@ '00055511EECC-3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -64,7 +61,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.54', 'via_device_id': , }) diff --git a/tests/components/homee/test_config_flow.py b/tests/components/homee/test_config_flow.py index 6f45dcbdb0d..3d2195443a2 100644 --- a/tests/components/homee/test_config_flow.py +++ b/tests/components/homee/test_config_flow.py @@ -1,15 +1,23 @@ """Test the Homee config flow.""" +from ipaddress import ip_address from unittest.mock import AsyncMock from pyHomee import HomeeAuthFailedException, HomeeConnectionFailedException import pytest -from homeassistant.components.homee.const import DOMAIN +from homeassistant import config_entries +from homeassistant.components.homee.const import ( + DOMAIN, + RESULT_CANNOT_CONNECT, + RESULT_INVALID_AUTH, + RESULT_UNKNOWN_ERROR, +) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import ( HOMEE_ID, @@ -24,6 +32,24 @@ from .conftest import ( from tests.common import MockConfigEntry +PARAMETRIZED_ERRORS = ( + ("side_eff", "error"), + [ + ( + HomeeConnectionFailedException("connection timed out"), + {"base": RESULT_CANNOT_CONNECT}, + ), + ( + HomeeAuthFailedException("wrong username or password"), + {"base": RESULT_INVALID_AUTH}, + ), + ( + Exception, + {"base": RESULT_UNKNOWN_ERROR}, + ), + ], +) + @pytest.mark.usefixtures("mock_homee", "mock_config_entry", "mock_setup_entry") async def test_config_flow( @@ -58,23 +84,7 @@ async def test_config_flow( assert result["result"].unique_id == HOMEE_ID -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_config_flow_errors( hass: HomeAssistant, mock_homee: AsyncMock, @@ -140,6 +150,172 @@ async def test_flow_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_homee", "mock_config_entry") +async def test_zeroconf_success( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homee: AsyncMock, +) -> None: + """Test zeroconf discovery flow.""" + mock_homee.get_access_token.side_effect = HomeeAuthFailedException( + "wrong username or password" + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["handler"] == DOMAIN + mock_setup_entry.assert_not_called() + + mock_homee.get_access_token.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["data"] == { + CONF_HOST: HOMEE_IP, + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + } + + mock_setup_entry.assert_called_once() + + +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) +async def test_zeroconf_confirm_errors( + hass: HomeAssistant, + mock_homee: AsyncMock, + side_eff: Exception, + error: dict[str, str], +) -> None: + """Test zeroconf discovery flow errors.""" + mock_homee.get_access_token.side_effect = HomeeAuthFailedException( + "wrong username or password" + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + flow_id = result["flow_id"] + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["handler"] == DOMAIN + + mock_homee.get_access_token.side_effect = side_eff + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == error + + mock_homee.get_access_token.side_effect = None + result = await hass.config_entries.flow.async_configure( + flow_id, + user_input={ + CONF_USERNAME: TESTUSER, + CONF_PASSWORD: TESTPASS, + }, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_zeroconf_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test zeroconf discovery flow when already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(HOMEE_IP), + ip_addresses=[ip_address(HOMEE_IP)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("side_eff", "ip", "reason"), + [ + ( + HomeeConnectionFailedException("connection timed out"), + HOMEE_IP, + RESULT_CANNOT_CONNECT, + ), + (Exception, HOMEE_IP, RESULT_CANNOT_CONNECT), + (None, "2001:db8::1", "ipv6_address"), + ], +) +async def test_zeroconf_errors( + hass: HomeAssistant, + mock_homee: AsyncMock, + side_eff: Exception, + ip: str, + reason: str, +) -> None: + """Test zeroconf discovery flow with an IPv6 address.""" + mock_homee.get_access_token.side_effect = side_eff + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + name=f"homee-{HOMEE_ID}._ssh._tcp.local.", + type="_ssh._tcp.local.", + hostname=f"homee-{HOMEE_ID}.local.", + ip_address=ip_address(ip), + ip_addresses=[ip_address(ip)], + port=22, + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + @pytest.mark.usefixtures("mock_homee", "mock_setup_entry") async def test_reauth_success( hass: HomeAssistant, @@ -171,23 +347,7 @@ async def test_reauth_success( assert mock_config_entry.data[CONF_PASSWORD] == NEW_TESTPASS -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_reauth_errors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -296,23 +456,7 @@ async def test_reconfigure_success( assert mock_config_entry.data[CONF_PASSWORD] == TESTPASS -@pytest.mark.parametrize( - ("side_eff", "error"), - [ - ( - HomeeConnectionFailedException("connection timed out"), - {"base": "cannot_connect"}, - ), - ( - HomeeAuthFailedException("wrong username or password"), - {"base": "invalid_auth"}, - ), - ( - Exception, - {"base": "unknown"}, - ), - ], -) +@pytest.mark.parametrize(*PARAMETRIZED_ERRORS) async def test_reconfigure_errors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 3f0f0a3c22b..47a9c398d16 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,6 +2,7 @@ from datetime import timedelta +from freezegun import freeze_time import pytest from homeassistant.components.homekit.const import ( @@ -22,6 +23,10 @@ from homeassistant.components.homekit.type_switches import ( Valve, ValveSwitch, ) +from homeassistant.components.input_number import ( + DOMAIN as INPUT_NUMBER_DOMAIN, + SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, +) from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, SERVICE_DOCK, @@ -30,6 +35,7 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntityFeature, ) from homeassistant.components.select import ATTR_OPTIONS +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, @@ -658,3 +664,223 @@ async def test_button_switch( await hass.async_block_till_done() assert acc.char_on.value is False assert len(events) == 1 + + +async def test_valve_switch_with_set_duration_characteristic( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with set duration characteristic.""" + entity_id = "switch.sprinkler" + + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "0") + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + {"type": "sprinkler", "linked_valve_duration": "input_number.valve_duration"}, + ) + acc.run() + await hass.async_block_till_done() + + # Assert initial state is synced + assert acc.get_duration() == 0 + + # Simulate setting duration from HomeKit + call_set_value = async_mock_service( + hass, INPUT_NUMBER_DOMAIN, INPUT_NUMBER_SERVICE_SET_VALUE + ) + acc.char_set_duration.client_update_value(300) + await hass.async_block_till_done() + assert call_set_value + assert call_set_value[0].data == { + "entity_id": "input_number.valve_duration", + "value": 300, + } + + # Assert state change in Home Assistant is synced to HomeKit + hass.states.async_set("input_number.valve_duration", "600") + await hass.async_block_till_done() + assert acc.get_duration() == 600 + + # Test fallback if no state is set + hass.states.async_remove("input_number.valve_duration") + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + # Test remaining duration fallback if no end time is linked + assert acc.get_remaining_duration() == 0 + + +async def test_valve_switch_with_remaining_duration_characteristic( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with remaining duration characteristic.""" + entity_id = "switch.sprinkler" + + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + {"type": "sprinkler", "linked_valve_end_time": "sensor.valve_end_time"}, + ) + acc.run() + await hass.async_block_till_done() + + # Assert initial state is synced + assert acc.get_remaining_duration() == 0 + + # Simulate remaining duration update from Home Assistant + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=90)).isoformat(), + ) + await hass.async_block_till_done() + + # Assert remaining duration is calculated correctly based on end time + assert acc.get_remaining_duration() == 90 + + # Test fallback if no state is set + hass.states.async_remove("sensor.valve_end_time") + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test get duration fallback if no duration is linked + assert acc.get_duration() == 0 + + +async def test_valve_switch_with_duration_characteristics( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve switch with set duration and remaining duration characteristics.""" + entity_id = "switch.sprinkler" + + # Test with duration and end time entities linked + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "300") + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Mock switch services to prevent errors + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_ON) + async_mock_service(hass, SWITCH_DOMAIN, SERVICE_TURN_OFF) + # Mock input_number service for set_duration calls + call_set_value = async_mock_service( + hass, INPUT_NUMBER_DOMAIN, INPUT_NUMBER_SERVICE_SET_VALUE + ) + + acc = ValveSwitch( + hass, + hk_driver, + "Sprinkler", + entity_id, + 5, + { + "type": "sprinkler", + "linked_valve_duration": "input_number.valve_duration", + "linked_valve_end_time": "sensor.valve_end_time", + }, + ) + acc.run() + await hass.async_block_till_done() + + # Test update_duration_chars with both characteristics + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=60)).isoformat(), + ) + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_set_duration.value == 300 + assert acc.get_remaining_duration() == 60 + + # Test get_duration fallback with invalid state + hass.states.async_set("input_number.valve_duration", "invalid") + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + # Test get_remaining_duration fallback with invalid state + hass.states.async_set("sensor.valve_end_time", "invalid") + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test get_remaining_duration with end time in the past + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() - timedelta(seconds=10)).isoformat(), + ) + await hass.async_block_till_done() + assert acc.get_remaining_duration() == 0 + + # Test set_duration with negative value + acc.set_duration(-10) + await hass.async_block_till_done() + assert acc.get_duration() == 0 + # Verify the service was called with correct parameters + assert len(call_set_value) == 1 + assert call_set_value[0].data == { + "entity_id": "input_number.valve_duration", + "value": -10, + } + + # Test set_duration with negative state + hass.states.async_set("sensor.valve_duration", -10) + await hass.async_block_till_done() + assert acc.get_duration() == 0 + + +async def test_valve_with_duration_characteristics( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: + """Test valve with set duration and remaining duration characteristics.""" + entity_id = "switch.sprinkler" + + # Test with duration and end time entities linked + hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set("input_number.valve_duration", "900") + hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat()) + await hass.async_block_till_done() + + # Using Valve instead of ValveSwitch + acc = Valve( + hass, + hk_driver, + "Valve", + entity_id, + 5, + { + "linked_valve_duration": "input_number.valve_duration", + "linked_valve_end_time": "sensor.valve_end_time", + }, + ) + acc.run() + await hass.async_block_till_done() + + with freeze_time(dt_util.utcnow()): + hass.states.async_set( + "sensor.valve_end_time", + (dt_util.utcnow() + timedelta(seconds=600)).isoformat(), + ) + await hass.async_block_till_done() + assert acc.get_duration() == 900 + assert acc.get_remaining_duration() == 600 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 66906c72266..4cb8eb41489 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -15,6 +15,8 @@ from homeassistant.components.homekit.const import ( CONF_LINKED_BATTERY_SENSOR, CONF_LINKED_DOORBELL_SENSOR, CONF_LINKED_MOTION_SENSOR, + CONF_LINKED_VALVE_DURATION, + CONF_LINKED_VALVE_END_TIME, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -128,7 +130,25 @@ def test_validate_entity_config() -> None: } }, {"switch.test": {CONF_TYPE: "invalid_type"}}, + { + "switch.test": { + CONF_TYPE: "sprinkler", + CONF_LINKED_VALVE_DURATION: "number.valve_duration", # Must be input_number entity + CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time", # Must be sensor (timestamp) entity + } + }, {"fan.test": {CONF_TYPE: "invalid_type"}}, + { + "valve.test": { + CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time", # Must be sensor (timestamp) entity + CONF_LINKED_VALVE_DURATION: "number.valve_duration", # Must be input_number + } + }, + { + "valve.test": { + CONF_TYPE: "sprinkler", # Extra keys not allowed + } + }, ] for conf in configs: @@ -212,6 +232,19 @@ def test_validate_entity_config() -> None: assert vec({"switch.demo": {CONF_TYPE: TYPE_VALVE}}) == { "switch.demo": {CONF_TYPE: TYPE_VALVE, CONF_LOW_BATTERY_THRESHOLD: 20} } + config = { + CONF_TYPE: TYPE_SPRINKLER, + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + } + assert vec({"switch.sprinkler": config}) == { + "switch.sprinkler": { + CONF_TYPE: TYPE_SPRINKLER, + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } assert vec({"sensor.co": {CONF_THRESHOLD_CO: 500}}) == { "sensor.co": {CONF_THRESHOLD_CO: 500, CONF_LOW_BATTERY_THRESHOLD: 20} } @@ -244,6 +277,17 @@ def test_validate_entity_config() -> None: CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, } } + config = { + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + } + assert vec({"valve.demo": config}) == { + "valve.demo": { + CONF_LINKED_VALVE_DURATION: "input_number.valve_duration", + CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time", + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } def test_validate_media_player_features() -> None: diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 4540cfd239a..3b075b44356 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -24,7 +24,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Sleekpoint Innovations', @@ -34,7 +33,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '0.8.16', }), 'entities': list([ @@ -655,7 +653,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -665,7 +662,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000A', - 'suggested_area': None, 'sw_version': '2.1.6', }), 'entities': list([ @@ -737,7 +733,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -747,7 +742,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000D', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -995,7 +989,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -1005,7 +998,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000B', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1253,7 +1245,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Anker', @@ -1263,7 +1254,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000C', - 'suggested_area': None, 'sw_version': '1.6.7', }), 'entities': list([ @@ -1515,7 +1505,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -1525,7 +1514,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa00000a0', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -1736,7 +1724,6 @@ '00:00:00:00:00:00:aid:33', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -1746,7 +1733,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '158d0007c59c6a', - 'suggested_area': None, 'sw_version': '0', }), 'entities': list([ @@ -1913,7 +1899,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -1923,7 +1908,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0000000123456789', - 'suggested_area': None, 'sw_version': '1.4.7', }), 'entities': list([ @@ -2205,7 +2189,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Aqara', @@ -2215,7 +2198,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111a1111a1a111', - 'suggested_area': None, 'sw_version': '9', }), 'entities': list([ @@ -2339,7 +2321,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netgear, Inc', @@ -2349,7 +2330,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00A0000000000', - 'suggested_area': None, 'sw_version': '1.10.931', }), 'entities': list([ @@ -2853,7 +2833,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ConnectSense', @@ -2863,7 +2842,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1020301376', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3325,7 +3303,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -3335,7 +3312,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -3500,7 +3476,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -3510,7 +3485,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -3982,7 +3956,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -3992,7 +3965,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4157,7 +4129,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4167,7 +4138,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4336,7 +4306,6 @@ '00:00:00:00:00:00:aid:4295608960', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4346,7 +4315,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4602,7 +4570,6 @@ '00:00:00:00:00:00:aid:4298360914', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4612,7 +4579,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -4861,7 +4827,6 @@ '00:00:00:00:00:00:aid:4298360921', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -4871,7 +4836,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5120,7 +5084,6 @@ '00:00:00:00:00:00:aid:4298527970', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5130,7 +5093,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5379,7 +5341,6 @@ '00:00:00:00:00:00:aid:4298527962', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5389,7 +5350,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5638,7 +5598,6 @@ '00:00:00:00:00:00:aid:4295016858', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5648,7 +5607,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -5904,7 +5862,6 @@ '00:00:00:00:00:00:aid:4298360712', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -5914,7 +5871,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6163,7 +6119,6 @@ '00:00:00:00:00:00:aid:4298649931', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6173,7 +6128,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6422,7 +6376,6 @@ '00:00:00:00:00:00:aid:4295608971', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6432,7 +6385,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6688,7 +6640,6 @@ '00:00:00:00:00:00:aid:4298584118', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6698,7 +6649,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -6947,7 +6897,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -6957,7 +6906,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '4.8.70226', }), 'entities': list([ @@ -7347,7 +7295,6 @@ '00:00:00:00:00:00:aid:4295016969', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7357,7 +7304,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7613,7 +7559,6 @@ '00:00:00:00:00:00:aid:4298568508', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7623,7 +7568,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -7876,7 +7820,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -7886,7 +7829,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8362,7 +8304,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8372,7 +8313,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8487,7 +8427,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8497,7 +8436,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789012', - 'suggested_area': None, 'sw_version': '4.2.394', }), 'entities': list([ @@ -8788,7 +8726,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8798,7 +8735,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -8963,7 +8899,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -8973,7 +8908,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -9142,7 +9076,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -9152,7 +9085,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456789016', - 'suggested_area': None, 'sw_version': '4.7.340214', }), 'entities': list([ @@ -9637,7 +9569,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'ecobee Inc.', @@ -9647,7 +9578,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '4.5.130201', }), 'entities': list([ @@ -9948,7 +9878,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Elgato', @@ -9958,7 +9887,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.8', }), 'entities': list([ @@ -10331,7 +10259,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Elgato', @@ -10341,7 +10268,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', - 'suggested_area': None, 'sw_version': '1.2.9', }), 'entities': list([ @@ -10702,7 +10628,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'José A. Jiménez Campos', @@ -10712,7 +10637,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-1', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -10924,7 +10848,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'José A. Jiménez Campos', @@ -10934,7 +10857,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-2', - 'suggested_area': None, 'sw_version': '5.0.18', }), 'entities': list([ @@ -11052,7 +10974,6 @@ '00:00:00:00:00:00:aid:123016423', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -11062,7 +10983,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11226,7 +11146,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11236,7 +11155,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -11308,7 +11226,6 @@ '00:00:00:00:00:00:aid:878448248', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -11318,7 +11235,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -11486,7 +11402,6 @@ '00:00:00:00:00:00:aid:766313939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11496,7 +11411,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11619,7 +11533,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11629,7 +11542,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11701,7 +11613,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -11711,7 +11622,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -11839,7 +11749,6 @@ '00:00:00:00:00:00:aid:1233851541', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lookin', @@ -11849,7 +11758,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12187,7 +12095,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12197,7 +12104,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12273,7 +12179,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12283,7 +12188,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12355,7 +12259,6 @@ '00:00:00:00:00:00:aid:3982136094', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'FirstAlert', @@ -12365,7 +12268,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -12541,7 +12443,6 @@ '00:00:00:00:00:00:aid:123016423', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -12551,7 +12452,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12715,7 +12615,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12725,7 +12624,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -12797,7 +12695,6 @@ '00:00:00:00:00:00:aid:878448248', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -12807,7 +12704,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', - 'suggested_area': None, 'sw_version': '3.6.2', }), 'entities': list([ @@ -12975,7 +12871,6 @@ '00:00:00:00:00:00:aid:766313939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -12985,7 +12880,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13108,7 +13002,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13118,7 +13011,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13190,7 +13082,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13200,7 +13091,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13329,7 +13219,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13339,7 +13228,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13411,7 +13299,6 @@ '00:00:00:00:00:00:aid:1256851357', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13421,7 +13308,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', - 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), 'entities': list([ @@ -13550,7 +13436,6 @@ '00:00:00:00:00:00:aid:1233851541', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lookin', @@ -13560,7 +13445,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -13907,7 +13791,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -13917,7 +13800,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -13993,7 +13875,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14003,7 +13884,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14075,7 +13955,6 @@ '00:00:00:00:00:00:aid:293334836', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'switchbot', @@ -14085,7 +13964,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14268,7 +14146,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14278,7 +14155,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14350,7 +14226,6 @@ '00:00:00:00:00:00:aid:293334836', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'switchbot', @@ -14360,7 +14235,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14543,7 +14417,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Home Assistant', @@ -14553,7 +14426,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', - 'suggested_area': None, 'sw_version': '2024.2.0', }), 'entities': list([ @@ -14625,7 +14497,6 @@ '00:00:00:00:00:00:aid:3982136094', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'FirstAlert', @@ -14635,7 +14506,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', - 'suggested_area': None, 'sw_version': '1.4.84', }), 'entities': list([ @@ -14826,7 +14696,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Garzola Marco', @@ -14836,7 +14705,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00000001', - 'suggested_area': None, 'sw_version': '1.0.0', }), 'entities': list([ @@ -15040,7 +14908,6 @@ '00:00:00:00:00:00:aid:6623462395276914', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15050,7 +14917,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276914', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15187,7 +15053,6 @@ '00:00:00:00:00:00:aid:6623462395276939', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15197,7 +15062,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462395276939', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15334,7 +15198,6 @@ '00:00:00:00:00:00:aid:6623462403113447', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15344,7 +15207,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403113447', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15481,7 +15343,6 @@ '00:00:00:00:00:00:aid:6623462403233419', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15491,7 +15352,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462403233419', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15628,7 +15488,6 @@ '00:00:00:00:00:00:aid:6623462412411853', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15638,7 +15497,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412411853', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15785,7 +15643,6 @@ '00:00:00:00:00:00:aid:6623462412413293', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15795,7 +15652,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462412413293', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -15942,7 +15798,6 @@ '00:00:00:00:00:00:aid:6623462389072572', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -15952,7 +15807,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462389072572', - 'suggested_area': None, 'sw_version': '45.1.17846', }), 'entities': list([ @@ -16276,7 +16130,6 @@ '00:00:00:00:00:00:aid:6623462378982941', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16286,7 +16139,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378982941', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16410,7 +16262,6 @@ '00:00:00:00:00:00:aid:6623462378983942', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16420,7 +16271,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462378983942', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16544,7 +16394,6 @@ '00:00:00:00:00:00:aid:6623462379122122', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16554,7 +16403,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379122122', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16678,7 +16526,6 @@ '00:00:00:00:00:00:aid:6623462379123707', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16688,7 +16535,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462379123707', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16812,7 +16658,6 @@ '00:00:00:00:00:00:aid:6623462383114163', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16822,7 +16667,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114163', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -16946,7 +16790,6 @@ '00:00:00:00:00:00:aid:6623462383114193', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -16956,7 +16799,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462383114193', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17080,7 +16922,6 @@ '00:00:00:00:00:00:aid:6623462385996792', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', @@ -17090,7 +16931,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '6623462385996792', - 'suggested_area': None, 'sw_version': '1.46.13', }), 'entities': list([ @@ -17214,7 +17054,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips Lighting', @@ -17224,7 +17063,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '123456', - 'suggested_area': None, 'sw_version': '1.32.1932126170', }), 'entities': list([ @@ -17300,7 +17138,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17310,7 +17147,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '2.2.15', }), 'entities': list([ @@ -17453,7 +17289,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17463,7 +17298,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EUCP03190xxxxx48', - 'suggested_area': None, 'sw_version': '2.3.7', }), 'entities': list([ @@ -17632,7 +17466,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Koogeek', @@ -17642,7 +17475,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'CNNT061751001372', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -17852,7 +17684,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lennox', @@ -17862,7 +17693,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'XXXXXXXX', - 'suggested_area': None, 'sw_version': '3.40.XX', }), 'entities': list([ @@ -18152,7 +17982,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'LG Electronics', @@ -18162,7 +17991,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '999AAAAAA999', - 'suggested_area': None, 'sw_version': '04.71.04', }), 'entities': list([ @@ -18344,7 +18172,6 @@ '00:00:00:00:00:00:aid:21474836482', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lutron Electronics Co., Inc', @@ -18354,7 +18181,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '39024290', - 'suggested_area': None, 'sw_version': '001.005', }), 'entities': list([ @@ -18477,7 +18303,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Lutron Electronics Co., Inc', @@ -18487,7 +18312,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '12344331', - 'suggested_area': None, 'sw_version': '08.08', }), 'entities': list([ @@ -18563,7 +18387,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Meross', @@ -18573,7 +18396,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'HH41234', - 'suggested_area': None, 'sw_version': '4.2.3', }), 'entities': list([ @@ -18859,7 +18681,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Meross', @@ -18869,7 +18690,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'BB1121', - 'suggested_area': None, 'sw_version': '4.1.9', }), 'entities': list([ @@ -18997,7 +18817,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Empowered Homes Inc.', @@ -19007,7 +18826,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '2.8.1', }), 'entities': list([ @@ -19347,7 +19165,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Nanoleaf', @@ -19357,7 +19174,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', - 'suggested_area': None, 'sw_version': '1.4.40', }), 'entities': list([ @@ -19632,7 +19448,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -19642,7 +19457,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'g738658', - 'suggested_area': None, 'sw_version': '80.0.0', }), 'entities': list([ @@ -19943,7 +19757,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -19953,7 +19766,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234', - 'suggested_area': None, 'sw_version': '1.0.3', }), 'entities': list([ @@ -20115,7 +19927,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -20125,7 +19936,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAAAAAAAA', - 'suggested_area': None, 'sw_version': '59', }), 'entities': list([ @@ -20441,7 +20251,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Green Electronics LLC', @@ -20451,7 +20260,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '00aa0000aa0a', - 'suggested_area': None, 'sw_version': '1.0.4', }), 'entities': list([ @@ -20887,7 +20695,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -20897,7 +20704,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21061,7 +20867,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21071,7 +20876,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0101.3521.0436', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21143,7 +20947,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21153,7 +20956,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -21321,7 +21123,6 @@ '00:00:00:00:00:00:aid:4', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21331,7 +21132,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21495,7 +21295,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21505,7 +21304,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21669,7 +21467,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21679,7 +21476,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -21843,7 +21639,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21853,7 +21648,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '0401.3521.0679', - 'suggested_area': None, 'sw_version': '1.3.0', }), 'entities': list([ @@ -21925,7 +21719,6 @@ '00:00:00:00:00:00:aid:5', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'RYSE Inc.', @@ -21935,7 +21728,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', - 'suggested_area': None, 'sw_version': '3.0.8', }), 'entities': list([ @@ -22103,7 +21895,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Schlage ', @@ -22113,7 +21904,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', - 'suggested_area': None, 'sw_version': '004.027.000', }), 'entities': list([ @@ -22232,7 +22022,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Hunter Fan', @@ -22242,7 +22031,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1234567890abcd', - 'suggested_area': None, 'sw_version': '', }), 'entities': list([ @@ -22422,7 +22210,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -22432,7 +22219,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -22553,7 +22339,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Moen Incorporated', @@ -22563,7 +22348,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '3.3.0', }), 'entities': list([ @@ -22968,7 +22752,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -22978,7 +22761,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '16.0.0', }), 'entities': list([ @@ -23198,7 +22980,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23208,7 +22989,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a1a11a1', - 'suggested_area': None, 'sw_version': '70', }), 'entities': list([ @@ -23280,7 +23060,6 @@ '00:00:00:00:00:00:aid:2', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23290,7 +23069,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'a11b111', - 'suggested_area': None, 'sw_version': '16', }), 'entities': list([ @@ -23506,7 +23284,6 @@ '00:00:00:00:00:00:aid:3', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VELUX', @@ -23516,7 +23293,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '1111111a114a111a', - 'suggested_area': None, 'sw_version': '48', }), 'entities': list([ @@ -23637,7 +23413,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -23647,7 +23422,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '0.0.0', }), 'entities': list([ @@ -23768,7 +23542,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Netatmo', @@ -23778,7 +23551,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': '**REDACTED**', - 'suggested_area': None, 'sw_version': '15.0.0', }), 'entities': list([ @@ -23898,7 +23670,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VOCOlinc', @@ -23908,7 +23679,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'AM01121849000327', - 'suggested_area': None, 'sw_version': '3.121.2', }), 'entities': list([ @@ -24219,7 +23989,6 @@ '00:00:00:00:00:00:aid:1', ]), ]), - 'is_new': False, 'labels': list([ ]), 'manufacturer': 'VOCOlinc', @@ -24229,7 +23998,6 @@ 'name_by_user': None, 'primary_config_entry': 'TestData', 'serial_number': 'EU0121203xxxxx07', - 'suggested_area': None, 'sw_version': '1.101.2', }), 'entities': list([ diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 656978a08a2..86c428b4413 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -328,6 +328,9 @@ async def test_snapshots( device_dict.pop("created_at", None) device_dict.pop("modified_at", None) device_dict.pop("_cache", None) + # This can be removed when suggested_area is removed from DeviceEntry + device_dict.pop("_suggested_area") + device_dict.pop("is_new") devices.append({"device": device_dict, "entities": entities}) diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index b005090309b..9b152988c24 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -365,14 +365,16 @@ async def test_hmip_garage_door_tormatic( assert ha_state.state == "closed" assert ha_state.attributes["current_position"] == 0 - service_call_counter = len(hmip_device.mock_calls) + service_call_counter = len(hmip_device.functionalChannels[1].mock_calls) await hass.services.async_call( "cover", "open_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 1 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.OPEN,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.OPEN @@ -381,9 +383,11 @@ async def test_hmip_garage_door_tormatic( await hass.services.async_call( "cover", "close_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 2 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.CLOSE,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.CLOSED @@ -392,9 +396,11 @@ async def test_hmip_garage_door_tormatic( await hass.services.async_call( "cover", "stop_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 5 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 3 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.STOP,) async def test_hmip_garage_door_hoermann( @@ -414,14 +420,16 @@ async def test_hmip_garage_door_hoermann( assert ha_state.state == "closed" assert ha_state.attributes["current_position"] == 0 - service_call_counter = len(hmip_device.mock_calls) + service_call_counter = len(hmip_device.functionalChannels[1].mock_calls) await hass.services.async_call( "cover", "open_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 1 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.OPEN,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.OPEN @@ -430,9 +438,11 @@ async def test_hmip_garage_door_hoermann( await hass.services.async_call( "cover", "close_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 2 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.CLOSE,) await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) ha_state = hass.states.get(entity_id) assert ha_state.state == CoverState.CLOSED @@ -441,9 +451,11 @@ async def test_hmip_garage_door_hoermann( await hass.services.async_call( "cover", "stop_cover", {"entity_id": entity_id}, blocking=True ) - assert len(hmip_device.mock_calls) == service_call_counter + 5 - assert hmip_device.mock_calls[-1][0] == "send_door_command_async" - assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) + assert len(hmip_device.functionalChannels[1].mock_calls) == service_call_counter + 3 + assert ( + hmip_device.functionalChannels[1].mock_calls[-1][0] == "async_send_door_command" + ) + assert hmip_device.functionalChannels[1].mock_calls[-1][1] == (DoorCommand.STOP,) async def test_hmip_cover_shutter_group( diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index a07c0745c45..967672580ec 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -70,7 +70,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -80,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 3224a0cc63e..972b7fc5728 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -79,7 +79,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -89,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -174,7 +172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -184,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_select.ambr b/tests/components/homewizard/snapshots/test_select.ambr index ecfd80e04da..0797256120c 100644 --- a/tests/components/homewizard/snapshots/test_select.ambr +++ b/tests/components/homewizard/snapshots/test_select.ambr @@ -80,7 +80,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -90,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 9f95e140edc..1bde08b3201 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -21,7 +21,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -109,7 +107,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -119,7 +116,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -202,7 +198,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -212,7 +207,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -295,7 +289,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -305,7 +298,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -388,7 +380,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -398,7 +389,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -481,7 +471,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -491,7 +480,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -574,7 +562,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -584,7 +571,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -667,7 +653,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -677,7 +662,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -753,7 +737,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -763,7 +746,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -846,7 +828,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -856,7 +837,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -935,7 +915,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -945,7 +924,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.00', 'via_device_id': None, }) @@ -1020,7 +998,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1030,7 +1007,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1113,7 +1089,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1123,7 +1098,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1206,7 +1180,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1216,7 +1189,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1299,7 +1271,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1309,7 +1280,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1392,7 +1362,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1402,7 +1371,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1485,7 +1453,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1495,7 +1462,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1578,7 +1544,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1588,7 +1553,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1668,7 +1632,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1678,7 +1641,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1761,7 +1723,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1771,7 +1732,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1854,7 +1814,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1864,7 +1823,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -1939,7 +1897,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -1949,7 +1906,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2028,7 +1984,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2038,7 +1993,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2121,7 +2075,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2131,7 +2084,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2214,7 +2166,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2224,7 +2175,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2307,7 +2257,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2317,7 +2266,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2400,7 +2348,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2410,7 +2357,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2493,7 +2439,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2503,7 +2448,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2586,7 +2530,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2596,7 +2539,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2679,7 +2621,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2689,7 +2630,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2772,7 +2712,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2782,7 +2721,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2865,7 +2803,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2875,7 +2812,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -2958,7 +2894,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -2968,7 +2903,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3051,7 +2985,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3061,7 +2994,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3144,7 +3076,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3154,7 +3085,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3234,7 +3164,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3244,7 +3173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3324,7 +3252,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3334,7 +3261,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3414,7 +3340,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3424,7 +3349,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3507,7 +3431,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3517,7 +3440,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3600,7 +3522,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3610,7 +3531,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3693,7 +3613,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3703,7 +3622,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3786,7 +3704,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3796,7 +3713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3879,7 +3795,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3889,7 +3804,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -3972,7 +3886,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -3982,7 +3895,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4065,7 +3977,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4075,7 +3986,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4158,7 +4068,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4168,7 +4077,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4251,7 +4159,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4261,7 +4168,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4344,7 +4250,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4354,7 +4259,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4429,7 +4333,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4439,7 +4342,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -4518,7 +4420,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4528,7 +4429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4608,7 +4508,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4618,7 +4517,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4701,7 +4599,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4711,7 +4608,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4794,7 +4690,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4804,7 +4699,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4887,7 +4781,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4897,7 +4790,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -4972,7 +4864,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -4982,7 +4873,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5065,7 +4955,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5075,7 +4964,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5158,7 +5046,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5168,7 +5055,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5251,7 +5137,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5261,7 +5146,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5344,7 +5228,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5354,7 +5237,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5437,7 +5319,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5447,7 +5328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5530,7 +5410,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5540,7 +5419,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5623,7 +5501,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5633,7 +5510,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5716,7 +5592,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5726,7 +5601,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5809,7 +5683,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5819,7 +5692,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5902,7 +5774,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -5912,7 +5783,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -5995,7 +5865,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6005,7 +5874,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6080,7 +5948,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6090,7 +5957,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6170,7 +6036,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6180,7 +6045,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6263,7 +6127,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6273,7 +6136,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6348,7 +6210,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6358,7 +6219,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6441,7 +6301,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6451,7 +6310,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6534,7 +6392,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6544,7 +6401,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6627,7 +6483,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6637,7 +6492,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6712,7 +6566,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6722,7 +6575,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6797,7 +6649,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6807,7 +6658,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6896,7 +6746,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6906,7 +6755,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -6989,7 +6837,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -6999,7 +6846,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7082,7 +6928,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7092,7 +6937,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7175,7 +7019,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7185,7 +7028,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7268,7 +7110,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7278,7 +7119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7353,7 +7193,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7363,7 +7202,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7438,7 +7276,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7448,7 +7285,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7523,7 +7359,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7533,7 +7368,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7608,7 +7442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7618,7 +7451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7693,7 +7525,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7703,7 +7534,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7778,7 +7608,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7788,7 +7617,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7867,7 +7695,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7877,7 +7704,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -7952,7 +7778,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -7962,7 +7787,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8037,7 +7861,6 @@ 'gas_meter_G001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8047,7 +7870,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_G001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8126,7 +7948,6 @@ 'heat_meter_H001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8136,7 +7957,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_H001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8215,7 +8035,6 @@ 'inlet_heat_meter_IH001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8225,7 +8044,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_IH001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8300,7 +8118,6 @@ 'warm_water_meter_WW001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8310,7 +8127,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_WW001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8389,7 +8205,6 @@ 'water_meter_W001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8399,7 +8214,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_W001', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -8482,7 +8296,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8492,7 +8305,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8572,7 +8384,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8582,7 +8393,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8665,7 +8475,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8675,7 +8484,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8758,7 +8566,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8768,7 +8575,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8851,7 +8657,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8861,7 +8666,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -8936,7 +8740,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -8946,7 +8749,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9029,7 +8831,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9039,7 +8840,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9122,7 +8922,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9132,7 +8931,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9215,7 +9013,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9225,7 +9022,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9308,7 +9104,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9318,7 +9113,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9401,7 +9195,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9411,7 +9204,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9494,7 +9286,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9504,7 +9295,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9587,7 +9377,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9597,7 +9386,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9680,7 +9468,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9690,7 +9477,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9773,7 +9559,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9783,7 +9568,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9866,7 +9650,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9876,7 +9659,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -9959,7 +9741,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -9969,7 +9750,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10044,7 +9824,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10054,7 +9833,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10134,7 +9912,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10144,7 +9921,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10227,7 +10003,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10237,7 +10012,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10312,7 +10086,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10322,7 +10095,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10405,7 +10177,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10415,7 +10186,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10498,7 +10268,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10508,7 +10277,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10591,7 +10359,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10601,7 +10368,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10676,7 +10442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10686,7 +10451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10761,7 +10525,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10771,7 +10534,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10860,7 +10622,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10870,7 +10631,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -10953,7 +10713,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -10963,7 +10722,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11046,7 +10804,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11056,7 +10813,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11139,7 +10895,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11149,7 +10904,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11232,7 +10986,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11242,7 +10995,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11317,7 +11069,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11327,7 +11078,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11402,7 +11152,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11412,7 +11161,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11487,7 +11235,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11497,7 +11244,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11572,7 +11318,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11582,7 +11327,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11657,7 +11401,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11667,7 +11410,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11742,7 +11484,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11752,7 +11493,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11831,7 +11571,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11841,7 +11580,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -11916,7 +11654,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -11926,7 +11663,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12001,7 +11737,6 @@ 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12011,7 +11746,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12090,7 +11824,6 @@ 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12100,7 +11833,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12179,7 +11911,6 @@ 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12189,7 +11920,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12264,7 +11994,6 @@ 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12274,7 +12003,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12353,7 +12081,6 @@ 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12363,7 +12090,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -12446,7 +12172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12456,7 +12181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12536,7 +12260,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12546,7 +12269,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12629,7 +12351,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12639,7 +12360,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12722,7 +12442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12732,7 +12451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12815,7 +12533,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12825,7 +12542,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -12908,7 +12624,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -12918,7 +12633,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13001,7 +12715,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13011,7 +12724,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13094,7 +12806,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13104,7 +12815,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13187,7 +12897,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13197,7 +12906,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13280,7 +12988,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13290,7 +12997,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13373,7 +13079,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13383,7 +13088,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13466,7 +13170,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13476,7 +13179,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13559,7 +13261,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13569,7 +13270,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13652,7 +13352,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13662,7 +13361,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13745,7 +13443,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13755,7 +13452,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13838,7 +13534,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13848,7 +13543,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -13923,7 +13617,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -13933,7 +13626,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14016,7 +13708,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14026,7 +13717,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14101,7 +13791,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14111,7 +13800,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14194,7 +13882,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14204,7 +13891,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14287,7 +13973,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14297,7 +13982,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14380,7 +14064,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14390,7 +14073,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14473,7 +14155,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14483,7 +14164,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14566,7 +14246,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14576,7 +14255,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14659,7 +14337,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14669,7 +14346,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14752,7 +14428,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14762,7 +14437,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14837,7 +14511,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14847,7 +14520,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -14922,7 +14594,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -14932,7 +14603,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15007,7 +14677,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15017,7 +14686,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15092,7 +14760,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15102,7 +14769,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15177,7 +14843,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15187,7 +14852,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15262,7 +14926,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15272,7 +14935,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15351,7 +15013,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15361,7 +15022,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15436,7 +15096,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15446,7 +15105,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.19', 'via_device_id': None, }) @@ -15525,7 +15183,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15535,7 +15192,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15618,7 +15274,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15628,7 +15283,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15711,7 +15365,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15721,7 +15374,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15804,7 +15456,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15814,7 +15465,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15897,7 +15547,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15907,7 +15556,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -15982,7 +15630,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -15992,7 +15639,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -16071,7 +15717,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16081,7 +15726,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16164,7 +15808,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16174,7 +15817,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16257,7 +15899,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16267,7 +15908,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16350,7 +15990,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16360,7 +15999,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16443,7 +16081,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16453,7 +16090,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16536,7 +16172,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16546,7 +16181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16629,7 +16263,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16639,7 +16272,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16719,7 +16351,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16729,7 +16360,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16812,7 +16442,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16822,7 +16451,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16905,7 +16533,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -16915,7 +16542,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -16998,7 +16624,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17008,7 +16633,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17083,7 +16707,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17093,7 +16716,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -17172,7 +16794,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17182,7 +16803,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17265,7 +16885,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17275,7 +16894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17354,7 +16972,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17364,7 +16981,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17439,7 +17055,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17449,7 +17064,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -17528,7 +17142,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17538,7 +17151,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17621,7 +17233,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17631,7 +17242,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17714,7 +17324,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17724,7 +17333,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17807,7 +17415,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17817,7 +17424,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17900,7 +17506,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -17910,7 +17515,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -17993,7 +17597,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18003,7 +17606,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18086,7 +17688,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18096,7 +17697,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18176,7 +17776,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18186,7 +17785,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18269,7 +17867,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18279,7 +17876,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18362,7 +17958,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18372,7 +17967,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18447,7 +18041,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18457,7 +18050,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18536,7 +18128,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18546,7 +18137,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18629,7 +18219,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18639,7 +18228,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18722,7 +18310,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18732,7 +18319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18815,7 +18401,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18825,7 +18410,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -18908,7 +18492,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -18918,7 +18501,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19001,7 +18583,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19011,7 +18592,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19094,7 +18674,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19104,7 +18683,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19187,7 +18765,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19197,7 +18774,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19280,7 +18856,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19290,7 +18865,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19373,7 +18947,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19383,7 +18956,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19466,7 +19038,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19476,7 +19047,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19559,7 +19129,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19569,7 +19138,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19652,7 +19220,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19662,7 +19229,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19742,7 +19308,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19752,7 +19317,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19832,7 +19396,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19842,7 +19405,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -19922,7 +19484,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -19932,7 +19493,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20015,7 +19575,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20025,7 +19584,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20108,7 +19666,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20118,7 +19675,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20201,7 +19757,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20211,7 +19766,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20294,7 +19848,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20304,7 +19857,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20387,7 +19939,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20397,7 +19948,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20480,7 +20030,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20490,7 +20039,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20573,7 +20121,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20583,7 +20130,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20666,7 +20212,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20676,7 +20221,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20759,7 +20303,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20769,7 +20312,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20852,7 +20394,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20862,7 +20403,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -20937,7 +20477,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -20947,7 +20486,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index c4e67003b58..d61979c84b5 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -69,7 +69,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -79,7 +78,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -154,7 +152,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -164,7 +161,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -240,7 +236,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -250,7 +245,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -325,7 +319,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -335,7 +328,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -410,7 +402,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -420,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.03', 'via_device_id': None, }) @@ -496,7 +486,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -506,7 +495,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -581,7 +569,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -591,7 +578,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -666,7 +652,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -676,7 +661,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.07', 'via_device_id': None, }) @@ -751,7 +735,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -761,7 +744,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.03', 'via_device_id': None, }) @@ -836,7 +818,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -846,7 +827,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) @@ -921,7 +901,6 @@ '5c2fafabcdef', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'HomeWizard', @@ -931,7 +910,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '3.06', 'via_device_id': None, }) diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 807996f1093..5f287b1d8e3 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -11,7 +11,11 @@ from homeassistant.components.light import ColorMode from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.util import color as color_util from .conftest import create_config_entry @@ -776,6 +780,7 @@ def test_hs_color() -> None: async def test_group_features( hass: HomeAssistant, + area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, mock_bridge_v1: Mock, @@ -966,16 +971,22 @@ async def test_group_features( entry = entity_registry.async_get("light.hue_lamp_1") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area is None + assert device_entry.area_id is None entry = entity_registry.async_get("light.hue_lamp_2") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_3") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Living Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living Room").id + ) entry = entity_registry.async_get("light.hue_lamp_4") device_entry = device_registry.async_get(entry.device_id) - assert device_entry.suggested_area == "Dining Room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Dining Room").id + ) diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 1428a75d7b4..82116391f4f 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'c7233734-b219-4287-a173-08e3643f89f0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Husqvarna', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': 'Garden', 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 3aa3504cc26..6628113d8c3 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -205,10 +205,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -219,6 +219,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -255,6 +258,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -268,6 +272,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -283,6 +288,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -300,13 +307,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -372,10 +372,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -386,6 +386,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -422,6 +425,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -435,6 +439,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -450,6 +455,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -467,13 +474,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , @@ -1568,10 +1568,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1582,6 +1582,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1618,6 +1621,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1631,6 +1635,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1646,6 +1651,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1663,13 +1670,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'config_entry_id': , @@ -1735,10 +1735,10 @@ 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', 'cutting_height_blocked', + 'cutting_height_problem', 'cutting_height_problem_curr', 'cutting_height_problem_dir', 'cutting_height_problem_drive', - 'cutting_height_problem', 'cutting_motor_problem', 'cutting_stopped_slope_too_steep', 'cutting_system_blocked', @@ -1749,6 +1749,9 @@ 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'error', + 'error_at_power_up', + 'fatal_error', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1785,6 +1788,7 @@ 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', + 'off', 'outside_working_area', 'poor_signal_quality', 'reference_station_communication_problem', @@ -1798,6 +1802,7 @@ 'slope_too_steep', 'sms_could_not_be_sent', 'stop_button_problem', + 'stopped', 'stuck_in_charging_station', 'switch_cord_problem', 'temporary_battery_problem', @@ -1813,6 +1818,8 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'wait_power_up', + 'wait_updating', 'weak_gps_signal', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', @@ -1830,13 +1837,6 @@ 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', - 'error_at_power_up', - 'error', - 'fatal_error', - 'off', - 'stopped', - 'wait_power_up', - 'wait_updating', ]), }), 'context': , diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index b7aa14ef0bf..6b4ab8236f9 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '00000000-0000-0000-0000-000000000003_1197489078', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Husqvarna', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..8f2bfadf56a --- /dev/null +++ b/tests/components/husqvarna_automower_ble/snapshots/test_sensor.ambr @@ -0,0 +1,54 @@ +# serializer version: 1 +# name: test_setup[sensor.husqvarna_automower_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.husqvarna_automower_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'husqvarna_automower_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000003_1197489078_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[sensor.husqvarna_automower_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Husqvarna AutoMower Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.husqvarna_automower_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/husqvarna_automower_ble/test_sensor.py b/tests/components/husqvarna_automower_ble/test_sensor.py new file mode 100644 index 00000000000..d1f0a13cc43 --- /dev/null +++ b/tests/components/husqvarna_automower_ble/test_sensor.py @@ -0,0 +1,32 @@ +"""Test the Husqvarna Automower Bluetooth setup.""" + +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = pytest.mark.usefixtures("mock_automower_client") + + +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup creates expected entities.""" + + with patch( + "homeassistant.components.husqvarna_automower_ble.PLATFORMS", [Platform.SENSOR] + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 058a5d35cd0..41e79911154 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -29,7 +29,6 @@ 'TestLS', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'iotty', @@ -39,7 +38,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index 7329eec7f70..02076bf5597 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '26e93f1a-c828-11ea-87d0-0242ac130003', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ista SE', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'eaf5c5c8-889f-4a3c-b68c-e9a676505762', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ista SE', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ituran/snapshots/test_init.ambr b/tests/components/ituran/snapshots/test_init.ambr index b97aef6027b..5fb786029b4 100644 --- a/tests/components/ituran/snapshots/test_init.ambr +++ b/tests/components/ituran/snapshots/test_init.ambr @@ -18,7 +18,6 @@ '12345678', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'mock make', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '12345678', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py index a63d9abb9a7..234cae2adca 100644 --- a/tests/components/jewish_calendar/test_config_flow.py +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -110,7 +110,6 @@ async def test_options_reconfigure( CONF_CANDLE_LIGHT_MINUTES: DEFAULT_CANDLE_LIGHT + 1, }, ) - assert result["result"] # The value of the "upcoming_shabbat_candle_lighting" sensor should be the new value assert config_entry.options[CONF_CANDLE_LIGHT_MINUTES] == DEFAULT_CANDLE_LIGHT + 1 diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 9c9f31a2544..2bee2f1f61c 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -65,7 +65,6 @@ 'outlet_1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -75,7 +74,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -98,7 +96,6 @@ '2_ch_power_strip', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -108,7 +105,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -179,7 +175,6 @@ 'outlet_2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -189,7 +184,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -212,7 +206,6 @@ '2_ch_power_strip', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -222,7 +215,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/lamarzocco/snapshots/test_init.ambr b/tests/components/lamarzocco/snapshots/test_init.ambr index 18b2fd0fbc3..bdebd35d6dd 100644 --- a/tests/components/lamarzocco/snapshots/test_init.ambr +++ b/tests/components/lamarzocco/snapshots/test_init.ambr @@ -25,7 +25,6 @@ 'GS012345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'La Marzocco', @@ -35,7 +34,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'GS012345', - 'suggested_area': None, 'sw_version': 'v1.17', 'via_device_id': None, }) diff --git a/tests/components/lektrico/snapshots/test_init.ambr b/tests/components/lektrico/snapshots/test_init.ambr index 35183bf5d75..e1b5a48fe27 100644 --- a/tests/components/lektrico/snapshots/test_init.ambr +++ b/tests/components/lektrico/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '500006', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Lektrico', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '500006', - 'suggested_area': None, 'sw_version': '1.44', 'via_device_id': None, }) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index e2a35bcb1b1..1b09d742876 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -14,7 +14,11 @@ from homeassistant.components.lifx.const import CONF_SERIAL from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ( ATTR_PROPERTIES_ID, @@ -585,6 +589,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: async def test_suggested_area( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -624,4 +629,4 @@ async def test_suggested_area( entity = entity_registry.async_get(entity_id) device = device_registry.async_get(entity.device_id) - assert device.suggested_area == "My LIFX Group" + assert device.area_id == area_registry.async_get_area_by_name("My LIFX Group").id diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr index 46fb4c1d4e0..662ffd51cb4 100644 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'trwnh_mastodon_social', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Mastodon gGmbH', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '4.4.0-nightly.2025-02-07', 'via_device_id': None, }) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 8e724e4d8ea..422b1c3de44 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -8,6 +8,7 @@ from aiomealie import ( Mealplan, MealplanResponse, Recipe, + RecipesResponse, ShoppingItemsResponse, ShoppingListsResponse, Statistics, @@ -63,6 +64,8 @@ def mock_mealie_client() -> Generator[AsyncMock]: ) recipe = Recipe.from_json(load_fixture("get_recipe.json", DOMAIN)) client.get_recipe.return_value = recipe + recipes = RecipesResponse.from_json(load_fixture("get_recipes.json", DOMAIN)) + client.get_recipes.return_value = recipes client.import_recipe.return_value = recipe client.get_shopping_lists.return_value = ShoppingListsResponse.from_json( load_fixture("get_shopping_lists.json", DOMAIN) diff --git a/tests/components/mealie/fixtures/get_recipe.json b/tests/components/mealie/fixtures/get_recipe.json index a5ccd1876e5..7e42986ebdc 100644 --- a/tests/components/mealie/fixtures/get_recipe.json +++ b/tests/components/mealie/fixtures/get_recipe.json @@ -63,8 +63,6 @@ "unit": null, "food": null, "note": "130g dark couverture chocolate (min. 55% cocoa content)", - "isFood": true, - "disableAmount": false, "display": "1 130g dark couverture chocolate (min. 55% cocoa content)", "title": null, "originalText": null, @@ -87,8 +85,6 @@ "unit": null, "food": null, "note": "150g softened butter", - "isFood": true, - "disableAmount": false, "display": "1 150g softened butter", "title": null, "originalText": null, diff --git a/tests/components/mealie/fixtures/get_recipes.json b/tests/components/mealie/fixtures/get_recipes.json new file mode 100644 index 00000000000..8ee91a1aa0e --- /dev/null +++ b/tests/components/mealie/fixtures/get_recipes.json @@ -0,0 +1,1692 @@ +{ + "page": 1, + "per_page": 50, + "total": 662, + "total_pages": 14, + "items": [ + { + "id": "e82f5449-c33b-437c-b712-337587199264", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "tu6y", + "slug": "tu6y", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T11:10:14.866359", + "createdAt": "2024-01-21T11:10:14.880721", + "updateAt": "2024-01-21T11:10:14.880723", + "lastMade": null + }, + { + "id": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno-1", + "image": "En9o", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:08:58.056854", + "createdAt": "2024-01-21T09:08:58.059401", + "updateAt": "2024-01-21T09:08:58.059403", + "lastMade": null + }, + { + "id": "90097c8b-9d80-468a-b497-73957ac0cd8b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Patates douces au four (1)", + "slug": "patates-douces-au-four-1", + "image": "aAhk", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T10:27:39.409746", + "createdAt": "2024-01-21T09:08:53.846294", + "updateAt": "2024-01-21T09:08:53.846295", + "lastMade": null + }, + { + "id": "98845807-9365-41fd-acd1-35630b468c27", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Sweet potatoes", + "slug": "sweet-potatoes", + "image": "kdhm", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T10:28:05.977615", + "createdAt": "2024-01-21T09:08:53.846294", + "updateAt": "2024-01-21T09:08:53.846295", + "lastMade": null + }, + { + "id": "40c227e0-3c7e-41f7-866d-5de04eaecdd7", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno", + "image": "tNbG", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:06:44.015829", + "createdAt": "2024-01-21T09:06:44.019650", + "updateAt": "2024-01-21T09:06:44.019653", + "lastMade": null + }, + { + "id": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Boeuf bourguignon : la vraie recette (2)", + "slug": "boeuf-bourguignon-la-vraie-recette-2", + "image": "nj5M", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:45:28.780361", + "createdAt": "2024-01-21T08:45:28.782322", + "updateAt": "2024-01-21T08:45:28.782324", + "lastMade": null + }, + { + "id": "fc42c7d1-7b0f-4e04-b88a-dbd80b81540b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Boeuf bourguignon : la vraie recette (1)", + "slug": "boeuf-bourguignon-la-vraie-recette-1", + "image": "rbU7", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:43:36.105722", + "createdAt": "2024-01-21T08:43:36.108116", + "updateAt": "2024-01-21T08:43:36.108118", + "lastMade": null + }, + { + "id": "89e63d72-7a51-4cef-b162-2e45035d0a91", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Veganes Marmor-Bananenbrot mit Erdnussbutter", + "slug": "veganes-marmor-bananenbrot-mit-erdnussbutter", + "image": "JSp3", + "recipeYield": "14 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "55 Minutes", + "description": "Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:28:11.008440", + "createdAt": "2024-01-21T08:28:11.011427", + "updateAt": "2024-01-21T08:28:11.011428", + "lastMade": null + }, + { + "id": "eab64457-97ba-4d6c-871c-cb1c724ccb51", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin", + "slug": "pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin", + "image": "9QMh", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:24:50.952774", + "createdAt": "2024-01-21T08:24:50.955843", + "updateAt": "2024-01-21T08:24:50.955845", + "lastMade": null + }, + { + "id": "12439e3d-3c1c-4dcc-9c6e-4afcea2a0542", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test123", + "slug": "test123", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:00:02.755328", + "createdAt": "2024-01-21T08:00:02.757103", + "updateAt": "2024-01-21T08:00:02.757105", + "lastMade": null + }, + { + "id": "6567f6ec-e410-49cb-a1a5-d08517184e78", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Bureeto", + "slug": "bureeto", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:37:39.940578", + "createdAt": "2024-01-21T07:37:39.942535", + "updateAt": "2024-01-21T07:37:39.942537", + "lastMade": null + }, + { + "id": "f7737d17-161c-4008-88d4-dd2616778cd0", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Subway Double Cookies", + "slug": "subway-double-cookies", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:34:53.944858", + "createdAt": "2024-01-21T07:34:53.946852", + "updateAt": "2024-01-21T07:34:53.946854", + "lastMade": null + }, + { + "id": "1904b717-4a8b-4de9-8909-56958875b5f4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "qwerty12345", + "slug": "qwerty12345", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:37:55.795675", + "createdAt": "2024-01-21T07:28:05.395272", + "updateAt": "2024-01-21T07:28:05.395274", + "lastMade": null + }, + { + "id": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Cheeseburger Sliders (Easy, 30-min Recipe)", + "slug": "cheeseburger-sliders-easy-30-min-recipe", + "image": "beGq", + "recipeYield": "24 servings", + "totalTime": "30 Minutes", + "prepTime": "8 Minutes", + "cookTime": null, + "performTime": "22 Minutes", + "description": "Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.", + "recipeCategory": [], + "tags": [ + { + "id": "7a4ca427-642f-4428-8dc7-557ea9c8d1b4", + "name": "Cheeseburger Sliders", + "slug": "cheeseburger-sliders" + }, + { + "id": "941558d2-50d5-4c9d-8890-a0258f18d493", + "name": "Sliders", + "slug": "sliders" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://natashaskitchen.com/cheeseburger-sliders/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:43:24.261010", + "createdAt": "2024-01-21T06:49:35.466777", + "updateAt": "2024-01-21T06:49:35.466778", + "lastMade": "2024-01-22T04:59:59" + }, + { + "id": "8a30d31d-aa14-411e-af0c-6b61a94f5291", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "meatloaf", + "slug": "meatloaf", + "image": null, + "recipeYield": "4", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T06:37:09.426467", + "createdAt": "2024-01-21T06:36:57.645658", + "updateAt": "2024-01-21T06:37:09.428351", + "lastMade": null + }, + { + "id": "f2f7880b-1136-436f-91b7-129788d8c117", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Richtig rheinischer Sauerbraten", + "slug": "richtig-rheinischer-sauerbraten", + "image": "kCBh", + "recipeYield": "4 servings", + "totalTime": "3 Hours 20 Minutes", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "2 Hours 20 Minutes", + "description": "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": 3, + "orgURL": "https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T05:37:55.419788", + "createdAt": "2024-01-21T05:24:03.402973", + "updateAt": "2024-01-21T05:37:55.422471", + "lastMade": null + }, + { + "id": "cf634591-0f82-4254-8e00-2f7e8b0c9022", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Orientalischer Gemüse-Hähnchen Eintopf", + "slug": "orientalischer-gemuse-hahnchen-eintopf", + "image": "kpBx", + "recipeYield": "6 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [ + { + "id": "518f3081-a919-4c80-9cad-75ffbd0e73d3", + "name": "Gemüse", + "slug": "gemuse" + }, + { + "id": "a3fff625-1902-4112-b169-54aec4f52ea7", + "name": "Hauptspeise", + "slug": "hauptspeise" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "1f87d43d-7d9d-4806-993a-fdb89117d64e", + "name": "Fleisch", + "slug": "fleisch" + }, + { + "id": "7caa64df-c65d-4fb0-9075-b788e6a05e1d", + "name": "Geflügel", + "slug": "geflugel" + }, + { + "id": "38d18d57-d817-491e-94f8-da923d2c540e", + "name": "Eintopf", + "slug": "eintopf" + }, + { + "id": "398fbd98-4175-4652-92a4-51e55482dc9b", + "name": "Schmoren", + "slug": "schmoren" + }, + { + "id": "ec303c13-a4f7-4de3-8a4f-d13b72ddd500", + "name": "Hülsenfrüchte", + "slug": "hulsenfruchte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:58:54.661618", + "createdAt": "2024-01-21T04:58:54.665601", + "updateAt": "2024-01-21T04:58:54.665603", + "lastMade": null + }, + { + "id": "05208856-d273-4cc9-bcfa-e0215d57108d", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 20240121", + "slug": "test-20240121", + "image": null, + "recipeYield": "4", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:56:20.569413", + "createdAt": "2024-01-21T04:55:49.820247", + "updateAt": "2024-01-21T04:56:20.571564", + "lastMade": null + }, + { + "id": "145eeb05-781a-4eb0-a656-afa8bc8c0164", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Loempia bowl", + "slug": "loempia-bowl", + "image": "McEx", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.lekkerensimpel.com/loempia-bowl/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:39:48.558572", + "createdAt": "2024-01-21T04:39:48.560422", + "updateAt": "2024-01-21T04:39:48.560424", + "lastMade": null + }, + { + "id": "5c6532aa-ad84-424c-bc05-c32d50430fe4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "5 Ingredient Chocolate Mousse", + "slug": "5-ingredient-chocolate-mousse", + "image": "bzqo", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": null, + "description": "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://thehappypear.ie/aquafaba-chocolate-mousse/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T06:06:26.305680", + "createdAt": "2024-01-21T04:14:34.624708", + "updateAt": "2024-01-21T06:06:26.308017", + "lastMade": null + }, + { + "id": "f2e684f2-49e0-45ee-90de-951344472f1c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Der perfekte Pfannkuchen - gelingt einfach immer", + "slug": "der-perfekte-pfannkuchen-gelingt-einfach-immer", + "image": "KGK6", + "recipeYield": "4 servings", + "totalTime": "15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "10 Minutes", + "description": "Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [ + { + "id": "4ec445c6-fc2f-4a1e-b666-93435a46ec42", + "name": "Schnell", + "slug": "schnell" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "66bc0f60-ff95-44e4-afef-8437b2c2d9af", + "name": "Backen", + "slug": "backen" + }, + { + "id": "48d2a71c-ed17-4c07-bf9f-bc9216936f54", + "name": "Kuchen", + "slug": "kuchen" + }, + { + "id": "b2821b25-94ea-4576-b488-276331b3d76e", + "name": "Kinder", + "slug": "kinder" + }, + { + "id": "fee5e626-792c-479d-a265-81a0029047f2", + "name": "Mehlspeisen", + "slug": "mehlspeisen" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:06:40.503968", + "createdAt": "2024-01-21T04:04:43.296547", + "updateAt": "2024-01-21T04:06:40.506886", + "lastMade": null + }, + { + "id": "cf239441-b75d-4dea-a48e-9d99b7cb5842", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Dinkel-Sauerteigbrot", + "slug": "dinkel-sauerteigbrot", + "image": "yNDq", + "recipeYield": "1", + "totalTime": "24h", + "prepTime": "1h", + "cookTime": null, + "performTime": "35min", + "description": "Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.", + "recipeCategory": [ + { + "id": "6d54ca14-eb71-4d3a-933d-5e88f68edb68", + "name": "Brot", + "slug": "brot" + } + ], + "tags": [ + { + "id": "0f80c5d5-d1ee-41ac-a949-54a76b446459", + "name": "Sourdough", + "slug": "sourdough" + } + ], + "tools": [ + { + "id": "1170e609-20d3-45b8-b0c7-3a4cfa614e88", + "name": "Backofen", + "slug": "backofen", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.besondersgut.ch/dinkel-sauerteigbrot/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:57:41.588112", + "createdAt": "2024-01-21T03:44:30.512149", + "updateAt": "2024-01-21T03:44:30.512151", + "lastMade": null + }, + { + "id": "2673eb90-6d78-4b95-af36-5db8c8a6da37", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 234234", + "slug": "test-234234", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T04:07:55.643655", + "createdAt": "2024-01-21T03:14:59.852966", + "updateAt": "2024-01-21T04:07:55.646291", + "lastMade": null + }, + { + "id": "0a723c54-af53-40e9-a15f-c87aae5ac688", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "test 243", + "slug": "test-243", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T02:20:32.570339", + "createdAt": "2024-01-21T02:20:32.572744", + "updateAt": "2024-01-21T02:20:32.572746", + "lastMade": null + }, + { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + }, + { + "id": "9d3cb303-a996-4144-948a-36afaeeef554", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tarta cytrynowa z bezą", + "slug": "tarta-cytrynowa-z-beza", + "image": "vxuL", + "recipeYield": "8 servings", + "totalTime": "1 Hour", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": null, + "description": "Tarta cytrynowa z bezą\r\nLekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko.\r\nDla kogo?\r\nLubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem!\r\nNa jaką okazję?\r\nNa rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby.\r\nCzy wiesz, że?\r\nZastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku.\r\nDla urozmaicenia:\r\nMartwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:27:12.082247", + "createdAt": "2024-01-21T01:27:12.088594", + "updateAt": "2024-01-21T01:27:12.088596", + "lastMade": null + }, + { + "id": "77f05a49-e869-4048-aa62-0d8a1f5a8f1c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Martins test Recipe", + "slug": "martins-test-recipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:26:38.793372", + "createdAt": "2024-01-21T01:26:38.802872", + "updateAt": "2024-01-21T01:26:38.802874", + "lastMade": null + }, + { + "id": "75a90207-9c10-4390-a265-c47a4b67fd69", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Muffinki czekoladowe", + "slug": "muffinki-czekoladowe", + "image": "xP1Q", + "recipeYield": "12", + "totalTime": null, + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "30 Minutes", + "description": "Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.", + "recipeCategory": [], + "tags": [ + { + "id": "ed2eed99-1285-4507-b5cb-b3047d64855c", + "name": "Muffinki Czekoladowe", + "slug": "muffinki-czekoladowe" + }, + { + "id": "e94d5223-5337-4e1b-b36e-7968c8823176", + "name": "Babeczki I Muffiny", + "slug": "babeczki-i-muffiny" + }, + { + "id": "2d06a44a-331a-4922-abb4-8047ee5e7c1c", + "name": "Sylwester", + "slug": "sylwester" + }, + { + "id": "c78edd8c-c96b-43fb-86c0-917ea5a08ac7", + "name": "Wegetariańska", + "slug": "wegetarianska" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://aniagotuje.pl/przepis/muffinki-czekoladowe", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:25:53.529639", + "createdAt": "2024-01-21T01:25:03.838184", + "updateAt": "2024-01-21T01:25:53.534515", + "lastMade": null + }, + { + "id": "4320ba72-377b-4657-8297-dce198f24cdf", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "My Test Recipe", + "slug": "my-test-recipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:22:10.331488", + "createdAt": "2024-01-21T01:22:10.361617", + "updateAt": "2024-01-21T01:22:10.361618", + "lastMade": null + }, + { + "id": "98dac844-31ee-426a-b16c-fb62a5dd2816", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "My Test Receipe", + "slug": "my-test-receipe", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T01:22:10.309993", + "createdAt": "2024-01-21T01:22:10.357806", + "updateAt": "2024-01-21T01:22:10.357807", + "lastMade": null + }, + { + "id": "c3c8f207-c704-415d-81b1-da9f032cf52f", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Patates douces au four", + "slug": "patates-douces-au-four", + "image": "r1ck", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T00:34:57.419501", + "createdAt": "2024-01-21T00:34:57.422137", + "updateAt": "2024-01-21T00:34:57.422139", + "lastMade": null + }, + { + "id": "1edb2f6e-133c-4be0-b516-3c23625a97ec", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Easy Homemade Pizza Dough", + "slug": "easy-homemade-pizza-dough", + "image": "gD94", + "recipeYield": "2 servings", + "totalTime": "2 Hours 30 Minutes", + "prepTime": "2 Hours 15 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T22:41:09.255367", + "createdAt": "2024-01-20T22:41:09.258070", + "updateAt": "2024-01-20T22:41:09.258071", + "lastMade": null + }, + { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + }, + { + "id": "6530ea6e-401e-4304-8a7a-12162ddf5b9c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + "slug": "serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce", + "image": "4Sys", + "recipeYield": "4 servings", + "totalTime": "2 Hours 15 Minutes", + "prepTime": "20 Minutes", + "cookTime": null, + "performTime": "55 Minutes", + "description": "This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.", + "recipeCategory": [], + "tags": [ + { + "id": "d7aea128-0e7b-4e0c-a236-e500717701bb", + "name": "Rice", + "slug": "rice" + }, + { + "id": "1dd3541c-ed6b-4a25-b829-9a71358409ef", + "name": "Chicken", + "slug": "chicken" + }, + { + "id": "eb871b57-ea46-4cb5-88a5-98064514e593", + "name": "Chicken And Rice", + "slug": "chicken-and-rice" + }, + { + "id": "2b0a0ed2-e799-4ab2-8a24-d5ce15827a8e", + "name": "Cook The Book", + "slug": "cook-the-book" + }, + { + "id": "e6783087-0cee-4f31-b588-268380f75335", + "name": "Halal", + "slug": "halal" + }, + { + "id": "a2d99845-8bd0-4a2a-9a56-f8a34f51039e", + "name": "Middle Eastern", + "slug": "middle-eastern" + }, + { + "id": "6b7b95b0-b3f8-467f-857d-ef036009d5e1", + "name": "New York City", + "slug": "new-york-city" + }, + { + "id": "6bd6c577-9d00-411f-88de-b8679c37ac58", + "name": "Serious Eats Book", + "slug": "serious-eats-book" + }, + { + "id": "d77a2071-43ae-40b1-854d-ae995a766fba", + "name": "Street Food", + "slug": "street-food" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T20:32:14.736668", + "createdAt": "2024-01-20T20:25:43.655397", + "updateAt": "2024-01-20T20:32:14.740947", + "lastMade": null + }, + { + "id": "c496cf9c-1ece-448a-9d3f-ef772f078a4e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Schnelle Käsespätzle", + "slug": "schnelle-kasespatzle", + "image": "8goY", + "recipeYield": "4 servings", + "totalTime": "40 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "30 Minutes", + "description": "Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T18:31:51.652135", + "createdAt": "2024-01-20T18:31:51.654414", + "updateAt": "2024-01-20T18:31:51.654415", + "lastMade": null + }, + { + "id": "49aa6f42-6760-4adf-b6cd-59592da485c3", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "taco", + "slug": "taco", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T17:25:27.960087", + "createdAt": "2024-01-20T17:25:27.961639", + "updateAt": "2024-01-20T17:25:27.961641", + "lastMade": null + }, + { + "id": "6402a253-2baa-460d-bf4f-b759bb655588", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Vodkapasta", + "slug": "vodkapasta", + "image": "z8BB", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ica.se/recept/vodkapasta-729011/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T01:58:25.398326", + "createdAt": "2024-01-20T15:35:35.492234", + "updateAt": "2024-01-21T01:58:25.400556", + "lastMade": "2024-01-21T22:59:59" + }, + { + "id": "4f54e9e1-f21d-40ec-a135-91e633dfb733", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Vodkapasta2", + "slug": "vodkapasta2", + "image": "Nqpz", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ica.se/recept/vodkapasta-729011/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T17:35:32.077132", + "createdAt": "2024-01-20T15:35:35.492234", + "updateAt": "2024-01-20T17:24:19.620474", + "lastMade": "2024-01-21T04:59:59" + }, + { + "id": "e1a3edb0-49a0-49a3-83e3-95554e932670", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Rub", + "slug": "rub", + "image": null, + "recipeYield": "1", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:55:15.172744", + "createdAt": "2024-01-20T13:53:34.298477", + "updateAt": "2024-01-20T13:55:15.174780", + "lastMade": null + }, + { + "id": "1a0f4e54-db5b-40f1-ab7e-166dab5f6523", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Banana Bread Chocolate Chip Cookies", + "slug": "banana-bread-chocolate-chip-cookies", + "image": "03XS", + "recipeYield": "", + "totalTime": null, + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + "recipeCategory": [], + "tags": [ + { + "id": "6a59e597-9aff-4716-961f-f236b93c34cc", + "name": "Cookies", + "slug": "cookies" + }, + { + "id": "1249f351-4b45-455d-b5f0-64eb0124a41e", + "name": "Banana", + "slug": "banana" + }, + { + "id": "81a446b9-4d8d-451d-a472-486987fad85a", + "name": "Bread", + "slug": "bread" + }, + { + "id": "c2536221-b1c3-4402-a104-46c632663748", + "name": "Chocolate Chip", + "slug": "chocolate-chip" + }, + { + "id": "c026c67f-0211-419f-9db8-7cd4c7608589", + "name": "Cookie", + "slug": "cookie" + }, + { + "id": "2f9e0bf5-02e2-4bdc-9b5d-a16d2fec885b", + "name": "American", + "slug": "american" + }, + { + "id": "2a7c5386-5d26-44fa-8a08-81747ee7f132", + "name": "Bake", + "slug": "bake" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:52:21.817496", + "createdAt": "2024-01-20T13:51:46.727976", + "updateAt": "2024-01-20T13:52:21.821329", + "lastMade": null + }, + { + "id": "447acae6-3424-4c16-8c26-c09040ad8041", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Cauliflower Bisque Recipe with Cheddar Cheese", + "slug": "cauliflower-bisque-recipe-with-cheddar-cheese", + "image": "KuXV", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:45:10.848270", + "createdAt": "2024-01-20T13:44:59.990057", + "updateAt": "2024-01-20T13:45:10.851647", + "lastMade": null + }, + { + "id": "864136a3-27b0-4f3b-a90f-486f42d6df7a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Prova ", + "slug": "prova", + "image": null, + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:44:41.788771", + "createdAt": "2024-01-20T13:42:56.178473", + "updateAt": "2024-01-20T13:42:56.178475", + "lastMade": null + }, + { + "id": "c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "pate au beurre (1)", + "slug": "pate-au-beurre-1", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:17:47.586659", + "createdAt": "2024-01-20T13:17:47.592852", + "updateAt": "2024-01-20T13:17:47.592854", + "lastMade": null + }, + { + "id": "d01865c3-0f18-4e8d-84c0-c14c345fdf9c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "pate au beurre", + "slug": "pate-au-beurre", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:16:49.702039", + "createdAt": "2024-01-20T13:16:49.704498", + "updateAt": "2024-01-20T13:16:49.704500", + "lastMade": null + }, + { + "id": "2cec2bb2-19b6-40b8-a36c-1a76ea29c517", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Sous Vide Cheesecake Recipe", + "slug": "sous-vide-cheesecake-recipe", + "image": "tmwm", + "recipeYield": "4 servings", + "totalTime": "2 Hours 10 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "1 Hour 30 Minutes", + "description": "Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://saltpepperskillet.com/recipes/sous-vide-cheesecake/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:07:19.939939", + "createdAt": "2024-01-20T13:07:19.946260", + "updateAt": "2024-01-20T13:07:19.946263", + "lastMade": null + }, + { + "id": "8e0e4566-9caf-4c2e-a01c-dcead23db86b", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "The Bomb Mini Cheesecakes", + "slug": "the-bomb-mini-cheesecakes", + "image": "xCYc", + "recipeYield": "10 servings", + "totalTime": "1 Hour 30 Minutes", + "prepTime": "30 Minutes", + "cookTime": null, + "performTime": null, + "description": "This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:05:24.037000", + "createdAt": "2024-01-20T13:05:24.039558", + "updateAt": "2024-01-20T13:05:24.039560", + "lastMade": null + }, + { + "id": "a051eafd-9712-4aee-a8e5-0cd10a6772ee", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tagliatelle al Salmone", + "slug": "tagliatelle-al-salmone", + "image": "qzaN", + "recipeYield": "4 servings", + "totalTime": "25 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "recipeCategory": [], + "tags": [ + { + "id": "518f3081-a919-4c80-9cad-75ffbd0e73d3", + "name": "Gemüse", + "slug": "gemuse" + }, + { + "id": "a3fff625-1902-4112-b169-54aec4f52ea7", + "name": "Hauptspeise", + "slug": "hauptspeise" + }, + { + "id": "4ec445c6-fc2f-4a1e-b666-93435a46ec42", + "name": "Schnell", + "slug": "schnell" + }, + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "6f349f84-655b-4740-8fa6-ed2716f17df7", + "name": "Gekocht", + "slug": "gekocht" + }, + { + "id": "77bc190f-dc6d-440b-aa82-f32bfe836018", + "name": "Europa", + "slug": "europa" + }, + { + "id": "7997c911-14ee-4e76-9895-debad7949ae2", + "name": "Pasta", + "slug": "pasta" + }, + { + "id": "04d2aea8-fc9a-4f9b-9a87-8f15189ab6f9", + "name": "Nudeln", + "slug": "nudeln" + }, + { + "id": "c56cd402-3ac7-479e-b96c-d4b64d177dd3", + "name": "Fisch", + "slug": "fisch" + }, + { + "id": "88015586-0885-4397-9098-039ae1109cd1", + "name": "Italien", + "slug": "italien" + }, + { + "id": "024b30ca-53cb-4243-ba6b-d830610f2f48", + "name": "Saucen", + "slug": "saucen" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:02:16.760030", + "createdAt": "2024-01-20T13:02:16.763188", + "updateAt": "2024-01-20T13:02:16.763189", + "lastMade": null + }, + { + "id": "093d51e9-0823-40ad-8e0e-a1d5790dd627", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Death by Chocolate", + "slug": "death-by-chocolate", + "image": "K9qP", + "recipeYield": "1 serving", + "totalTime": null, + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "25 Minutes", + "description": "Hier ist der Name Programm: Den \"Tod durch Schokolade\" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T12:58:50.926224", + "createdAt": "2024-01-20T12:58:50.928810", + "updateAt": "2024-01-20T12:58:50.928812", + "lastMade": null + }, + { + "id": "2d1f62ec-4200-4cfd-987e-c75755d7607c", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Palak Dal Rezept aus Indien", + "slug": "palak-dal-rezept-aus-indien", + "image": "jKQ3", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.", + "recipeCategory": [], + "tags": [ + { + "id": "38d18d57-d817-491e-94f8-da923d2c540e", + "name": "Eintopf", + "slug": "eintopf" + }, + { + "id": "43f12acf-a8df-45bd-b33d-20bfe7a7e607", + "name": "Indisch", + "slug": "indisch" + }, + { + "id": "ede834ac-ab8f-4c79-8a42-dfa0270fd18b", + "name": "Linsen", + "slug": "linsen" + }, + { + "id": "2b6283e2-b8e0-4b3d-90d9-66f322ca77aa", + "name": "Spinat", + "slug": "spinat" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T12:46:54.570376", + "createdAt": "2024-01-20T12:46:54.573341", + "updateAt": "2024-01-20T12:46:54.573342", + "lastMade": null + }, + { + "id": "973dc36d-1661-49b4-ad2d-0b7191034fb3", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "householdId": "cd2bb87f-5e4c-4dc6-8477-af9537200014", + "name": "Tortelline - á la Romana", + "slug": "tortelline-a-la-romana", + "image": "rkSn", + "recipeYield": "4 servings", + "totalTime": "30 Minutes", + "prepTime": "30 Minutes", + "cookTime": null, + "performTime": null, + "description": "Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!", + "recipeCategory": [], + "tags": [ + { + "id": "4c79c0b7-c2d0-415a-b5cf-138cfce92c7e", + "name": "Einfach", + "slug": "einfach" + }, + { + "id": "7997c911-14ee-4e76-9895-debad7949ae2", + "name": "Pasta", + "slug": "pasta" + }, + { + "id": "04d2aea8-fc9a-4f9b-9a87-8f15189ab6f9", + "name": "Nudeln", + "slug": "nudeln" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-20T13:44:42.215472", + "createdAt": "2024-01-20T12:29:47.825708", + "updateAt": "2024-01-20T13:44:42.218635", + "lastMade": "2024-01-21T20:59:59" + } + ], + "next": "/recipes?page=2&perPage=50&orderDirection=desc", + "previous": null +} diff --git a/tests/components/mealie/fixtures/get_shopping_items.json b/tests/components/mealie/fixtures/get_shopping_items.json index 1016440816b..81db48f2e1a 100644 --- a/tests/components/mealie/fixtures/get_shopping_items.json +++ b/tests/components/mealie/fixtures/get_shopping_items.json @@ -9,8 +9,6 @@ "unit": null, "food": null, "note": "Apples", - "isFood": false, - "disableAmount": true, "display": "2 Apples", "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", "checked": false, diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index a694c72fcf6..c4d649fcec6 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -383,10 +383,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -433,10 +433,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', @@ -483,10 +483,10 @@ 'items': list([ dict({ 'checked': False, - 'disable_amount': True, + 'disable_amount': None, 'display': '2 Apples', 'food_id': None, - 'is_food': False, + 'is_food': None, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index aada173ffc3..50da06ca005 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'v1.10.2', 'via_device_id': None, }) diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 56626c7b5c4..a1cb758098e 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -1,4 +1,1242 @@ # serializer version: 1 +# name: test_service_get_recipes[service_data0] + dict({ + 'recipes': dict({ + 'items': list([ + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'tu6y', + 'original_url': None, + 'recipe_id': 'e82f5449-c33b-437c-b712-337587199264', + 'recipe_yield': None, + 'slug': 'tu6y', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'En9o', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': 'f79f7e9d-4b58-4930-a586-2b127f16ee34', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'aAhk', + 'name': 'Patates douces au four (1)', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '90097c8b-9d80-468a-b497-73957ac0cd8b', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kdhm', + 'name': 'Sweet potatoes', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '98845807-9365-41fd-acd1-35630b468c27', + 'recipe_yield': '', + 'slug': 'sweet-potatoes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tNbG', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': '40c227e0-3c7e-41f7-866d-5de04eaecdd7', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nj5M', + 'name': 'Boeuf bourguignon : la vraie recette (2)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': '9c7b8aee-c93c-4b1b-ab48-2625d444743a', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rbU7', + 'name': 'Boeuf bourguignon : la vraie recette (1)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': 'fc42c7d1-7b0f-4e04-b88a-dbd80b81540b', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'JSp3', + 'name': 'Veganes Marmor-Bananenbrot mit Erdnussbutter', + 'original_url': 'https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/', + 'recipe_id': '89e63d72-7a51-4cef-b162-2e45035d0a91', + 'recipe_yield': '14 servings', + 'slug': 'veganes-marmor-bananenbrot-mit-erdnussbutter', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '9QMh', + 'name': 'Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin', + 'original_url': 'https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/', + 'recipe_id': 'eab64457-97ba-4d6c-871c-cb1c724ccb51', + 'recipe_yield': '', + 'slug': 'pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test123', + 'original_url': None, + 'recipe_id': '12439e3d-3c1c-4dcc-9c6e-4afcea2a0542', + 'recipe_yield': None, + 'slug': 'test123', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Bureeto', + 'original_url': None, + 'recipe_id': '6567f6ec-e410-49cb-a1a5-d08517184e78', + 'recipe_yield': None, + 'slug': 'bureeto', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Subway Double Cookies', + 'original_url': None, + 'recipe_id': 'f7737d17-161c-4008-88d4-dd2616778cd0', + 'recipe_yield': None, + 'slug': 'subway-double-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'qwerty12345', + 'original_url': None, + 'recipe_id': '1904b717-4a8b-4de9-8909-56958875b5f4', + 'recipe_yield': None, + 'slug': 'qwerty12345', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'beGq', + 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', + 'recipe_id': '8bdd3656-5e7e-45d3-a3c4-557390846a22', + 'recipe_yield': '24 servings', + 'slug': 'cheeseburger-sliders-easy-30-min-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'meatloaf', + 'original_url': None, + 'recipe_id': '8a30d31d-aa14-411e-af0c-6b61a94f5291', + 'recipe_yield': '4', + 'slug': 'meatloaf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kCBh', + 'name': 'Richtig rheinischer Sauerbraten', + 'original_url': 'https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html', + 'recipe_id': 'f2f7880b-1136-436f-91b7-129788d8c117', + 'recipe_yield': '4 servings', + 'slug': 'richtig-rheinischer-sauerbraten', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kpBx', + 'name': 'Orientalischer Gemüse-Hähnchen Eintopf', + 'original_url': 'https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html', + 'recipe_id': 'cf634591-0f82-4254-8e00-2f7e8b0c9022', + 'recipe_yield': '6 servings', + 'slug': 'orientalischer-gemuse-hahnchen-eintopf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 20240121', + 'original_url': None, + 'recipe_id': '05208856-d273-4cc9-bcfa-e0215d57108d', + 'recipe_yield': '4', + 'slug': 'test-20240121', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'McEx', + 'name': 'Loempia bowl', + 'original_url': 'https://www.lekkerensimpel.com/loempia-bowl/', + 'recipe_id': '145eeb05-781a-4eb0-a656-afa8bc8c0164', + 'recipe_yield': '', + 'slug': 'loempia-bowl', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'bzqo', + 'name': '5 Ingredient Chocolate Mousse', + 'original_url': 'https://thehappypear.ie/aquafaba-chocolate-mousse/', + 'recipe_id': '5c6532aa-ad84-424c-bc05-c32d50430fe4', + 'recipe_yield': '6 servings', + 'slug': '5-ingredient-chocolate-mousse', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KGK6', + 'name': 'Der perfekte Pfannkuchen - gelingt einfach immer', + 'original_url': 'https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html', + 'recipe_id': 'f2e684f2-49e0-45ee-90de-951344472f1c', + 'recipe_yield': '4 servings', + 'slug': 'der-perfekte-pfannkuchen-gelingt-einfach-immer', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'yNDq', + 'name': 'Dinkel-Sauerteigbrot', + 'original_url': 'https://www.besondersgut.ch/dinkel-sauerteigbrot/', + 'recipe_id': 'cf239441-b75d-4dea-a48e-9d99b7cb5842', + 'recipe_yield': '1', + 'slug': 'dinkel-sauerteigbrot', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 234234', + 'original_url': None, + 'recipe_id': '2673eb90-6d78-4b95-af36-5db8c8a6da37', + 'recipe_yield': None, + 'slug': 'test-234234', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 243', + 'original_url': None, + 'recipe_id': '0a723c54-af53-40e9-a15f-c87aae5ac688', + 'recipe_yield': None, + 'slug': 'test-243', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nOPT', + 'name': 'Einfacher Nudelauflauf mit Brokkoli', + 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', + 'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159', + 'recipe_yield': '4 servings', + 'slug': 'einfacher-nudelauflauf-mit-brokkoli', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': ''' + Tarta cytrynowa z bezą + Lekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko. + Dla kogo? + Lubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem! + Na jaką okazję? + Na rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby. + Czy wiesz, że? + Zastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku. + Dla urozmaicenia: + Martwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół. + ''', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'vxuL', + 'name': 'Tarta cytrynowa z bezą', + 'original_url': 'https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza', + 'recipe_id': '9d3cb303-a996-4144-948a-36afaeeef554', + 'recipe_yield': '8 servings', + 'slug': 'tarta-cytrynowa-z-beza', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Martins test Recipe', + 'original_url': None, + 'recipe_id': '77f05a49-e869-4048-aa62-0d8a1f5a8f1c', + 'recipe_yield': None, + 'slug': 'martins-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xP1Q', + 'name': 'Muffinki czekoladowe', + 'original_url': 'https://aniagotuje.pl/przepis/muffinki-czekoladowe', + 'recipe_id': '75a90207-9c10-4390-a265-c47a4b67fd69', + 'recipe_yield': '12', + 'slug': 'muffinki-czekoladowe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Recipe', + 'original_url': None, + 'recipe_id': '4320ba72-377b-4657-8297-dce198f24cdf', + 'recipe_yield': None, + 'slug': 'my-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Receipe', + 'original_url': None, + 'recipe_id': '98dac844-31ee-426a-b16c-fb62a5dd2816', + 'recipe_yield': None, + 'slug': 'my-test-receipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'r1ck', + 'name': 'Patates douces au four', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': 'c3c8f207-c704-415d-81b1-da9f032cf52f', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'gD94', + 'name': 'Easy Homemade Pizza Dough', + 'original_url': 'https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/', + 'recipe_id': '1edb2f6e-133c-4be0-b516-3c23625a97ec', + 'recipe_yield': '2 servings', + 'slug': 'easy-homemade-pizza-dough', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '356X', + 'name': 'All-American Beef Stew Recipe', + 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', + 'recipe_id': '48f39d27-4b8e-4c14-bf36-4e1e6497e75e', + 'recipe_yield': '6 servings', + 'slug': 'all-american-beef-stew-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '4Sys', + 'name': "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + 'original_url': 'https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe', + 'recipe_id': '6530ea6e-401e-4304-8a7a-12162ddf5b9c', + 'recipe_yield': '4 servings', + 'slug': 'serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '8goY', + 'name': 'Schnelle Käsespätzle', + 'original_url': 'https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html', + 'recipe_id': 'c496cf9c-1ece-448a-9d3f-ef772f078a4e', + 'recipe_yield': '4 servings', + 'slug': 'schnelle-kasespatzle', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'taco', + 'original_url': None, + 'recipe_id': '49aa6f42-6760-4adf-b6cd-59592da485c3', + 'recipe_yield': None, + 'slug': 'taco', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'z8BB', + 'name': 'Vodkapasta', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '6402a253-2baa-460d-bf4f-b759bb655588', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'Nqpz', + 'name': 'Vodkapasta2', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '4f54e9e1-f21d-40ec-a135-91e633dfb733', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Rub', + 'original_url': None, + 'recipe_id': 'e1a3edb0-49a0-49a3-83e3-95554e932670', + 'recipe_yield': '1', + 'slug': 'rub', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '03XS', + 'name': 'Banana Bread Chocolate Chip Cookies', + 'original_url': 'https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html', + 'recipe_id': '1a0f4e54-db5b-40f1-ab7e-166dab5f6523', + 'recipe_yield': '', + 'slug': 'banana-bread-chocolate-chip-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KuXV', + 'name': 'Cauliflower Bisque Recipe with Cheddar Cheese', + 'original_url': 'https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/', + 'recipe_id': '447acae6-3424-4c16-8c26-c09040ad8041', + 'recipe_yield': '', + 'slug': 'cauliflower-bisque-recipe-with-cheddar-cheese', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Prova ', + 'original_url': None, + 'recipe_id': '864136a3-27b0-4f3b-a90f-486f42d6df7a', + 'recipe_yield': '', + 'slug': 'prova', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre (1)', + 'original_url': None, + 'recipe_id': 'c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4', + 'recipe_yield': None, + 'slug': 'pate-au-beurre-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre', + 'original_url': None, + 'recipe_id': 'd01865c3-0f18-4e8d-84c0-c14c345fdf9c', + 'recipe_yield': None, + 'slug': 'pate-au-beurre', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tmwm', + 'name': 'Sous Vide Cheesecake Recipe', + 'original_url': 'https://saltpepperskillet.com/recipes/sous-vide-cheesecake/', + 'recipe_id': '2cec2bb2-19b6-40b8-a36c-1a76ea29c517', + 'recipe_yield': '4 servings', + 'slug': 'sous-vide-cheesecake-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xCYc', + 'name': 'The Bomb Mini Cheesecakes', + 'original_url': 'https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes', + 'recipe_id': '8e0e4566-9caf-4c2e-a01c-dcead23db86b', + 'recipe_yield': '10 servings', + 'slug': 'the-bomb-mini-cheesecakes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'qzaN', + 'name': 'Tagliatelle al Salmone', + 'original_url': 'https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html', + 'recipe_id': 'a051eafd-9712-4aee-a8e5-0cd10a6772ee', + 'recipe_yield': '4 servings', + 'slug': 'tagliatelle-al-salmone', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Hier ist der Name Programm: Den "Tod durch Schokolade" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'K9qP', + 'name': 'Death by Chocolate', + 'original_url': 'https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html', + 'recipe_id': '093d51e9-0823-40ad-8e0e-a1d5790dd627', + 'recipe_yield': '1 serving', + 'slug': 'death-by-chocolate', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'jKQ3', + 'name': 'Palak Dal Rezept aus Indien', + 'original_url': 'https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/', + 'recipe_id': '2d1f62ec-4200-4cfd-987e-c75755d7607c', + 'recipe_yield': '4 servings', + 'slug': 'palak-dal-rezept-aus-indien', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rkSn', + 'name': 'Tortelline - á la Romana', + 'original_url': 'https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html', + 'recipe_id': '973dc36d-1661-49b4-ad2d-0b7191034fb3', + 'recipe_yield': '4 servings', + 'slug': 'tortelline-a-la-romana', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + ]), + }), + }) +# --- +# name: test_service_get_recipes[service_data1] + dict({ + 'recipes': dict({ + 'items': list([ + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'tu6y', + 'original_url': None, + 'recipe_id': 'e82f5449-c33b-437c-b712-337587199264', + 'recipe_yield': None, + 'slug': 'tu6y', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'En9o', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': 'f79f7e9d-4b58-4930-a586-2b127f16ee34', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'aAhk', + 'name': 'Patates douces au four (1)', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '90097c8b-9d80-468a-b497-73957ac0cd8b', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kdhm', + 'name': 'Sweet potatoes', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': '98845807-9365-41fd-acd1-35630b468c27', + 'recipe_yield': '', + 'slug': 'sweet-potatoes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tNbG', + 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο', + 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', + 'recipe_id': '40c227e0-3c7e-41f7-866d-5de04eaecdd7', + 'recipe_yield': '6 servings', + 'slug': 'eukole-makaronada-me-kephtedakia-ston-phourno', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nj5M', + 'name': 'Boeuf bourguignon : la vraie recette (2)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': '9c7b8aee-c93c-4b1b-ab48-2625d444743a', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rbU7', + 'name': 'Boeuf bourguignon : la vraie recette (1)', + 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', + 'recipe_id': 'fc42c7d1-7b0f-4e04-b88a-dbd80b81540b', + 'recipe_yield': '4 servings', + 'slug': 'boeuf-bourguignon-la-vraie-recette-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Dieses einfache vegane Erdnussbutter-Schoko-Marmor-Bananenbrot Rezept enthält kein Öl und keinen raffiniernten Zucker, ist aber so fluffig, weich, saftig und lecker wie ein Kuchen! Zubereitet mit vielen gesunden Bananen, gelingt es auch glutenfrei und eignet sich perfekt zum Frühstück, als Dessert oder Snack für Zwischendurch!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'JSp3', + 'name': 'Veganes Marmor-Bananenbrot mit Erdnussbutter', + 'original_url': 'https://biancazapatka.com/de/erdnussbutter-schoko-bananenbrot/', + 'recipe_id': '89e63d72-7a51-4cef-b162-2e45035d0a91', + 'recipe_yield': '14 servings', + 'slug': 'veganes-marmor-bananenbrot-mit-erdnussbutter', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Es ist kein Geheimnis: Ich mag es gerne schnell und einfach. Und ich liebe Pasta! Deshalb habe ich mich vor ein paar Wochen auf die Suche nach der perfekten, schnellen Tomatensoße gemacht. Es muss da draußen doch irgendein Rezept geben, das (fast) genauso schnell zuzubereiten ist, wie Miracoli und dabei aber das schöne Gefühl hinterlässt, ...', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '9QMh', + 'name': 'Pasta mit Tomaten, Knoblauch und Basilikum - einfach (und) genial! - Kuechenchaotin', + 'original_url': 'https://kuechenchaotin.de/pasta-mit-tomaten-knoblauch-basilikum/', + 'recipe_id': 'eab64457-97ba-4d6c-871c-cb1c724ccb51', + 'recipe_yield': '', + 'slug': 'pasta-mit-tomaten-knoblauch-und-basilikum-einfach-und-genial-kuechenchaotin', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test123', + 'original_url': None, + 'recipe_id': '12439e3d-3c1c-4dcc-9c6e-4afcea2a0542', + 'recipe_yield': None, + 'slug': 'test123', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Bureeto', + 'original_url': None, + 'recipe_id': '6567f6ec-e410-49cb-a1a5-d08517184e78', + 'recipe_yield': None, + 'slug': 'bureeto', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Subway Double Cookies', + 'original_url': None, + 'recipe_id': 'f7737d17-161c-4008-88d4-dd2616778cd0', + 'recipe_yield': None, + 'slug': 'subway-double-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'qwerty12345', + 'original_url': None, + 'recipe_id': '1904b717-4a8b-4de9-8909-56958875b5f4', + 'recipe_yield': None, + 'slug': 'qwerty12345', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'beGq', + 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', + 'recipe_id': '8bdd3656-5e7e-45d3-a3c4-557390846a22', + 'recipe_yield': '24 servings', + 'slug': 'cheeseburger-sliders-easy-30-min-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'meatloaf', + 'original_url': None, + 'recipe_id': '8a30d31d-aa14-411e-af0c-6b61a94f5291', + 'recipe_yield': '4', + 'slug': 'meatloaf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Richtig rheinischer Sauerbraten - Rheinischer geht's nicht! Über 536 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kCBh', + 'name': 'Richtig rheinischer Sauerbraten', + 'original_url': 'https://www.chefkoch.de/rezepte/937641199437984/Richtig-rheinischer-Sauerbraten.html', + 'recipe_id': 'f2f7880b-1136-436f-91b7-129788d8c117', + 'recipe_yield': '4 servings', + 'slug': 'richtig-rheinischer-sauerbraten', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Orientalischer Gemüse-Hähnchen Eintopf. Über 164 Bewertungen und für köstlich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'kpBx', + 'name': 'Orientalischer Gemüse-Hähnchen Eintopf', + 'original_url': 'https://www.chefkoch.de/rezepte/2307761368177614/Orientalischer-Gemuese-Haehnchen-Eintopf.html', + 'recipe_id': 'cf634591-0f82-4254-8e00-2f7e8b0c9022', + 'recipe_yield': '6 servings', + 'slug': 'orientalischer-gemuse-hahnchen-eintopf', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 20240121', + 'original_url': None, + 'recipe_id': '05208856-d273-4cc9-bcfa-e0215d57108d', + 'recipe_yield': '4', + 'slug': 'test-20240121', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Zet in 20 minuten deze lekkere loempia bowl in elkaar. Makkelijk, snel en weer eens wat anders. Lekker met prei, sojasaus en kipgehakt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'McEx', + 'name': 'Loempia bowl', + 'original_url': 'https://www.lekkerensimpel.com/loempia-bowl/', + 'recipe_id': '145eeb05-781a-4eb0-a656-afa8bc8c0164', + 'recipe_yield': '', + 'slug': 'loempia-bowl', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Chocolate Mousse with Aquafaba, to make the fluffiest of mousses. Whip up this dessert in literally five minutes and chill in the fridge until you're ready to serve!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'bzqo', + 'name': '5 Ingredient Chocolate Mousse', + 'original_url': 'https://thehappypear.ie/aquafaba-chocolate-mousse/', + 'recipe_id': '5c6532aa-ad84-424c-bc05-c32d50430fe4', + 'recipe_yield': '6 servings', + 'slug': '5-ingredient-chocolate-mousse', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Der perfekte Pfannkuchen - gelingt einfach immer - von Kindern geliebt und auch für Kochneulinge super geeignet. Über 2529 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KGK6', + 'name': 'Der perfekte Pfannkuchen - gelingt einfach immer', + 'original_url': 'https://www.chefkoch.de/rezepte/1208161226570428/Der-perfekte-Pfannkuchen-gelingt-einfach-immer.html', + 'recipe_id': 'f2e684f2-49e0-45ee-90de-951344472f1c', + 'recipe_yield': '4 servings', + 'slug': 'der-perfekte-pfannkuchen-gelingt-einfach-immer', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Für alle Liebhaber von Dinkel ist dieses Dinkel-Sauerteigbrot ein absolutes Muss. Aussen knusprig und innen herrlich feucht und grossporig.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'yNDq', + 'name': 'Dinkel-Sauerteigbrot', + 'original_url': 'https://www.besondersgut.ch/dinkel-sauerteigbrot/', + 'recipe_id': 'cf239441-b75d-4dea-a48e-9d99b7cb5842', + 'recipe_yield': '1', + 'slug': 'dinkel-sauerteigbrot', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 234234', + 'original_url': None, + 'recipe_id': '2673eb90-6d78-4b95-af36-5db8c8a6da37', + 'recipe_yield': None, + 'slug': 'test-234234', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'test 243', + 'original_url': None, + 'recipe_id': '0a723c54-af53-40e9-a15f-c87aae5ac688', + 'recipe_yield': None, + 'slug': 'test-243', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'nOPT', + 'name': 'Einfacher Nudelauflauf mit Brokkoli', + 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', + 'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159', + 'recipe_yield': '4 servings', + 'slug': 'einfacher-nudelauflauf-mit-brokkoli', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': ''' + Tarta cytrynowa z bezą + Lekko kwaśna masa cytrynowa, która równoważy słodycz bezy – jeśli to brzmi jak ciasto, które chętnie zjesz na deser, wypróbuj nasz przepis! Tarta z bezą i masą cytrynową nawiązuje do kuchni francuskiej, znanej z wyśmienitych quiche i tart. Tym razem proponujemy ją w wersji na słodko. + Dla kogo? + Lubisz ciasta o delikatnym, kruchym spodzie? Posmakuje ci tarta cytrynowa z bezą. Przepis jest wprost stworzony dla miłośników lekko cierpkiego smaku cytrusów w wypiekach. Tarta cytrynowa z bezą zdecydowanie nie jest mdłym ciastem! + Na jaką okazję? + Na rodzinnym stole, zamiast zwykłego sernika lub ciasta czekoladowego, może stanąć właśnie tarta cytrynowa z bezą. Przepis ten skradnie serce twojej przyjaciółki lub przyjaciela, którego zaprosisz na herbatę i ciasto. Naszym zdaniem ma też dużą szansę stać się hitem urodzinowej imprezy, gdy pojawi się tuż obok tortu. Tarta cytrynowa z bezą smakuje doskonale w okresie świątecznym – upiecz ją na Wielkanoc oprócz tradycyjnego mazurka i baby. + Czy wiesz, że? + Zastanawiasz się, czy kupione kilka dni temu cytryny możesz przeznaczyć do przepisu na tartę? Jest wiele sposobów na przedłużenie ich świeżości. Niektórzy trzymają je w lodówce, w torebce zamykanej strunowo. Ciekawostka: im mocniej pachnie cytryna, tym kwaśniejsza będzie w smaku. + Dla urozmaicenia: + Martwisz się o to, czy każda warstwa tarty odpowiednio się upiecze? Mamy na to sposób. Piecz ją w piekarniku bez termoobiegu, ustawionym na grzanie góra–dół. + ''', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'vxuL', + 'name': 'Tarta cytrynowa z bezą', + 'original_url': 'https://www.przepisy.pl/przepis/tarta-cytrynowa-z-beza', + 'recipe_id': '9d3cb303-a996-4144-948a-36afaeeef554', + 'recipe_yield': '8 servings', + 'slug': 'tarta-cytrynowa-z-beza', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Martins test Recipe', + 'original_url': None, + 'recipe_id': '77f05a49-e869-4048-aa62-0d8a1f5a8f1c', + 'recipe_yield': None, + 'slug': 'martins-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Muffinki czekoladowe to przepyszny i bardzo prosty w przygotowaniu mini deser pieczony w papilotkach. Przepis na najlepsze, bardzo wilgotne i puszyste muffinki czekoladowe polecam każdemu miłośnikowi czekolady.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xP1Q', + 'name': 'Muffinki czekoladowe', + 'original_url': 'https://aniagotuje.pl/przepis/muffinki-czekoladowe', + 'recipe_id': '75a90207-9c10-4390-a265-c47a4b67fd69', + 'recipe_yield': '12', + 'slug': 'muffinki-czekoladowe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Recipe', + 'original_url': None, + 'recipe_id': '4320ba72-377b-4657-8297-dce198f24cdf', + 'recipe_yield': None, + 'slug': 'my-test-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'My Test Receipe', + 'original_url': None, + 'recipe_id': '98dac844-31ee-426a-b16c-fb62a5dd2816', + 'recipe_yield': None, + 'slug': 'my-test-receipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Régalez vous avec ces patates douces cuites au four et légèrement parfumées au thym et au piment. Super bon avec un poulet rôti par exemple.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'r1ck', + 'name': 'Patates douces au four', + 'original_url': 'https://www.papillesetpupilles.fr/2018/10/patates-douces-au-four.html/', + 'recipe_id': 'c3c8f207-c704-415d-81b1-da9f032cf52f', + 'recipe_yield': '', + 'slug': 'patates-douces-au-four', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Follow these basic instructions for a thick, crisp, and chewy pizza crust at home. The recipe yields enough pizza dough for two 12-inch pizzas and you can freeze half of the dough for later. Close to 2 pounds of dough total.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'gD94', + 'name': 'Easy Homemade Pizza Dough', + 'original_url': 'https://sallysbakingaddiction.com/homemade-pizza-crust-recipe/', + 'recipe_id': '1edb2f6e-133c-4be0-b516-3c23625a97ec', + 'recipe_yield': '2 servings', + 'slug': 'easy-homemade-pizza-dough', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '356X', + 'name': 'All-American Beef Stew Recipe', + 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', + 'recipe_id': '48f39d27-4b8e-4c14-bf36-4e1e6497e75e', + 'recipe_yield': '6 servings', + 'slug': 'all-american-beef-stew-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This utterly faithful recipe perfectly recreates a New York City halal-cart classic: Chicken and Rice with White Sauce. The chicken is marinated with herbs, lemon, and spices; the rice golden; the sauce, as white and creamy as ever.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '4Sys', + 'name': "Serious Eats' Halal Cart-Style Chicken and Rice With White Sauce", + 'original_url': 'https://www.seriouseats.com/serious-eats-halal-cart-style-chicken-and-rice-white-sauce-recipe', + 'recipe_id': '6530ea6e-401e-4304-8a7a-12162ddf5b9c', + 'recipe_yield': '4 servings', + 'slug': 'serious-eats-halal-cart-style-chicken-and-rice-with-white-sauce', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Schnelle Käsespätzle. Über 1201 Bewertungen und für sehr gut befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '8goY', + 'name': 'Schnelle Käsespätzle', + 'original_url': 'https://www.chefkoch.de/rezepte/1062121211526182/Schnelle-Kaesespaetzle.html', + 'recipe_id': 'c496cf9c-1ece-448a-9d3f-ef772f078a4e', + 'recipe_yield': '4 servings', + 'slug': 'schnelle-kasespatzle', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'taco', + 'original_url': None, + 'recipe_id': '49aa6f42-6760-4adf-b6cd-59592da485c3', + 'recipe_yield': None, + 'slug': 'taco', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'z8BB', + 'name': 'Vodkapasta', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '6402a253-2baa-460d-bf4f-b759bb655588', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Har du inte provat denna trendiga pasta är det hög tid! Enkel och gräddig vardagspasta med smak av tomat och chili och en hemlig ingrediens som ger denna rätt extra sting, nämligen vodka.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'Nqpz', + 'name': 'Vodkapasta2', + 'original_url': 'https://www.ica.se/recept/vodkapasta-729011/', + 'recipe_id': '4f54e9e1-f21d-40ec-a135-91e633dfb733', + 'recipe_yield': '4 servings', + 'slug': 'vodkapasta2', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Rub', + 'original_url': None, + 'recipe_id': 'e1a3edb0-49a0-49a3-83e3-95554e932670', + 'recipe_yield': '1', + 'slug': 'rub', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Tender and moist, these chocolate chip cookies were a HUGE hit in the Test Kitchen. They're like banana bread in a cookie form. Outside, there are crisp edges like a cookie. Inside, though, it's soft like banana bread. We opted to add chocolate chips and nuts. It's a classic flavor combination in banana bread and works just as well in these cookies.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': '03XS', + 'name': 'Banana Bread Chocolate Chip Cookies', + 'original_url': 'https://www.justapinch.com/recipes/dessert/cookies/banana-bread-chocolate-chip-cookies.html', + 'recipe_id': '1a0f4e54-db5b-40f1-ab7e-166dab5f6523', + 'recipe_yield': '', + 'slug': 'banana-bread-chocolate-chip-cookies', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': "Hello friends, today I'm going to share with you how to make a delicious soup/bisque. A Cauliflower Bisques Recipe with Cheddar Cheese. One of my favorite soups to make when its cold outside. We will be continuing the soup collection so let me know what you think in the comments below!", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'KuXV', + 'name': 'Cauliflower Bisque Recipe with Cheddar Cheese', + 'original_url': 'https://chefjeanpierre.com/recipes/soups/creamy-cauliflower-bisque/', + 'recipe_id': '447acae6-3424-4c16-8c26-c09040ad8041', + 'recipe_yield': '', + 'slug': 'cauliflower-bisque-recipe-with-cheddar-cheese', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'Prova ', + 'original_url': None, + 'recipe_id': '864136a3-27b0-4f3b-a90f-486f42d6df7a', + 'recipe_yield': '', + 'slug': 'prova', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre (1)', + 'original_url': None, + 'recipe_id': 'c7ccf4c7-c5f4-4191-a79b-1a49d068f6a4', + 'recipe_yield': None, + 'slug': 'pate-au-beurre-1', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': '', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': None, + 'name': 'pate au beurre', + 'original_url': None, + 'recipe_id': 'd01865c3-0f18-4e8d-84c0-c14c345fdf9c', + 'recipe_yield': None, + 'slug': 'pate-au-beurre', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Individual foolproof mason jar cheesecakes with strawberry compote and a Graham cracker crumble topping. Foolproof, simple, and delicious.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'tmwm', + 'name': 'Sous Vide Cheesecake Recipe', + 'original_url': 'https://saltpepperskillet.com/recipes/sous-vide-cheesecake/', + 'recipe_id': '2cec2bb2-19b6-40b8-a36c-1a76ea29c517', + 'recipe_yield': '4 servings', + 'slug': 'sous-vide-cheesecake-recipe', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'This is a variation of the several cheese cake recipes that have been used for sous vide. These make a fabulous 4oz cheese cake for dessert. Garnish with a raspberry or blackberry and impress your family and friends. They’ll keep great in the fridge for a week easily.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'xCYc', + 'name': 'The Bomb Mini Cheesecakes', + 'original_url': 'https://recipes.anovaculinary.com/recipe/the-bomb-cheesecakes', + 'recipe_id': '8e0e4566-9caf-4c2e-a01c-dcead23db86b', + 'recipe_yield': '10 servings', + 'slug': 'the-bomb-mini-cheesecakes', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tagliatelle al Salmone - wie beim Italiener. Über 1568 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'qzaN', + 'name': 'Tagliatelle al Salmone', + 'original_url': 'https://www.chefkoch.de/rezepte/2109501340136606/Tagliatelle-al-Salmone.html', + 'recipe_id': 'a051eafd-9712-4aee-a8e5-0cd10a6772ee', + 'recipe_yield': '4 servings', + 'slug': 'tagliatelle-al-salmone', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Hier ist der Name Programm: Den "Tod durch Schokolade" müsst ihr zwar hoffentlich nicht erleiden, aber Chocoholics werden diesen Kuchen lieben!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'K9qP', + 'name': 'Death by Chocolate', + 'original_url': 'https://www.backenmachtgluecklich.de/rezepte/death-by-chocolate-kuchen.html', + 'recipe_id': '093d51e9-0823-40ad-8e0e-a1d5790dd627', + 'recipe_yield': '1 serving', + 'slug': 'death-by-chocolate', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Palak Dal ist in Grunde genommen Spinat (Palak) mit Linsen oder anderen Hülsenfrüchten (Dal) vom indischen Subkontinent. Es kommen noch Zwiebeln, Tomaten und einige indische Gewürze dazu. Damit ist das Palak Dal ein super einfaches und zugleich veganes indisches Rezept. Es schmeckt hervorragend mit Naan-Brot und etwas gewürztem Joghurt.', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'jKQ3', + 'name': 'Palak Dal Rezept aus Indien', + 'original_url': 'https://www.fernweh-koch.de/palak-dal-indischer-spinat-linsen-rezept/', + 'recipe_id': '2d1f62ec-4200-4cfd-987e-c75755d7607c', + 'recipe_yield': '4 servings', + 'slug': 'palak-dal-rezept-aus-indien', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + dict({ + 'description': 'Tortelline - á la Romana. Über 13 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!', + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': 'cd2bb87f-5e4c-4dc6-8477-af9537200014', + 'image': 'rkSn', + 'name': 'Tortelline - á la Romana', + 'original_url': 'https://www.chefkoch.de/rezepte/74441028021809/Tortelline-a-la-Romana.html', + 'recipe_id': '973dc36d-1661-49b4-ad2d-0b7191034fb3', + 'recipe_yield': '4 servings', + 'slug': 'tortelline-a-la-romana', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + ]), + }), + }) +# --- # name: test_service_import_recipe dict({ 'recipe': dict({ @@ -9,7 +1247,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -23,7 +1261,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', @@ -525,7 +1763,7 @@ 'image': 'SuPW', 'ingredients': list([ dict({ - 'is_food': True, + 'is_food': None, 'note': '130g dark couverture chocolate (min. 55% cocoa content)', 'quantity': 1.0, 'reference_id': 'a3adfe78-d157-44d8-98be-9c133e45bb4e', @@ -539,7 +1777,7 @@ 'unit': None, }), dict({ - 'is_food': True, + 'is_food': None, 'note': '150g softened butter', 'quantity': 1.0, 'reference_id': 'f6ce06bf-8b02-43e6-8316-0dc3fb0da0fc', diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 57c55159bdc..2ced94a7399 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -21,6 +21,8 @@ from homeassistant.components.mealie.const import ( ATTR_NOTE_TEXT, ATTR_NOTE_TITLE, ATTR_RECIPE_ID, + ATTR_RESULT_LIMIT, + ATTR_SEARCH_TERMS, ATTR_START_DATE, ATTR_URL, DOMAIN, @@ -28,6 +30,7 @@ from homeassistant.components.mealie.const import ( from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, + SERVICE_GET_RECIPES, SERVICE_IMPORT_RECIPE, SERVICE_SET_MEALPLAN, SERVICE_SET_RANDOM_MEALPLAN, @@ -150,6 +153,42 @@ async def test_service_recipe( assert response == snapshot +@pytest.mark.parametrize( + "service_data", + [ + # Default call + {ATTR_CONFIG_ENTRY_ID: "mock_entry_id"}, + # With search terms and result limit + { + ATTR_CONFIG_ENTRY_ID: "mock_entry_id", + ATTR_SEARCH_TERMS: "pasta", + ATTR_RESULT_LIMIT: 5, + }, + ], +) +async def test_service_get_recipes( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + service_data: dict, +) -> None: + """Test the get_recipes service.""" + await setup_integration(hass, mock_config_entry) + + # Patch entry_id into service_data for each run + service_data = {**service_data, ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_RECIPES, + service_data, + blocking=True, + return_response=True, + ) + assert response == snapshot + + async def test_service_import_recipe( hass: HomeAssistant, mock_mealie_client: AsyncMock, @@ -332,6 +371,22 @@ async def test_service_set_mealplan( ServiceValidationError, "Recipe with ID or slug `recipe_id` not found", ), + ( + SERVICE_GET_RECIPES, + {}, + "get_recipes", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ( + SERVICE_GET_RECIPES, + {ATTR_SEARCH_TERMS: "pasta"}, + "get_recipes", + MealieNotFoundError, + ServiceValidationError, + "No recipes found matching your search", + ), ( SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}, @@ -402,6 +457,11 @@ async def test_services_connection_error( [ (SERVICE_GET_MEALPLAN, {}), (SERVICE_GET_RECIPE, {ATTR_RECIPE_ID: "recipe_id"}), + (SERVICE_GET_RECIPES, {}), + ( + SERVICE_GET_RECIPES, + {ATTR_SEARCH_TERMS: "pasta", ATTR_RESULT_LIMIT: 5}, + ), (SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}), ( SERVICE_SET_RANDOM_MEALPLAN, diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py index d156ef3a0f1..0f001cacacd 100644 --- a/tests/components/mealie/test_todo.py +++ b/tests/components/mealie/test_todo.py @@ -221,8 +221,6 @@ async def test_moving_todo_item( display=None, checked=False, position=1, - is_food=False, - disable_amount=None, quantity=2.0, label_id=None, food_id=None, diff --git a/tests/components/meater/snapshots/test_init.ambr b/tests/components/meater/snapshots/test_init.ambr index 68e4ba32a4a..654e631cdda 100644 --- a/tests/components/meater/snapshots/test_init.ambr +++ b/tests/components/meater/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Apption Labs', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/miele/fixtures/5_devices.json b/tests/components/miele/fixtures/5_devices.json index 113babbd3f7..2e76c1f6ef5 100644 --- a/tests/components/miele/fixtures/5_devices.json +++ b/tests/components/miele/fixtures/5_devices.json @@ -648,5 +648,129 @@ }, "batteryLevel": null } + }, + "DummyAppliance_12": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 12, + "value_localized": "Oven" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "16", + "techType": "H7660BP", + "matNumber": "11120960", + "swids": [] + }, + "xkmIdentLabel": { + "techType": "EK057", + "releaseVersion": "08.32" + } + }, + "state": { + "ProgramID": { + "value_raw": 356, + "value_localized": "Defrost", + "key_localized": "Program name" + }, + "status": { + "value_raw": 5, + "value_localized": "In use", + "key_localized": "status" + }, + "programType": { + "value_raw": 1, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 3073, + "value_localized": "Heating-up phase", + "key_localized": "Program phase" + }, + "remainingTime": [0, 5], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": 2500, + "value_localized": 25.0, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": 1954, + "value_localized": 19.54, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": 2200, + "value_localized": 22.0, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": true + }, + "ambientLight": null, + "light": 1, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/fixtures/action_fridge_freezer.json b/tests/components/miele/fixtures/action_fridge_freezer.json new file mode 100644 index 00000000000..94ee43a90fe --- /dev/null +++ b/tests/components/miele/fixtures/action_fridge_freezer.json @@ -0,0 +1,31 @@ +{ + "processAction": [6], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + }, + { + "zone": 2, + "min": -28, + "max": -14 + }, + { + "zone": 3, + "min": -30, + "max": -15 + } + ], + "deviceName": true, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [1], + "runOnTime": [] +} diff --git a/tests/components/miele/fixtures/fridge_freezer.json b/tests/components/miele/fixtures/fridge_freezer.json index 5d091b9c74e..8ca28befc35 100644 --- a/tests/components/miele/fixtures/fridge_freezer.json +++ b/tests/components/miele/fixtures/fridge_freezer.json @@ -53,6 +53,11 @@ "value_raw": -1800, "value_localized": -18.0, "unit": "Celsius" + }, + { + "value_raw": -2500, + "value_localized": -25.0, + "unit": "Celsius" } ], "coreTargetTemperature": [], @@ -68,8 +73,8 @@ "unit": "Celsius" }, { - "value_raw": -32768, - "value_localized": null, + "value_raw": -2800, + "value_localized": -28.0, "unit": "Celsius" } ], diff --git a/tests/components/miele/fixtures/programs.json b/tests/components/miele/fixtures/programs.json index 06eddc5fedc..1c232059d59 100644 --- a/tests/components/miele/fixtures/programs.json +++ b/tests/components/miele/fixtures/programs.json @@ -11,7 +11,7 @@ }, { "programId": 123, - "program": "Dark garments / Denim", + "program": "Dark garments / Denim ", "parameters": {} }, { @@ -30,5 +30,9 @@ "mandatory": true } } + }, + { + "programId": 24000, + "program": "Ristretto" } ] diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 0fb24c893c4..3b8b7488d9b 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_states[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -41,7 +41,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.freezer-state] +# name: test_climate_states[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -63,7 +63,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -105,7 +105,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -127,7 +127,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -169,7 +169,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, @@ -191,7 +191,7 @@ 'state': 'cool', }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-entry] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -233,7 +233,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-state] +# name: test_climate_states_api_push[freezer-platforms0][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, @@ -255,3 +255,195 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -18, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -18, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 4, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 4, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -28, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -25, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/miele/snapshots/test_init.ambr b/tests/components/miele/snapshots/test_init.ambr index eee976ab09f..81f6c0c3a35 100644 --- a/tests/components/miele/snapshots/test_init.ambr +++ b/tests/components/miele/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'Dummy_Appliance_1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Miele', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'Dummy_Appliance_1', - 'suggested_area': None, 'sw_version': '31.17', 'via_device_id': None, }) diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 915eda4d361..2805a683077 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,1141 @@ # serializer version: 1 +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_74_off-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_1_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_3', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_2_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_7', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_3_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_15', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74_off-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_4_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_0', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plate 5', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_74-state_plate_step-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction_plate_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 5', + 'options': list([ + 'plate_step_0', + 'plate_step_1', + 'plate_step_10', + 'plate_step_11', + 'plate_step_12', + 'plate_step_13', + 'plate_step_14', + 'plate_step_15', + 'plate_step_16', + 'plate_step_17', + 'plate_step_18', + 'plate_step_2', + 'plate_step_3', + 'plate_step_4', + 'plate_step_5', + 'plate_step_6', + 'plate_step_7', + 'plate_step_8', + 'plate_step_9', + 'plate_step_boost', + 'plate_step_warming', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'plate_step_boost', + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hood', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:turbine', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_18-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hood', + 'icon': 'mdi:turbine', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/snapshots/test_services.ambr b/tests/components/miele/snapshots/test_services.ambr index 3095ec9b6fb..3c3feca7832 100644 --- a/tests/components/miele/snapshots/test_services.ambr +++ b/tests/components/miele/snapshots/test_services.ambr @@ -43,6 +43,12 @@ 'program': 'Fan plus', 'program_id': 13, }), + dict({ + 'parameters': dict({ + }), + 'program': 'Ristretto', + 'program_id': 24000, + }), ]), }) # --- diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index c4966430a9d..392a6712707 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -15,21 +15,13 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform TEST_PLATFORM = CLIMATE_DOMAIN -pytestmark = [ - pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]), - pytest.mark.parametrize( - "load_action_file", - ["action_freezer.json"], - ids=[ - "freezer", - ], - ), -] +pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) ENTITY_ID = "climate.freezer" SERVICE_SET_TEMPERATURE = "set_temperature" +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -42,7 +34,24 @@ async def test_climate_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_device_file", ["fridge_freezer.json"]) +@pytest.mark.parametrize( + "load_action_file", ["action_fridge_freezer.json"], ids=["fridge_freezer"] +) +async def test_climate_states_mulizone( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test climate entity state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_climate_states_api_push( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -56,6 +65,7 @@ async def test_climate_states_api_push( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_set_target( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -74,6 +84,7 @@ async def test_set_target( ) +@pytest.mark.parametrize("load_action_file", ["action_freezer.json"], ids=["freezer"]) async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index f35404a665b..e5051a683c9 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -256,3 +256,18 @@ async def test_vacuum_sensor_states( """Test robot vacuum cleaner sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["fan_devices.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_fan_hob_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test robot fan / hob sensor state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/miele/test_services.py b/tests/components/miele/test_services.py index 2bf0e2deb9c..38b9f064b55 100644 --- a/tests/components/miele/test_services.py +++ b/tests/components/miele/test_services.py @@ -1,5 +1,6 @@ """Tests the services provided by the miele integration.""" +from datetime import timedelta from unittest.mock import MagicMock from aiohttp import ClientResponseError @@ -9,11 +10,13 @@ from voluptuous import MultipleInvalid from homeassistant.components.miele.const import DOMAIN from homeassistant.components.miele.services import ( + ATTR_DURATION, ATTR_PROGRAM_ID, SERVICE_GET_PROGRAMS, SERVICE_SET_PROGRAM, + SERVICE_SET_PROGRAM_OVEN, ) -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceRegistry @@ -49,6 +52,50 @@ async def test_services( ) +@pytest.mark.parametrize( + ("call_arguments", "miele_arguments"), + [ + ( + {ATTR_PROGRAM_ID: 24}, + {"programId": 24}, + ), + ( + {ATTR_PROGRAM_ID: 25, ATTR_DURATION: timedelta(minutes=75)}, + {"programId": 25, "duration": [1, 15]}, + ), + ( + { + ATTR_PROGRAM_ID: 26, + ATTR_DURATION: timedelta(minutes=135), + ATTR_TEMPERATURE: 180, + }, + {"programId": 26, "duration": [2, 15], "temperature": 180}, + ), + ], +) +async def test_services_oven( + hass: HomeAssistant, + device_registry: DeviceRegistry, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + call_arguments: dict, + miele_arguments: dict, +) -> None: + """Tests that the custom services are correct for ovens.""" + + await setup_integration(hass, mock_config_entry) + device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)}) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PROGRAM_OVEN, + {ATTR_DEVICE_ID: device.id, **call_arguments}, + blocking=True, + ) + mock_miele_client.set_program.assert_called_once_with( + TEST_APPLIANCE, miele_arguments + ) + + async def test_services_with_response( hass: HomeAssistant, device_registry: DeviceRegistry, @@ -71,11 +118,20 @@ async def test_services_with_response( ) +@pytest.mark.parametrize( + ("service", "error"), + [ + (SERVICE_SET_PROGRAM, "'Set program' action failed"), + (SERVICE_SET_PROGRAM_OVEN, "'Set program on oven' action failed"), + ], +) async def test_service_api_errors( hass: HomeAssistant, device_registry: DeviceRegistry, mock_miele_client: MagicMock, mock_config_entry: MockConfigEntry, + service: str, + error: str, ) -> None: """Test service api errors.""" await setup_integration(hass, mock_config_entry) @@ -83,10 +139,10 @@ async def test_service_api_errors( # Test http error mock_miele_client.set_program.side_effect = ClientResponseError("TestInfo", "test") - with pytest.raises(HomeAssistantError, match="'Set program' action failed"): + with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( DOMAIN, - SERVICE_SET_PROGRAM, + service, {ATTR_DEVICE_ID: device.id, ATTR_PROGRAM_ID: 1}, blocking=True, ) diff --git a/tests/components/mill/test_coordinator.py b/tests/components/mill/test_coordinator.py index a2a3bd57b65..2e6e08016b7 100644 --- a/tests/components/mill/test_coordinator.py +++ b/tests/components/mill/test_coordinator.py @@ -11,12 +11,15 @@ from homeassistant.components.recorder.statistics import statistics_during_perio from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.components.recorder.common import async_wait_recording_done async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -31,7 +34,7 @@ async def test_mill_historic_data(recorder_mock: Recorder, hass: HomeAssistant) statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -96,6 +99,8 @@ async def test_mill_historic_data_no_heater( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -110,7 +115,7 @@ async def test_mill_historic_data_no_heater( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -133,6 +138,8 @@ async def test_mill_historic_data_no_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): 2, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -145,7 +152,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=data) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -168,7 +175,7 @@ async def test_mill_historic_data_no_data( mill_data_connection.fetch_historic_energy_usage = AsyncMock(return_value=None) coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) @@ -192,6 +199,8 @@ async def test_mill_historic_data_invalid_data( ) -> None: """Test historic data from Mill.""" + entry = MockConfigEntry(domain=DOMAIN) + data = { dt_util.parse_datetime("2024-12-03T00:00:00+01:00"): None, dt_util.parse_datetime("2024-12-03T01:00:00+01:00"): 3, @@ -206,7 +215,7 @@ async def test_mill_historic_data_invalid_data( statistic_id = f"{DOMAIN}:energy_dev_id" coordinator = MillHistoricDataUpdateCoordinator( - hass, mill_data_connection=mill_data_connection + hass, entry, mill_data_connection=mill_data_connection ) await coordinator._async_update_data() await async_wait_recording_done(hass) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 3e87925c1cd..fdaed0c323f 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -32,7 +32,11 @@ from homeassistant.const import ( ) from homeassistant.core import HassJobType, HomeAssistant from homeassistant.generated.mqtt import MQTT -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -94,6 +98,117 @@ MOCK_SUBENTRY_BUTTON_COMPONENT = { "entity_picture": "https://example.com/365d05e6607c4dfb8ae915cff71a954b", }, } +MOCK_SUBENTRY_CLIMATE_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851386": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851386", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # single target temperature + "temperature_command_topic": "temperature-command-topic", + "temperature_command_template": "{{ value }}", + "temperature_state_topic": "temperature-state-topic", + "temperature_state_template": "{{ value_json.temperature }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + # power settings + "power_command_topic": "power-command-topic", + "power_command_template": "{{ value }}", + "payload_on": "ON", + "payload_off": "OFF", + # current action settings + "action_topic": "action-topic", + "action_template": "{{ value_json.current_action }}", + # target humidity + "target_humidity_command_topic": "target-humidity-command-topic", + "target_humidity_command_template": "{{ value }}", + "target_humidity_state_topic": "target-humidity-state-topic", + "target_humidity_state_template": "{{ value_json.target_humidity }}", + "min_humidity": 20, + "max_humidity": 80, + # current temperature + "current_temperature_topic": "current-temperature-topic", + "current_temperature_template": "{{ value_json.temperature }}", + # current humidity + "current_humidity_topic": "current-humidity-topic", + "current_humidity_template": "{{ value_json.humidity }}", + # preset mode + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "{{ value }}", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "preset_modes": ["auto", "eco"], + # fan mode + "fan_mode_command_topic": "fan-mode-command-topic", + "fan_mode_command_template": "{{ value }}", + "fan_mode_state_topic": "fan-mode-state-topic", + "fan_mode_state_template": "{{ value_json.fan_mode }}", + "fan_modes": ["off", "low", "medium", "high"], + # swing mode + "swing_mode_command_topic": "swing-mode-command-topic", + "swing_mode_command_template": "{{ value }}", + "swing_mode_state_topic": "swing-mode-state-topic", + "swing_mode_state_template": "{{ value_json.swing_mode }}", + "swing_modes": ["off", "on"], + # swing horizontal mode + "swing_horizontal_mode_command_topic": "swing-horizontal-mode-command-topic", + "swing_horizontal_mode_command_template": "{{ value }}", + "swing_horizontal_mode_state_topic": "swing-horizontal-mode-state-topic", + "swing_horizontal_mode_state_template": "{{ value_json.swing_horizontal_mode }}", + "swing_horizontal_modes": ["off", "on"], + }, +} +MOCK_SUBENTRY_CLIMATE_HIGH_LOW_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851387": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851387", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # high/low target temperature + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, +} +MOCK_SUBENTRY_CLIMATE_NO_TARGET_TEMP_COMPONENT = { + "b085c09efba7ec76acd94e2e0f851388": { + "platform": "climate", + "name": "Cooler", + "entity_category": None, + "entity_picture": "https://example.com/b085c09efba7ec76acd94e2e0f851388", + "temperature_unit": "C", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + }, +} MOCK_SUBENTRY_COVER_COMPONENT = { "b37acf667fa04c688ad7dfb27de2178b": { "platform": "cover", @@ -312,6 +427,18 @@ MOCK_BUTTON_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BUTTON_COMPONENT, } +MOCK_CLIMATE_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_CLIMATE_COMPONENT, +} +MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, + "components": MOCK_SUBENTRY_CLIMATE_HIGH_LOW_COMPONENT, +} +MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, + "components": MOCK_SUBENTRY_CLIMATE_NO_TARGET_TEMP_COMPONENT, +} MOCK_COVER_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_COVER_COMPONENT, @@ -1292,13 +1419,14 @@ async def help_test_entity_device_info_with_identifier( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) + device = device_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -1307,7 +1435,7 @@ async def help_test_entity_device_info_with_identifier( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" @@ -1327,13 +1455,14 @@ async def help_test_entity_device_info_with_connection( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) config["unique_id"] = "veryunique" - registry = dr.async_get(hass) + area_registry = ar.async_get(hass) + device_registry = dr.async_get(hass) data = json.dumps(config) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device( + device = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None @@ -1344,7 +1473,7 @@ async def help_test_entity_device_info_with_connection( assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" - assert device.suggested_area == "default_area" + assert device.area_id == area_registry.async_get_area_by_name("default_area").id assert device.configuration_url == "http://example.com" diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 568fb7ea39d..333febe8844 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -29,10 +29,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.components.mqtt.climate import ( - DEFAULT_INITIAL_TEMPERATURE, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, VALUE_TEMPLATE_KEYS, ) +from homeassistant.components.mqtt.const import ( + DEFAULT_CLIMATE_INITIAL_TEMPERATURE as DEFAULT_INITIAL_TEMPERATURE, +) from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index ce0a0c44a79..ff1f954bace 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -35,6 +35,9 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE, + MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, MOCK_COVER_SUBENTRY_DATA_SINGLE, MOCK_FAN_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, @@ -2700,6 +2703,224 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Restart", ), + ( + MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": True, + "climate_feature_current_humidity": True, + "climate_feature_current_temperature": True, + "climate_feature_power": True, + "climate_feature_preset_modes": True, + "climate_feature_fan_modes": True, + "climate_feature_swing_horizontal_modes": True, + "climate_feature_swing_modes": True, + "climate_feature_target_temperature": "single", + "climate_feature_target_humidity": True, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # single target temperature + "target_temperature_settings": { + "temperature_command_topic": "temperature-command-topic", + "temperature_command_template": "{{ value }}", + "temperature_state_topic": "temperature-state-topic", + "temperature_state_template": "{{ value_json.temperature }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + # power settings + "climate_power_settings": { + "power_command_topic": "power-command-topic", + "power_command_template": "{{ value }}", + "payload_on": "ON", + "payload_off": "OFF", + }, + # current action settings + "climate_action_settings": { + "action_topic": "action-topic", + "action_template": "{{ value_json.current_action }}", + }, + # target humidity + "target_humidity_settings": { + "target_humidity_command_topic": "target-humidity-command-topic", + "target_humidity_command_template": "{{ value }}", + "target_humidity_state_topic": "target-humidity-state-topic", + "target_humidity_state_template": "{{ value_json.target_humidity }}", + "min_humidity": 20, + "max_humidity": 80, + }, + # current temperature + "current_temperature_settings": { + "current_temperature_topic": "current-temperature-topic", + "current_temperature_template": "{{ value_json.temperature }}", + }, + # current humidity + "current_humidity_settings": { + "current_humidity_topic": "current-humidity-topic", + "current_humidity_template": "{{ value_json.humidity }}", + }, + # preset mode + "climate_preset_mode_settings": { + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "{{ value }}", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "preset_modes": ["auto", "eco"], + }, + # fan mode + "climate_fan_mode_settings": { + "fan_mode_command_topic": "fan-mode-command-topic", + "fan_mode_command_template": "{{ value }}", + "fan_mode_state_topic": "fan-mode-state-topic", + "fan_mode_state_template": "{{ value_json.fan_mode }}", + "fan_modes": ["off", "low", "medium", "high"], + }, + # swing mode + "climate_swing_mode_settings": { + "swing_mode_command_topic": "swing-mode-command-topic", + "swing_mode_command_template": "{{ value }}", + "swing_mode_state_topic": "swing-mode-state-topic", + "swing_mode_state_template": "{{ value_json.swing_mode }}", + "swing_modes": ["off", "on"], + }, + # swing horizontal mode + "climate_swing_horizontal_mode_settings": { + "swing_horizontal_mode_command_topic": "swing-horizontal-mode-command-topic", + "swing_horizontal_mode_command_template": "{{ value }}", + "swing_horizontal_mode_state_topic": "swing-horizontal-mode-state-topic", + "swing_horizontal_mode_state_template": "{{ value_json.swing_horizontal_mode }}", + "swing_horizontal_modes": ["off", "on"], + }, + }, + ( + ( + { + "modes": ["off", "heat", "cool", "auto"], + "target_temperature_settings": { + "temperature_command_topic": "test-topic#invalid" + }, + }, + {"target_temperature_settings": "invalid_publish_topic"}, + ), + ( + { + "modes": [], + "target_temperature_settings": { + "temperature_command_topic": "test-topic" + }, + }, + {"modes": "empty_list_not_allowed"}, + ), + ( + { + "modes": ["off", "heat", "cool", "auto"], + "target_temperature_settings": { + "temperature_command_topic": "test-topic", + "min_temp": 19.0, + "max_temp": 18.0, + }, + "target_humidity_settings": { + "target_humidity_command_topic": "test-topic", + "min_humidity": 50, + "max_humidity": 40, + }, + "climate_preset_mode_settings": { + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["none"], + }, + }, + { + "target_temperature_settings": "max_below_min_temperature", + "target_humidity_settings": "max_below_min_humidity", + "climate_preset_mode_settings": "preset_mode_none_not_allowed", + }, + ), + ), + "Milk notifier Cooler", + ), + ( + MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + (), + "Milk notifier Cooler", + ), + ( + MOCK_CLIMATE_NO_TARGET_TEMP_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Cooler"}, + { + "temperature_unit": "C", + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "none", + "climate_feature_target_humidity": False, + }, + (), + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool", "auto"], + }, + (), + "Milk notifier Cooler", + ), ( MOCK_COVER_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, @@ -3130,6 +3351,9 @@ async def test_migrate_of_incompatible_config_entry( ids=[ "binary_sensor", "button", + "climate_single", + "climate_high_low", + "climate_no_target_temp", "cover", "fan", "notify_with_entity_name", @@ -3631,8 +3855,144 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( }, {"optimistic", "state_value_template", "entity_picture"}, ), + ( + ( + ConfigSubentryData( + data=MOCK_CLIMATE_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + {}, + { + "current_humidity_topic", + "action_topic", + "swing_modes", + "max_humidity", + "fan_modes", + "action_template", + "current_temperature_template", + "temperature_state_template", + "entity_picture", + "target_humidity_state_template", + "fan_mode_state_topic", + "swing_horizontal_mode_command_template", + "power_command_template", + "swing_horizontal_modes", + "current_temperature_topic", + "temperature_command_topic", + "swing_mode_command_topic", + "fan_mode_command_template", + "swing_horizontal_mode_state_template", + "preset_mode_command_template", + "swing_mode_command_template", + "temperature_state_topic", + "preset_mode_value_template", + "fan_mode_state_template", + "swing_horizontal_mode_command_topic", + "min_humidity", + "temperature_command_template", + "preset_modes", + "swing_horizontal_mode_state_topic", + "target_humidity_state_topic", + "target_humidity_command_topic", + "preset_mode_command_topic", + "payload_on", + "payload_off", + "power_command_topic", + "current_humidity_template", + "preset_mode_state_topic", + "fan_mode_command_topic", + "swing_mode_state_template", + "target_humidity_command_template", + "swing_mode_state_topic", + }, + ), + ( + ( + ConfigSubentryData( + data=MOCK_CLIMATE_HIGH_LOW_SUBENTRY_DATA_SINGLE, + subentry_type="device", + title="Mock subentry", + ), + ), + (), + { + "climate_feature_action": False, + "climate_feature_current_humidity": False, + "climate_feature_current_temperature": False, + "climate_feature_power": False, + "climate_feature_preset_modes": False, + "climate_feature_fan_modes": False, + "climate_feature_swing_horizontal_modes": False, + "climate_feature_swing_modes": False, + "climate_feature_target_temperature": "high_low", + "climate_feature_target_humidity": False, + }, + { + "mode_command_topic": "mode-command-topic", + "mode_command_template": "{{ value }}", + "mode_state_topic": "mode-state-topic", + "mode_state_template": "{{ value_json.mode }}", + "modes": ["off", "heat", "cool"], + # high/low target temperature + "target_temperature_settings": { + "temperature_low_command_topic": "temperature-low-command-topic", + "temperature_low_command_template": "{{ value }}", + "temperature_low_state_topic": "temperature-low-state-topic", + "temperature_low_state_template": "{{ value_json.temperature_low }}", + "temperature_high_command_topic": "temperature-high-command-topic", + "temperature_high_command_template": "{{ value }}", + "temperature_high_state_topic": "temperature-high-state-topic", + "temperature_high_state_template": "{{ value_json.temperature_high }}", + "min_temp": 8, + "max_temp": 28, + "precision": "0.1", + "temp_step": 1.0, + "initial": 19.0, + }, + }, + {}, + {"entity_picture"}, + ), ], - ids=["notify", "sensor", "light_basic"], + ids=["notify", "sensor", "light_basic", "climate_single", "climate_high_low"], ) async def test_subentry_reconfigure_edit_entity_single_entity( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f789d7f3be1..1aeb9843b54 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -604,6 +604,23 @@ def test_entity_device_info_schema() -> None: ) +@pytest.mark.parametrize( + ("side_effect", "error_message"), + [ + ( + ValueError("Invalid value for sensor"), + "Value error while updating " + "state of sensor.test_sensor, topic: 'test/state' " + "with payload: b'payload causing errors'", + ), + ( + TypeError("Invalid value for sensor"), + "Exception raised while updating " + "state of sensor.test_sensor, topic: 'test/state' " + "with payload: b'payload causing errors'", + ), + ], +) @pytest.mark.parametrize( "hass_config", [ @@ -625,6 +642,8 @@ async def test_handle_logging_on_writing_the_entity_state( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, + side_effect: Exception, + error_message: str, ) -> None: """Test on log handling when an error occurs writing the state.""" await mqtt_mock_entry() @@ -637,7 +656,7 @@ async def test_handle_logging_on_writing_the_entity_state( assert state.state == "initial_state" with patch( "homeassistant.helpers.entity.Entity.async_write_ha_state", - side_effect=ValueError("Invalid value for sensor"), + side_effect=side_effect, ): async_fire_mqtt_message(hass, "test/state", b"payload causing errors") await hass.async_block_till_done() @@ -645,11 +664,7 @@ async def test_handle_logging_on_writing_the_entity_state( assert state is not None assert state.state == "initial_state" assert "Invalid value for sensor" in caplog.text - assert ( - "Exception raised while updating " - "state of sensor.test_sensor, topic: 'test/state' " - "with payload: b'payload causing errors'" in caplog.text - ) + assert error_message in caplog.text async def test_receiving_non_utf8_message_gets_logged( diff --git a/tests/components/myuplink/snapshots/test_init.ambr b/tests/components/myuplink/snapshots/test_init.ambr index 14be11c36ec..66b4c9efe35 100644 --- a/tests/components/myuplink/snapshots/test_init.ambr +++ b/tests/components/myuplink/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Jäspi', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10001', - 'suggested_area': None, 'sw_version': '9682R7A', 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Nibe', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10002', - 'suggested_area': None, 'sw_version': '9682R7B', 'via_device_id': None, }) @@ -83,7 +79,6 @@ 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Nibe', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '10003', - 'suggested_area': None, 'sw_version': '9682R7C', 'via_device_id': None, }) diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 35e7f7efc29..3f8d924bdbf 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0009999992', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Bubbendorf', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ '0009999993', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Bubbendorf', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -83,7 +79,6 @@ '00:11:22:33:00:11:45:fe', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -116,7 +110,6 @@ '1002003001', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Smarther', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Corridor', 'sw_version': None, 'via_device_id': None, }) @@ -149,7 +141,6 @@ '12:34:56:00:00:a1:4c:da', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -159,7 +150,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -182,7 +172,6 @@ '12:34:56:00:01:01:01:a1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -192,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -215,7 +203,6 @@ '12:34:56:00:16:0e#0', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -225,7 +212,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -248,7 +234,6 @@ '12:34:56:00:16:0e#1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -258,7 +243,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -281,7 +265,6 @@ '12:34:56:00:16:0e#2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -291,7 +274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -314,7 +296,6 @@ '12:34:56:00:16:0e#3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -324,7 +305,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -347,7 +327,6 @@ '12:34:56:00:16:0e#4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -357,7 +336,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -380,7 +358,6 @@ '12:34:56:00:16:0e#5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -390,7 +367,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -413,7 +389,6 @@ '12:34:56:00:16:0e#6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -423,7 +398,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -446,7 +420,6 @@ '12:34:56:00:16:0e#7', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -456,7 +429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -479,7 +451,6 @@ '12:34:56:00:16:0e#8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -489,7 +460,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -512,7 +482,6 @@ '12:34:56:00:16:0e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -522,7 +491,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -545,7 +513,6 @@ '12:34:56:00:f1:62', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -555,7 +522,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -578,7 +544,6 @@ '12:34:56:03:1b:e4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -588,7 +553,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -611,7 +575,6 @@ '12:34:56:10:b9:0e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -621,7 +584,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -644,7 +606,6 @@ '12:34:56:10:f1:66', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -654,7 +615,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -677,7 +637,6 @@ '12:34:56:25:cf:a8', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -687,7 +646,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -710,7 +668,6 @@ '12:34:56:26:65:14', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -720,7 +677,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -743,7 +699,6 @@ '12:34:56:26:68:92', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -753,7 +708,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -776,7 +730,6 @@ '12:34:56:26:69:0c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -786,7 +739,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -809,7 +761,6 @@ '12:34:56:3e:c5:46', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -819,7 +770,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -842,7 +792,6 @@ '12:34:56:80:00:12:ac:f2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Legrand', @@ -852,7 +801,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -875,7 +823,6 @@ '12:34:56:80:1c:42', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -885,7 +832,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -908,7 +854,6 @@ '12:34:56:80:44:92', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -918,7 +863,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -941,7 +885,6 @@ '12:34:56:80:7e:18', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -951,7 +894,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -974,7 +916,6 @@ '12:34:56:80:bb:26', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -984,7 +925,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1007,7 +947,6 @@ '12:34:56:80:c1:ea', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1017,7 +956,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1040,7 +978,6 @@ '222452125', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1050,7 +987,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Bureau', 'sw_version': None, 'via_device_id': None, }) @@ -1073,7 +1009,6 @@ '2746182631', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1083,7 +1018,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Livingroom', 'sw_version': None, 'via_device_id': None, }) @@ -1106,7 +1040,6 @@ '2833524037', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1116,7 +1049,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Entrada', 'sw_version': None, 'via_device_id': None, }) @@ -1139,7 +1071,6 @@ '2940411577', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1149,7 +1080,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Cocina', 'sw_version': None, 'via_device_id': None, }) @@ -1172,7 +1102,6 @@ '91763b24c43d3e344f424e8b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1182,7 +1111,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1205,7 +1133,6 @@ 'Home avg', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1215,7 +1142,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1238,7 +1164,6 @@ 'Home max', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1248,7 +1173,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1271,7 +1195,6 @@ 'Home min', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netatmo', @@ -1281,7 +1204,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 2a806be8ae1..fd58e6e0002 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'FFFFFFFFFFFFF', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Netgear', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'FFFFFFFFFFFFF', - 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', 'via_device_id': None, }) diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py index 6f1fb94478d..18c038c17a0 100644 --- a/tests/components/nut/test_init.py +++ b/tests/components/nut/test_init.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr from homeassistant.setup import async_setup_component from .util import _get_mock_nutclient, async_init_integration @@ -247,6 +247,7 @@ async def test_serial_number( async def test_device_location( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test for suggested location on device.""" @@ -269,7 +270,10 @@ async def test_device_location( ) assert device_entry is not None - assert device_entry.suggested_area == mock_device_location + assert ( + device_entry.area_id + == area_registry.async_get_area_by_name(mock_device_location).id + ) async def test_update_options(hass: HomeAssistant) -> None: diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr index d9ce6f15a4d..f920b064f0b 100644 --- a/tests/components/nyt_games/snapshots/test_init.ambr +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '218886794_connections', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ '218886794_spelling_bee', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -83,7 +79,6 @@ '218886794_wordle', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'New York Times', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/ohme/snapshots/test_init.ambr b/tests/components/ohme/snapshots/test_init.ambr index ccf09f546cf..dc49f5f4042 100644 --- a/tests/components/ohme/snapshots/test_init.ambr +++ b/tests/components/ohme/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'chargerid', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ohme', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'chargerid', - 'suggested_area': None, 'sw_version': 'v2.65', 'via_device_id': None, }) diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 07e56a78fae..6ea2ad11103 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'W1122333044455', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ondilo', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'W2233304445566', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ondilo', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.7.1-stable', 'via_device_id': None, }) diff --git a/tests/components/onedrive/snapshots/test_init.ambr b/tests/components/onedrive/snapshots/test_init.ambr index 9b2ed7e4d94..2573c34e1fa 100644 --- a/tests/components/onedrive/snapshots/test_init.ambr +++ b/tests/components/onedrive/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'mock_drive_id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Microsoft', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/onewire/snapshots/test_init.ambr b/tests/components/onewire/snapshots/test_init.ambr index 9b2a0e00a62..b879541d4ca 100644 --- a/tests/components/onewire/snapshots/test_init.ambr +++ b/tests/components/onewire/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '05.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -50,7 +48,6 @@ '10.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -83,7 +79,6 @@ '12.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -116,7 +110,6 @@ '1D.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': , }) @@ -149,7 +141,6 @@ '1F.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -159,7 +150,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -182,7 +172,6 @@ '20.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -192,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -215,7 +203,6 @@ '22.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -225,7 +212,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -248,7 +234,6 @@ '26.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -258,7 +243,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -281,7 +265,6 @@ '28.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -291,7 +274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -314,7 +296,6 @@ '28.222222222222', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -324,7 +305,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -347,7 +327,6 @@ '28.222222222223', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -357,7 +336,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222223', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -380,7 +358,6 @@ '29.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -390,7 +367,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -413,7 +389,6 @@ '30.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -423,7 +398,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -446,7 +420,6 @@ '3A.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -456,7 +429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -479,7 +451,6 @@ '3B.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -489,7 +460,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -512,7 +482,6 @@ '42.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -522,7 +491,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -545,7 +513,6 @@ '7E.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Embedded Data Systems', @@ -555,7 +522,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -578,7 +544,6 @@ '7E.222222222222', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Embedded Data Systems', @@ -588,7 +553,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '222222222222', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -611,7 +575,6 @@ 'A6.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Maxim Integrated', @@ -621,7 +584,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -644,7 +606,6 @@ 'EF.111111111111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', @@ -654,7 +615,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111111', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -677,7 +637,6 @@ 'EF.111111111112', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', @@ -687,7 +646,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111112', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) @@ -710,7 +668,6 @@ 'EF.111111111113', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Hobby Boards', @@ -720,7 +677,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '111111111113', - 'suggested_area': None, 'sw_version': '3.2', 'via_device_id': None, }) diff --git a/tests/components/open_router/conftest.py b/tests/components/open_router/conftest.py index 7bb967f369f..33ca4d790c9 100644 --- a/tests/components/open_router/conftest.py +++ b/tests/components/open_router/conftest.py @@ -49,9 +49,19 @@ def conversation_subentry_data(enable_assist: bool) -> dict[str, Any]: return res +@pytest.fixture +def ai_task_data_subentry_data() -> dict[str, Any]: + """Mock AI task subentry data.""" + return { + CONF_MODEL: "google/gemini-1.5-pro", + } + + @pytest.fixture def mock_config_entry( - hass: HomeAssistant, conversation_subentry_data: dict[str, Any] + hass: HomeAssistant, + conversation_subentry_data: dict[str, Any], + ai_task_data_subentry_data: dict[str, Any], ) -> MockConfigEntry: """Mock a config entry.""" return MockConfigEntry( @@ -67,7 +77,14 @@ def mock_config_entry( subentry_type="conversation", title="GPT-3.5 Turbo", unique_id=None, - ) + ), + ConfigSubentryData( + data=ai_task_data_subentry_data, + subentry_id="ABCDEG", + subentry_type="ai_task_data", + title="Gemini 1.5 Pro", + unique_id=None, + ), ], ) diff --git a/tests/components/open_router/fixtures/models.json b/tests/components/open_router/fixtures/models.json index 0a35686094e..b17f584c0e6 100644 --- a/tests/components/open_router/fixtures/models.json +++ b/tests/components/open_router/fixtures/models.json @@ -85,6 +85,7 @@ "logit_bias", "logprobs", "top_logprobs", + "structured_outputs", "response_format" ] } diff --git a/tests/components/open_router/snapshots/test_ai_task.ambr b/tests/components/open_router/snapshots/test_ai_task.ambr new file mode 100644 index 00000000000..0839f6fef9b --- /dev/null +++ b/tests/components/open_router/snapshots/test_ai_task.ambr @@ -0,0 +1,53 @@ +# serializer version: 1 +# name: test_all_entities[ai_task.gemini_1_5_pro-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'ai_task', + 'entity_category': None, + 'entity_id': 'ai_task.gemini_1_5_pro', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'conversation': dict({ + 'should_expose': False, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'open_router', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'ABCDEG', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[ai_task.gemini_1_5_pro-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Gemini 1.5 Pro', + 'supported_features': , + }), + 'context': , + 'entity_id': 'ai_task.gemini_1_5_pro', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/open_router/test_ai_task.py b/tests/components/open_router/test_ai_task.py new file mode 100644 index 00000000000..0b6c2933be7 --- /dev/null +++ b/tests/components/open_router/test_ai_task.py @@ -0,0 +1,210 @@ +"""Test AI Task structured data generation.""" + +from unittest.mock import AsyncMock, patch + +from openai.types import CompletionUsage +from openai.types.chat import ChatCompletion, ChatCompletionMessage +from openai.types.chat.chat_completion import Choice +import pytest +from syrupy.assertion import SnapshotAssertion +import voluptuous as vol + +from homeassistant.components import ai_task +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er, selector + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.open_router.PLATFORMS", + [Platform.AI_TASK], + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_generate_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task data generation.""" + await setup_integration(hass, mock_config_entry) + + entity_id = "ai_task.gemini_1_5_pro" + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="The test data", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + result = await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id=entity_id, + instructions="Generate test data", + ) + + assert result.data == "The test data" + + +async def test_generate_structured_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task structured data generation.""" + await setup_integration(hass, mock_config_entry) + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content='{"characters": ["Mario", "Luigi"]}', + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + result = await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id="ai_task.gemini_1_5_pro", + instructions="Generate test data", + structure=vol.Schema( + { + vol.Required("characters"): selector.selector( + { + "text": { + "multiple": True, + } + } + ) + }, + ), + ) + + assert result.data == {"characters": ["Mario", "Luigi"]} + assert mock_openai_client.chat.completions.create.call_args_list[0][1][ + "response_format" + ] == { + "json_schema": { + "name": "Test Task", + "schema": { + "properties": { + "characters": { + "items": {"type": "string"}, + "type": "array", + } + }, + "required": ["characters"], + "type": "object", + }, + "strict": True, + }, + "type": "json_schema", + } + + +async def test_generate_invalid_structured_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openai_client: AsyncMock, +) -> None: + """Test AI Task with invalid JSON response.""" + await setup_integration(hass, mock_config_entry) + + mock_openai_client.chat.completions.create = AsyncMock( + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="INVALID JSON RESPONSE", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="x-ai/grok-3", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ) + ) + + with pytest.raises( + HomeAssistantError, match="Error with OpenRouter structured response" + ): + await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id="ai_task.gemini_1_5_pro", + instructions="Generate test data", + structure=vol.Schema( + { + vol.Required("characters"): selector.selector( + { + "text": { + "multiple": True, + } + } + ) + }, + ), + ) diff --git a/tests/components/open_router/test_config_flow.py b/tests/components/open_router/test_config_flow.py index 0720f6d90f5..b406e75507b 100644 --- a/tests/components/open_router/test_config_flow.py +++ b/tests/components/open_router/test_config_flow.py @@ -110,9 +110,6 @@ async def test_create_conversation_agent( mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation agent.""" - - mock_config_entry.add_to_hass(hass) - await setup_integration(hass, mock_config_entry) result = await hass.config_entries.subentries.async_init( @@ -152,9 +149,6 @@ async def test_create_conversation_agent_no_control( mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation agent without control over the LLM API.""" - - mock_config_entry.add_to_hass(hass) - await setup_integration(hass, mock_config_entry) result = await hass.config_entries.subentries.async_init( @@ -184,3 +178,63 @@ async def test_create_conversation_agent_no_control( CONF_MODEL: "openai/gpt-3.5-turbo", CONF_PROMPT: "you are an assistant", } + + +async def test_create_ai_task( + hass: HomeAssistant, + mock_open_router_client: AsyncMock, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test creating an AI Task.""" + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.subentries.async_init( + (mock_config_entry.entry_id, "ai_task_data"), + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "user" + + assert result["data_schema"].schema["model"].config["options"] == [ + {"value": "openai/gpt-4", "label": "OpenAI: GPT-4"}, + ] + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + {CONF_MODEL: "openai/gpt-4"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_MODEL: "openai/gpt-4"} + + +@pytest.mark.parametrize( + "subentry_type", + ["conversation", "ai_task_data"], +) +@pytest.mark.parametrize( + ("exception", "reason"), + [(OpenRouterError("exception"), "cannot_connect"), (Exception, "unknown")], +) +async def test_subentry_exceptions( + hass: HomeAssistant, + mock_open_router_client: AsyncMock, + mock_openai_client: AsyncMock, + mock_config_entry: MockConfigEntry, + subentry_type: str, + exception: Exception, + reason: str, +) -> None: + """Test subentry flow exceptions.""" + await setup_integration(hass, mock_config_entry) + + mock_open_router_client.get_models.side_effect = exception + + result = await hass.config_entries.subentries.async_init( + (mock_config_entry.entry_id, subentry_type), + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/open_router/test_conversation.py b/tests/components/open_router/test_conversation.py index 93f8264801a..afbdd907f93 100644 --- a/tests/components/open_router/test_conversation.py +++ b/tests/components/open_router/test_conversation.py @@ -1,6 +1,6 @@ """Tests for the OpenRouter integration.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from freezegun import freeze_time from openai.types import CompletionUsage @@ -15,6 +15,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import conversation +from homeassistant.const import Platform from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry as er, intent @@ -40,7 +41,11 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - await setup_integration(hass, mock_config_entry) + with patch( + "homeassistant.components.open_router.PLATFORMS", + [Platform.CONVERSATION], + ): + await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr index 4eff869b016..f5006ac979f 100644 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ b/tests/components/openai_conversation/snapshots/test_init.ambr @@ -11,7 +11,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': 'OpenAI', @@ -21,7 +20,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -38,7 +36,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': 'OpenAI', @@ -48,7 +45,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/overseerr/snapshots/test_init.ambr b/tests/components/overseerr/snapshots/test_init.ambr index 2709f532ef6..f861ccaa9ed 100644 --- a/tests/components/overseerr/snapshots/test_init.ambr +++ b/tests/components/overseerr/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '01JG00V55WEVTJ0CJHM0GAD7PC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/palazzetti/snapshots/test_init.ambr b/tests/components/palazzetti/snapshots/test_init.ambr index fc96cab4fad..3fca1d851ce 100644 --- a/tests/components/palazzetti/snapshots/test_init.ambr +++ b/tests/components/palazzetti/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Palazzetti', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.0.0', 'via_device_id': None, }) diff --git a/tests/components/peblar/snapshots/test_init.ambr b/tests/components/peblar/snapshots/test_init.ambr index 8a7cefc523d..21edc32c629 100644 --- a/tests/components/peblar/snapshots/test_init.ambr +++ b/tests/components/peblar/snapshots/test_init.ambr @@ -25,7 +25,6 @@ '23-45-A4O-MOF', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Peblar', @@ -35,7 +34,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '23-45-A4O-MOF', - 'suggested_area': None, 'sw_version': '1.6.1+1+WL-1', 'via_device_id': None, }) diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr index f09bfe61065..8cb8642f13a 100644 --- a/tests/components/ping/snapshots/test_sensor.ambr +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -1,4 +1,59 @@ # serializer version: 1 +# name: test_setup_and_update[jitter] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.10_10_10_10_jitter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Jitter', + 'platform': 'ping', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'jitter', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_and_update[jitter].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '10.10.10.10 Jitter', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.10_10_10_10_jitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.5', + }) +# --- # name: test_setup_and_update[round_trip_time_average] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ping/test_sensor.py b/tests/components/ping/test_sensor.py index bdc8b7d28e4..95a31aa5c08 100644 --- a/tests/components/ping/test_sensor.py +++ b/tests/components/ping/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.helpers import entity_registry as er "round_trip_time_maximum", "round_trip_time_mean_deviation", # should be None in the snapshot "round_trip_time_minimum", + "jitter", ], ) async def test_setup_and_update( diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py index 8480d7ecf5d..bfbdc9a72bd 100644 --- a/tests/components/playstation_network/conftest.py +++ b/tests/components/playstation_network/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock, patch +from psnawp_api.models import User from psnawp_api.models.group.group import Group from psnawp_api.models.trophies import ( PlatformType, @@ -14,6 +15,7 @@ from psnawp_api.models.trophies import ( import pytest from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN +from homeassistant.config_entries import ConfigSubentryData from tests.common import MockConfigEntry @@ -32,6 +34,15 @@ def mock_config_entry() -> MockConfigEntry: CONF_NPSSO: NPSSO_TOKEN, }, unique_id=PSN_ID, + subentries_data=[ + ConfigSubentryData( + data={}, + subentry_id="ABCDEF", + subentry_type="friend", + title="PublicUniversalFriend", + unique_id="fren-psn-id", + ) + ], ) @@ -170,6 +181,12 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]: ], } client.me.return_value.get_groups.return_value = [group] + fren = MagicMock( + spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend" + ) + + client.user.return_value.friends_list.return_value = [fren] + yield client diff --git a/tests/components/playstation_network/snapshots/test_diagnostics.ambr b/tests/components/playstation_network/snapshots/test_diagnostics.ambr index 894fa2d9084..ca5e9f98628 100644 --- a/tests/components/playstation_network/snapshots/test_diagnostics.ambr +++ b/tests/components/playstation_network/snapshots/test_diagnostics.ambr @@ -21,7 +21,6 @@ 'title_name': "Assassin's Creed® III Liberation", }), }), - 'availability': 'availableToPlay', 'presence': dict({ 'basicPresence': dict({ 'availability': 'availableToPlay', diff --git a/tests/components/playstation_network/snapshots/test_notify.ambr b/tests/components/playstation_network/snapshots/test_notify.ambr index 60525925787..d8c32918433 100644 --- a/tests/components/playstation_network/snapshots/test_notify.ambr +++ b/tests/components/playstation_network/snapshots/test_notify.ambr @@ -1,4 +1,53 @@ # serializer version: 1 +# name: test_notify_platform[notify.testuser_direct_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'notify', + 'entity_category': None, + 'entity_id': 'notify.testuser_direct_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Direct message', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_direct_message', + 'unit_of_measurement': None, + }) +# --- +# name: test_notify_platform[notify.testuser_direct_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Direct message', + 'supported_features': , + }), + 'context': , + 'entity_id': 'notify.testuser_direct_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_notify_platform[notify.testuser_group_publicuniversalfriend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/playstation_network/snapshots/test_sensor.ambr b/tests/components/playstation_network/snapshots/test_sensor.ambr index a00e3c4ff0a..046989cebe6 100644 --- a/tests/components/playstation_network/snapshots/test_sensor.ambr +++ b/tests/components/playstation_network/snapshots/test_sensor.ambr @@ -146,6 +146,55 @@ 'state': '2025-06-30T01:42:15+00:00', }) # --- +# name: test_sensors[sensor.testuser_last_online_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_last_online_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last online', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_last_online', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_last_online_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testuser Last online', + }), + 'context': , + 'entity_id': 'sensor.testuser_last_online_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-06-30T01:42:15+00:00', + }) +# --- # name: test_sensors[sensor.testuser_next_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -195,6 +244,102 @@ 'state': '19', }) # --- +# name: test_sensors[sensor.testuser_now_playing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_now_playing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Now playing', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'my-psn-id_now_playing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_now_playing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Now playing', + }), + 'context': , + 'entity_id': 'sensor.testuser_now_playing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'STAR WARS Jedi: Survivor™', + }) +# --- +# name: test_sensors[sensor.testuser_now_playing_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_now_playing_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Now playing', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_now_playing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_now_playing_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testuser Now playing', + }), + 'context': , + 'entity_id': 'sensor.testuser_now_playing_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'STAR WARS Jedi: Survivor™', + }) +# --- # name: test_sensors[sensor.testuser_online_id-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -244,6 +389,55 @@ 'state': 'testuser', }) # --- +# name: test_sensors[sensor.testuser_online_id_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_online_id_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Online ID', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_online_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_online_id_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png', + 'friendly_name': 'testuser Online ID', + }), + 'context': , + 'entity_id': 'sensor.testuser_online_id_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'testuser', + }) +# --- # name: test_sensors[sensor.testuser_online_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -306,6 +500,68 @@ 'state': 'availabletoplay', }) # --- +# name: test_sensors[sensor.testuser_online_status_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testuser_online_status_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Online status', + 'platform': 'playstation_network', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'fren-psn-id_online_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testuser_online_status_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'testuser Online status', + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), + }), + 'context': , + 'entity_id': 'sensor.testuser_online_status_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'availabletoplay', + }) +# --- # name: test_sensors[sensor.testuser_platinum_trophies-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/playstation_network/test_config_flow.py b/tests/components/playstation_network/test_config_flow.py index dc3ad55c64f..4194f1fb258 100644 --- a/tests/components/playstation_network/test_config_flow.py +++ b/tests/components/playstation_network/test_config_flow.py @@ -10,8 +10,17 @@ from homeassistant.components.playstation_network.config_flow import ( PSNAWPInvalidTokenError, PSNAWPNotFoundError, ) -from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.components.playstation_network.const import ( + CONF_ACCOUNT_ID, + CONF_NPSSO, + DOMAIN, +) +from homeassistant.config_entries import ( + SOURCE_USER, + ConfigEntryState, + ConfigSubentry, + ConfigSubentryData, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -67,6 +76,45 @@ async def test_form_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_form_already_configured_as_subentry(hass: HomeAssistant) -> None: + """Test we abort form login when entry is already configured as subentry of another entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="PublicUniversalFriend", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id="fren-psn-id", + subentries_data=[ + ConfigSubentryData( + data={CONF_ACCOUNT_ID: PSN_ID}, + subentry_id="ABCDEF", + subentry_type="friend", + title="test-user", + unique_id=PSN_ID, + ) + ], + ) + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NPSSO: NPSSO_TOKEN}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_as_subentry" + + @pytest.mark.parametrize( ("raise_error", "text_error"), [ @@ -325,3 +373,123 @@ async def test_flow_reconfigure( assert config_entry.data[CONF_NPSSO] == "NEW_NPSSO_TOKEN" assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow(hass: HomeAssistant) -> None: + """Test add friend subentry flow.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="test-user", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id=PSN_ID, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + subentry_id = list(config_entry.subentries)[0] + assert config_entry.subentries == { + subentry_id: ConfigSubentry( + data={}, + subentry_id=subentry_id, + subentry_type="friend", + title="PublicUniversalFriend", + unique_id="fren-psn-id", + ) + } + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test we abort add friend subentry flow when already configured.""" + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_psnawpapi") +async def test_add_friend_flow_already_configured_as_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test we abort add friend subentry flow when already configured as config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="test-user", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id=PSN_ID, + ) + fren_config_entry = MockConfigEntry( + domain=DOMAIN, + title="PublicUniversalFriend", + data={ + CONF_NPSSO: NPSSO_TOKEN, + }, + unique_id="fren-psn-id", + ) + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + fren_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(fren_config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={CONF_ACCOUNT_ID: "fren-psn-id"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_as_entry" diff --git a/tests/components/playstation_network/test_init.py b/tests/components/playstation_network/test_init.py index c1f2691d623..6db4cb6ab6a 100644 --- a/tests/components/playstation_network/test_init.py +++ b/tests/components/playstation_network/test_init.py @@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from psnawp_api.core import ( PSNAWPAuthenticationError, PSNAWPClientError, + PSNAWPForbiddenError, PSNAWPNotFoundError, PSNAWPServerError, ) @@ -263,3 +264,83 @@ async def test_trophy_title_coordinator_play_new_game( state.attributes["entity_picture"] == "https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG" ) + + +@pytest.mark.parametrize( + "exception", + [PSNAWPNotFoundError, PSNAWPServerError, PSNAWPClientError, PSNAWPForbiddenError], +) +async def test_friends_coordinator_update_data_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, + exception: Exception, +) -> None: + """Test friends coordinator setup fails in _update_data.""" + + mock_psnawpapi.user.return_value.get_presence.side_effect = [ + mock_psnawpapi.user.return_value.get_presence.return_value, + exception, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("exception", "state"), + [ + (PSNAWPNotFoundError, ConfigEntryState.SETUP_ERROR), + (PSNAWPAuthenticationError, ConfigEntryState.SETUP_ERROR), + (PSNAWPServerError, ConfigEntryState.SETUP_RETRY), + (PSNAWPClientError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_friends_coordinator_setup_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, + exception: Exception, + state: ConfigEntryState, +) -> None: + """Test friends coordinator setup fails in _async_setup.""" + + mock_psnawpapi.user.side_effect = [ + mock_psnawpapi.user.return_value, + exception, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is state + + +async def test_friends_coordinator_auth_failed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, +) -> None: + """Test friends coordinator starts reauth on authentication error.""" + mock_psnawpapi.user.side_effect = [ + mock_psnawpapi.user.return_value, + PSNAWPAuthenticationError, + ] + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == config_entry.entry_id diff --git a/tests/components/playstation_network/test_notify.py b/tests/components/playstation_network/test_notify.py index ebaac37a09f..f81e03dfcc4 100644 --- a/tests/components/playstation_network/test_notify.py +++ b/tests/components/playstation_network/test_notify.py @@ -55,11 +55,16 @@ async def test_notify_platform( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +@pytest.mark.parametrize( + "entity_id", + ["notify.testuser_group_publicuniversalfriend", "notify.testuser_direct_message"], +) @freeze_time("2025-07-28T00:00:00+00:00") async def test_send_message( hass: HomeAssistant, config_entry: MockConfigEntry, mock_psnawpapi: MagicMock, + entity_id: str, ) -> None: """Test send message.""" @@ -69,7 +74,7 @@ async def test_send_message( assert config_entry.state is ConfigEntryState.LOADED - state = hass.states.get("notify.testuser_group_publicuniversalfriend") + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN @@ -77,13 +82,13 @@ async def test_send_message( NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, { - ATTR_ENTITY_ID: "notify.testuser_group_publicuniversalfriend", + ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "henlo fren", }, blocking=True, ) - state = hass.states.get("notify.testuser_group_publicuniversalfriend") + state = hass.states.get(entity_id) assert state assert state.state == "2025-07-28T00:00:00+00:00" mock_psnawpapi.group.return_value.send_message.assert_called_once_with("henlo fren") diff --git a/tests/components/plugwise/fixtures/legacy_anna/data.json b/tests/components/plugwise/fixtures/legacy_anna/data.json index cc7e66fb174..75c12a4c8c2 100644 --- a/tests/components/plugwise/fixtures/legacy_anna/data.json +++ b/tests/components/plugwise/fixtures/legacy_anna/data.json @@ -35,6 +35,7 @@ }, "0d266432d64443e283b5d708ae98b455": { "active_preset": "home", + "available_schedules": [], "climate_mode": "heat", "control_state": "heating", "dev_class": "thermostat", @@ -44,6 +45,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["away", "vacation", "asleep", "home", "no_frost"], + "select_schedule": null, "sensors": { "illuminance": 150.8, "setpoint": 20.5, diff --git a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json index 06459a11798..f1880ba69e1 100644 --- a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json +++ b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json @@ -112,12 +112,14 @@ }, "446ac08dd04d4eff8ac57489757b7314": { "active_preset": "no_frost", + "available_schedules": [], "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", "name": "Garage", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": null, "sensors": { "temperature": 15.6 }, @@ -587,7 +589,6 @@ "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." } }, - "select_regulation_mode": "heating", "sensors": { "outdoor_temperature": 7.81 }, diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 4aa367bc116..91411c323ac 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -131,6 +131,8 @@ }), '446ac08dd04d4eff8ac57489757b7314': dict({ 'active_preset': 'no_frost', + 'available_schedules': list([ + ]), 'climate_mode': 'heat', 'control_state': 'idle', 'dev_class': 'climate', @@ -143,6 +145,7 @@ 'vacation', 'no_frost', ]), + 'select_schedule': None, 'sensors': dict({ 'temperature': 15.6, }), @@ -635,7 +638,6 @@ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), - 'select_regulation_mode': 'heating', 'sensors': dict({ 'outdoor_temperature': 7.81, }), diff --git a/tests/components/qbus/conftest.py b/tests/components/qbus/conftest.py index 9b42a6a3de8..e1febea524b 100644 --- a/tests/components/qbus/conftest.py +++ b/tests/components/qbus/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for qbus.""" -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator import json from unittest.mock import AsyncMock, patch @@ -64,3 +64,22 @@ async def setup_integration( async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config)) await hass.async_block_till_done() + + +@pytest.fixture +async def setup_integration_deferred( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + mock_config_entry: MockConfigEntry, + payload_config: JsonObjectType, +) -> Callable[[], Awaitable]: + """Set up the integration.""" + + async def run() -> None: + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config)) + await hass.async_block_till_done() + + return run diff --git a/tests/components/qbus/fixtures/payload_config.json b/tests/components/qbus/fixtures/payload_config.json index 2cad6c623db..883eca19276 100644 --- a/tests/components/qbus/fixtures/payload_config.json +++ b/tests/components/qbus/fixtures/payload_config.json @@ -116,7 +116,7 @@ { "id": "UL30", "location": "Guest bedroom", - "locationId": 0, + "locationId": 3, "name": "CURTAINS", "originalName": "CURTAINS", "refId": "000001/108", @@ -144,7 +144,7 @@ }, "id": "UL31", "location": "Living", - "locationId": 8, + "locationId": 0, "name": "SLATS", "originalName": "SLATS", "properties": { @@ -183,6 +183,340 @@ }, "refId": "000001/4", "type": "shutter" + }, + { + "id": "UL40", + "location": "Tuin", + "locationId": 12, + "name": "Luchtdruk", + "originalName": "Luchtdruk", + "refId": "000001/81", + "type": "gauge", + "variant": "AirPressure", + "actions": {}, + "properties": { + "currentValue": { + "max": 1500, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "mbar", + "write": false + } + } + }, + { + "id": "UL41", + "location": "Tuin", + "locationId": 12, + "name": "Luchtkwaliteit", + "originalName": "Luchtkwaliteit", + "refId": "000001/82", + "type": "gauge", + "variant": "AirQuality", + "actions": {}, + "properties": { + "currentValue": { + "max": 1500, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "ppm", + "write": false + } + } + }, + { + "id": "UL42", + "location": "Garage", + "locationId": 27, + "name": "Stroom", + "originalName": "Stroom", + "refId": "000001/83", + "type": "gauge", + "variant": "Current", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "kWh", + "write": false + } + } + }, + { + "id": "UL43", + "location": "Garage", + "locationId": 27, + "name": "Energie", + "originalName": "Energie", + "refId": "000001/84", + "type": "gauge", + "variant": "Energy", + "actions": {}, + "properties": { + "currentValue": { + "read": true, + "step": 0.1, + "type": "number", + "unit": "A", + "write": false + } + } + }, + { + "id": "UL44", + "location": "Garage", + "locationId": 27, + "name": "Gas", + "originalName": "Gas", + "refId": "000001/85", + "type": "gauge", + "variant": "Gas", + "actions": {}, + "properties": { + "currentValue": { + "max": 5, + "min": 0, + "read": true, + "step": 0.001, + "type": "number", + "unit": "m³/h", + "write": false + } + } + }, + { + "id": "UL45", + "location": "Garage", + "locationId": 27, + "name": "Gas flow", + "originalName": "Gas flow", + "refId": "000001/86", + "type": "gauge", + "variant": "GasFlow", + "actions": {}, + "properties": { + "currentValue": { + "max": 10, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "m³/h", + "write": false + } + } + }, + { + "id": "UL46", + "location": "Living", + "locationId": 0, + "name": "Vochtigheid living", + "originalName": "Vochtigheid living", + "refId": "000001/87", + "type": "gauge", + "variant": "Humidity", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "%", + "write": false + } + } + }, + { + "id": "UL47", + "location": "Tuin", + "locationId": 12, + "name": "Lichtsterkte tuin", + "originalName": "Lichtsterkte tuin", + "refId": "000001/88", + "type": "gauge", + "variant": "Light", + "actions": {}, + "properties": { + "currentValue": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "lx", + "write": false + } + } + }, + { + "id": "UL40", + "location": "Tuin", + "locationId": 12, + "name": "Regenput", + "originalName": "Regenput", + "refId": "000001/40", + "type": "gauge", + "variant": "WaterLevel", + "actions": {}, + "properties": { + "currentValue": { + "max": 100, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "unit": "m", + "write": false + } + } + }, + { + "id": "UL60", + "location": "Tuin", + "locationId": 12, + "name": "Weersensor", + "originalName": "Weersensor", + "refId": "000001/21007", + "type": "weatherstation", + "variant": [null], + "actions": {}, + "properties": { + "dayLight": { + "max": 1000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "light": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightEast": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightSouth": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "lightWest": { + "max": 100000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "raining": { + "read": true, + "type": "boolean", + "write": false + }, + "temperature": { + "max": 100, + "min": -100, + "read": true, + "step": 0.1, + "type": "number", + "write": false + }, + "twilight": { + "read": true, + "type": "boolean", + "write": false + }, + "wind": { + "max": 1000, + "min": 0, + "read": true, + "step": 0.1, + "type": "number", + "write": false + } + } + }, + { + "id": "UL70", + "location": "", + "locationId": 0, + "name": "Luchtsensor", + "originalName": "Luchtsensor", + "refId": "000001/224", + "type": "ventilation", + "variant": [null], + "actions": {}, + "properties": { + "co2": { + "max": 5000, + "min": 0, + "read": true, + "step": 16, + "type": "number", + "unit": "ppm", + "write": false + }, + "currRegime": { + "enumValues": ["Manueel", "Nacht", "Boost", "Uit", "Auto"], + "read": true, + "type": "enumString", + "write": true + }, + "refresh": { + "max": 100, + "min": 0, + "read": true, + "step": 1, + "type": "number", + "write": true + } + } + }, + { + "id": "UL80", + "location": "Kitchen", + "locationId": 8, + "name": "Vochtigheid keuken", + "originalName": "Vochtigheid keuken", + "properties": { + "currRegime": { + "enumValues": ["Manual", "Cook", "Boost", "Off", "Auto"], + "read": true, + "type": "enumString", + "write": true + }, + "value": { + "read": true, + "step": 1, + "type": "percent", + "write": false + } + }, + "refId": "000001/94/1", + "type": "humidity" } ] } diff --git a/tests/components/qbus/snapshots/test_sensor.ambr b/tests/components/qbus/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fe665057b1e --- /dev/null +++ b/tests/components/qbus/snapshots/test_sensor.ambr @@ -0,0 +1,1047 @@ +# serializer version: 1 +# name: test_sensor[sensor.energie-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energie', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_84', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.energie-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energie', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energie', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.gas-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_85', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Gas', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.gas_flow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas_flow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_86', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.gas_flow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Gas Flow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas_flow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.lichtsterkte_tuin-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lichtsterkte_tuin', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_88', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.lichtsterkte_tuin-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Lichtsterkte Tuin', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.lichtsterkte_tuin', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.living_th_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.living_th_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_120', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.living_th_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Living Th Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.living_th_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtdruk-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luchtdruk', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_81', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.luchtdruk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Luchtdruk', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luchtdruk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtkwaliteit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luchtkwaliteit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_82', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.luchtkwaliteit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Luchtkwaliteit', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.luchtkwaliteit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.luchtsensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luchtsensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_224', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.luchtsensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Luchtsensor', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.luchtsensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.regenput-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.regenput', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_40', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.regenput-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Regenput', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.regenput', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.stroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.stroom', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_83', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.stroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Stroom', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.vochtigheid_keuken-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vochtigheid_keuken', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_94-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.vochtigheid_keuken-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Vochtigheid Keuken', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vochtigheid_keuken', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.vochtigheid_living-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vochtigheid_living', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_87', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.vochtigheid_living-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Vochtigheid Living', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vochtigheid_living', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.weersensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Weersensor', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weersensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_daylight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_daylight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daylight', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'daylight', + 'unique_id': 'ctd_000001_21007_daylight', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_daylight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Daylight', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_daylight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_light', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_east-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_illuminance_east', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance east', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_east', + 'unique_id': 'ctd_000001_21007_light_east', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_east-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance east', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_east', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_south-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_illuminance_south', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance south', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_south', + 'unique_id': 'ctd_000001_21007_light_south', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_south-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance south', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_south', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_west-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_illuminance_west', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance west', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_west', + 'unique_id': 'ctd_000001_21007_light_west', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensor[sensor.weersensor_illuminance_west-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Weersensor Illuminance west', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.weersensor_illuminance_west', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.weersensor_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weersensor_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_21007_wind', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.weersensor_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'wind_speed', + 'friendly_name': 'Weersensor Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weersensor_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/qbus/test_sensor.py b/tests/components/qbus/test_sensor.py new file mode 100644 index 00000000000..255b29eb7f0 --- /dev/null +++ b/tests/components/qbus/test_sensor.py @@ -0,0 +1,27 @@ +"""Test Qbus sensors.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensor( + hass: HomeAssistant, + setup_integration_deferred: Callable[[], Awaitable], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor.""" + + with patch("homeassistant.components.qbus.PLATFORMS", [Platform.SENSOR]): + await setup_integration_deferred() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/rainforest_raven/snapshots/test_init.ambr b/tests/components/rainforest_raven/snapshots/test_init.ambr index 8a143f9963f..9cc89cfcc9e 100644 --- a/tests/components/rainforest_raven/snapshots/test_init.ambr +++ b/tests/components/rainforest_raven/snapshots/test_init.ambr @@ -22,7 +22,6 @@ 'abcdef0123456789', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Rainforest Automation, Inc.', @@ -32,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '2.0.0 (7400)', 'via_device_id': None, }), diff --git a/tests/components/renault/snapshots/test_init.ambr b/tests/components/renault/snapshots/test_init.ambr index 9a10083b227..15b3c599711 100644 --- a/tests/components/renault/snapshots/test_init.ambr +++ b/tests/components/renault/snapshots/test_init.ambr @@ -18,7 +18,6 @@ 'VF1CAPTURFUELVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -53,7 +51,6 @@ 'VF1CAPTURPHEVVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -63,7 +60,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -88,7 +84,6 @@ 'VF1TWINGOIIIVIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -98,7 +93,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -123,7 +117,6 @@ 'VF1ZOE40VIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -133,7 +126,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -158,7 +150,6 @@ 'VF1ZOE50VIN', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Renault', @@ -168,7 +159,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index bbaf70e0a9b..1474e90c8ea 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -599,7 +599,6 @@ async def test_fix_issue_aborted( "handler": "fake_integration", "reason": "not_given", "description_placeholders": None, - "result": None, } await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index c3aec4f0968..bc5022a7724 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -9,7 +9,11 @@ from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.roku.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -77,12 +81,13 @@ async def test_roku_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_binary_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -158,4 +163,6 @@ async def test_rokutv_binary_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 7586e85b715..2607c79086a 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -60,7 +60,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.core_config import async_process_ha_core_config -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -100,7 +104,7 @@ async def test_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/roku3-idle.json"], indirect=True) @@ -118,6 +122,7 @@ async def test_idle_setup( @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_setup( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -146,7 +151,9 @@ async def test_tv_setup( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) @pytest.mark.parametrize( diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index e65424e3e66..72f57729cc4 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -12,7 +12,11 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from . import UPNP_SERIAL @@ -60,12 +64,13 @@ async def test_roku_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "7.5.0" assert device_entry.hw_version == "4200X" - assert device_entry.suggested_area is None + assert device_entry.area_id is None @pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_sensors( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, @@ -106,4 +111,6 @@ async def test_rokutv_sensors( assert device_entry.entry_type is None assert device_entry.sw_version == "9.2.0" assert device_entry.hw_version == "7820X" - assert device_entry.suggested_area == "Living room" + assert ( + device_entry.area_id == area_registry.async_get_area_by_name("Living room").id + ) diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 8eb77006061..25925ac3865 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '8381BE13', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index e3185a06b24..b02f80f1dfd 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -21,7 +21,6 @@ '00:11:22:33:44:55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Russound', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index b29b824a7dd..4be166ecf25 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -22,7 +22,6 @@ 'be9554b9-c9fb-41f4-8920-22da015376a4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -32,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -57,7 +55,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -67,7 +64,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -96,7 +92,6 @@ 'be9554b9-c9fb-41f4-8920-22da015376a4', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -106,7 +101,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/schlage/snapshots/test_init.ambr b/tests/components/schlage/snapshots/test_init.ambr index a7f94b80038..1b6cc3f1cdb 100644 --- a/tests/components/schlage/snapshots/test_init.ambr +++ b/tests/components/schlage/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'test', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Schlage', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0', 'via_device_id': None, }) diff --git a/tests/components/sensibo/snapshots/test_entity.ambr b/tests/components/sensibo/snapshots/test_entity.ambr index 80ee847cb55..ba075d764f5 100644 --- a/tests/components/sensibo/snapshots/test_entity.ambr +++ b/tests/components/sensibo/snapshots/test_entity.ambr @@ -22,7 +22,6 @@ 'ABC999111', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -32,7 +31,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': 'Hallway', 'sw_version': 'SKY30046', 'via_device_id': None, }), @@ -57,7 +55,6 @@ 'AAZZAAZZ', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -67,7 +64,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654321', - 'suggested_area': 'Kitchen', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -92,7 +88,6 @@ 'BBZZBBZZ', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -102,7 +97,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0987654329', - 'suggested_area': 'Bedroom', 'sw_version': 'PUR00111', 'via_device_id': None, }), @@ -123,7 +117,6 @@ 'AABBCC', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -133,7 +126,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V17', 'via_device_id': , }), diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 0ee34eebf3f..f046f95ed42 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), @@ -151,7 +149,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -161,7 +158,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 39dd9e512ae..5d5c6d0edba 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index cd762a4b2ea..0440505859a 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -18,7 +18,6 @@ 'e4:5d:51:00:11:22', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', 'via_device_id': None, }), diff --git a/tests/components/slide_local/snapshots/test_init.ambr b/tests/components/slide_local/snapshots/test_init.ambr index 5b1a9f5ce2f..cc93a49b98a 100644 --- a/tests/components/slide_local/snapshots/test_init.ambr +++ b/tests/components/slide_local/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Innovation in Motion', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890ab', - 'suggested_area': None, 'sw_version': 2, 'via_device_id': None, }) diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 6ce3992d2b4..17a9d6691cc 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -20,7 +20,6 @@ '7c16163e-c94e-482f-95f6-139ae0cd9d5e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -30,7 +29,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -53,7 +51,6 @@ 'f0af21a2-d5a1-437c-b10a-b34a87394b71', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -63,7 +60,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Toilet', 'sw_version': None, 'via_device_id': None, }) @@ -86,7 +82,6 @@ 'bf53a150-f8a4-45d1-aac4-86252475d551', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -96,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -119,7 +113,6 @@ '68e786a6-7f61-4c3a-9e13-70b803cf782b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -129,7 +122,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -152,7 +144,6 @@ '286ba274-4093-4bcb-849c-a1a3efe7b1e5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -162,7 +153,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -185,7 +175,6 @@ '10e06a70-ee7d-4832-85e9-a0a06a7a05bd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Arlo', @@ -195,7 +184,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -218,7 +206,6 @@ '571af102-15db-4030-b76b-245a691f74a5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WonderLabs Company', @@ -228,7 +215,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -251,7 +237,6 @@ 'd0268a69-abfb-4c92-a646-61cec2e510ad', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -261,7 +246,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -284,7 +268,6 @@ '2d9a892b-1c93-45a5-84cb-0e81889498c6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -294,7 +277,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -317,7 +299,6 @@ 'a3a970ea-e09c-9c04-161b-94c934e21666', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -327,7 +308,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ASM-KR-TP1-22-ACMB1M_16240426', 'via_device_id': None, }) @@ -350,7 +330,6 @@ '4165c51e-bf6b-c5b6-fd53-127d6248754b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -360,7 +339,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AEH-WW-TP1-22-AE6000_17240903', 'via_device_id': None, }) @@ -383,7 +361,6 @@ '96a5ef74-5832-a84b-f1f7-ca799957065d', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -393,7 +370,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -416,7 +392,6 @@ 'c76d6f38-1b7f-13dd-37b5-db18d5272783', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -426,7 +401,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'ARTIK051_PRAC_20K_11230313', 'via_device_id': None, }) @@ -449,7 +423,6 @@ '4ece486b-89db-f06a-d54d-748b676b4d8e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -459,7 +432,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'ARA-WW-TP1-22-COMMON_11240702', 'via_device_id': None, }) @@ -482,7 +454,6 @@ 'F8042E25-0E53-0000-0000-000000000000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -492,7 +463,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -515,7 +485,6 @@ '808dbd84-f357-47e2-a0cd-3b66fa22d584', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -525,7 +494,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -548,7 +516,6 @@ '2bad3237-4886-e699-1b90-4a51a3d55c8a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -558,7 +525,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'AKS-WW-TP2-20-MICROWAVE-OTR_40230125', 'via_device_id': None, }) @@ -581,7 +547,6 @@ '9447959a-0dfa-6b27-d40d-650da525c53f', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -591,7 +556,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1X-21-OVEN_40211229', 'via_device_id': None, }) @@ -614,7 +578,6 @@ '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -624,7 +587,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'AKS-WW-TP1-20-OVEN-3-CR_40240205', 'via_device_id': None, }) @@ -647,7 +609,6 @@ '7db87911-7dce-1cf2-7119-b953432a2f09', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -657,7 +618,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'A-RFWW-TP2-21-COMMON_20220110', 'via_device_id': None, }) @@ -680,7 +640,6 @@ '7d3feb98-8a36-4351-c362-5e21ad3a78dd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -690,7 +649,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20240616.213423', 'via_device_id': None, }) @@ -713,7 +671,6 @@ '5758b2ec-563e-f39b-ec39-208e54aabf60', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -723,7 +680,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'A-RFWW-TP1-22-REV1_20241030', 'via_device_id': None, }) @@ -746,7 +702,6 @@ '05accb39-2017-c98b-a5ab-04a81f4d3d9a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -756,7 +711,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250123.105306', 'via_device_id': None, }) @@ -779,7 +733,6 @@ '3442dfc6-17c0-a65f-dae0-4c6e01786f44', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -789,7 +742,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '1.0', 'via_device_id': None, }) @@ -812,7 +764,6 @@ '1f98ebd0-ac48-d802-7f62-000001200100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -822,7 +773,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -845,7 +795,6 @@ '6a7d5349-0a66-0277-058d-000001200101', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -855,7 +804,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -878,7 +826,6 @@ '3810e5ad-5351-d9f9-12ff-000001200000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -888,7 +835,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '20250317.1', 'via_device_id': None, }) @@ -911,7 +857,6 @@ 'f36dc7ce-cac0-0667-dc14-a3704eb5e676', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -921,7 +866,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_DW_A51_20_COMMON_30230714', 'via_device_id': None, }) @@ -944,7 +888,6 @@ 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -954,7 +897,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_DF_TP2_20_COMMON_30230807', 'via_device_id': None, }) @@ -977,7 +919,6 @@ '02f7256e-8353-5bdd-547f-bd5b1647e01b', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -987,7 +928,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1010,7 +950,6 @@ '3a6c4e05-811d-5041-e956-3d04c424cbcd', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1020,7 +959,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1043,7 +981,6 @@ 'f984b91d-f250-9d42-3436-33f09a422a47', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1053,7 +990,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'DA_WM_TP2_20_COMMON_30230804', 'via_device_id': None, }) @@ -1076,7 +1012,6 @@ '63803fae-cbed-f356-a063-2cf148ae3ca7', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1086,7 +1021,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_A51_20_COMMON_30230708', 'via_device_id': None, }) @@ -1109,7 +1043,6 @@ 'b854ca5f-dc54-140d-6349-758b4d973c41', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1119,7 +1052,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'DA_WM_TP1_21_COMMON_30240927', 'via_device_id': None, }) @@ -1142,7 +1074,6 @@ 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1152,7 +1083,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1175,7 +1105,6 @@ 'd5dc3299-c266-41c7-bd08-f540aea54b89', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1185,7 +1114,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206213001', 'via_device_id': None, }) @@ -1208,7 +1136,6 @@ '028469cb-6e89-4f14-8d9a-bfbca5e0fbfc', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1218,7 +1145,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250206151734', 'via_device_id': None, }) @@ -1241,7 +1167,6 @@ '1888b38f-6246-4f1e-911b-bfcfb66999db', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'ecobee', @@ -1251,7 +1176,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '250308073247', 'via_device_id': None, }) @@ -1274,7 +1198,6 @@ 'f1af21a2-d5a1-437c-b10a-b34a87394b71', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1284,7 +1207,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1307,7 +1229,6 @@ '3b57dca3-9a90-4f27-ba80-f947b1e60d58', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'CopperLabs', @@ -1317,7 +1238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1340,7 +1260,6 @@ 'aaedaf28-2ae0-4c1d-b57e-87f6a420c298', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1350,7 +1269,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1373,7 +1291,6 @@ '656569c2-7976-4232-a789-34b4d1176c3a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1383,7 +1300,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1406,7 +1322,6 @@ '6d95a8b7-4ee3-429a-a13a-00ec9354170c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1416,7 +1331,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1439,7 +1353,6 @@ '5e5b97f3-3094-44e6-abc0-f61283412d6a', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1449,7 +1362,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1472,7 +1384,6 @@ '69a271f6-6537-4982-8cd9-979866872692', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1482,7 +1393,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1505,7 +1415,6 @@ '440063de-a200-40b5-8a6b-f3399eaa0370', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Signify Netherlands B.V.', @@ -1515,7 +1424,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1538,7 +1446,6 @@ 'cb958955-b015-498c-9e62-fc0c51abd054', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Signify Netherlands B.V.', @@ -1548,7 +1455,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.122.2', 'via_device_id': None, }) @@ -1571,7 +1477,6 @@ 'afcf3b91-0000-1111-2222-ddff2a0a6577', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1581,7 +1486,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'HW-Q80RWWB-1012.6', 'via_device_id': None, }) @@ -1604,7 +1508,6 @@ '71afed1c-006d-4e48-b16e-e7f88f9fd638', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1614,7 +1517,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1637,7 +1539,6 @@ '83d660e4-b0c8-4881-a674-d9f1730366c1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1647,7 +1548,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1670,7 +1570,6 @@ 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1680,7 +1579,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'V310XXU1AWK1', 'via_device_id': None, }) @@ -1703,7 +1601,6 @@ '184c67cc-69e2-44b6-8f73-55c963068ad9', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1713,7 +1610,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1736,7 +1632,6 @@ '692ea4e9-2022-4ed8-8a57-1b884a59cc38', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1746,7 +1641,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -1769,7 +1663,6 @@ '7d246592-93db-4d72-a10d-5a51793ece8c', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1779,7 +1672,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1802,7 +1694,6 @@ '2409a73c-918a-4d1f-b4f5-c27468c71d70', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Emerson', @@ -1812,7 +1703,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '6004971003', 'via_device_id': None, }) @@ -1835,7 +1725,6 @@ 'bf4b1167-48a3-4af7-9186-0900a678ffa5', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Sensibo', @@ -1845,7 +1734,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SKY40147', 'via_device_id': None, }) @@ -1868,7 +1756,6 @@ '550a1c72-65a0-4d55-b97b-75168e055398', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1878,7 +1765,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1901,7 +1787,6 @@ 'c85fced9-c474-4a47-93c2-037cc7829536', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -1911,7 +1796,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -1934,7 +1818,6 @@ '6602696a-1e48-49e4-919f-69406f5b5da1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -1944,7 +1827,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.3.1 Build 240621 Rel.162048', 'via_device_id': None, }) @@ -1967,7 +1849,6 @@ '0d94e5db-8501-2355-eb4f-214163702cac', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -1977,7 +1858,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'SAT-iMX8M23WWC-1010.5', 'via_device_id': None, }) @@ -2000,7 +1880,6 @@ 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -2010,7 +1889,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'SAT-MT8532D24WWC-1016.0', 'via_device_id': None, }) @@ -2033,7 +1911,6 @@ '5cc1c096-98b9-460c-8f1c-1045509ec605', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -2043,7 +1920,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'latest', 'via_device_id': None, }) @@ -2066,7 +1942,6 @@ '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Samsung Electronics', @@ -2076,7 +1951,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': 'T-KTMAKUC-1290.3', 'via_device_id': None, }) @@ -2099,7 +1973,6 @@ '2894dc93-0f11-49cc-8a81-3a684cebebf6', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2109,7 +1982,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2132,7 +2004,6 @@ '612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2142,7 +2013,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2165,7 +2035,6 @@ 'a2a6018b-2663-4727-9d1d-8f56953b5116', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2175,7 +2044,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2198,7 +2066,6 @@ 'a9f587c5-5d8b-4273-8907-e7f609af5158', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2208,7 +2075,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': None, 'via_device_id': None, }) @@ -2235,7 +2101,6 @@ '074fa784-8be8-4c70-8e22-6f5ed6f81b7e', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -2245,7 +2110,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Theater', 'sw_version': '000.055.00005', 'via_device_id': None, }) diff --git a/tests/components/smarty/snapshots/test_init.ambr b/tests/components/smarty/snapshots/test_init.ambr index a292cc97f47..989e95bde7e 100644 --- a/tests/components/smarty/snapshots/test_init.ambr +++ b/tests/components/smarty/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '01JAZ5DPW8C62D620DGYNG2R8H', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Salda', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 127, 'via_device_id': None, }) diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index ba374199254..7f46daef13c 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -17,7 +17,6 @@ 'id': , 'identifiers': set({ }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'SMLIGHT', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': 'core: v2.3.6 / zigbee: 20240314', 'via_device_id': None, }) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index b15d7698e05..84ad624cdc8 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -54,7 +54,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import area_registry as ar, entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_UPNP, @@ -83,11 +83,15 @@ async def test_device_registry( assert reg_device.manufacturer == "Sonos" assert reg_device.name == "Zone A" # Default device provides battery info, area should not be suggested - assert reg_device.suggested_area is None + assert reg_device.area_id is None async def test_device_registry_not_portable( - hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: DeviceRegistry, + async_setup_sonos, + soco, ) -> None: """Test non-portable sonos device registered in the device registry to ensure area suggested.""" soco.get_battery_info.return_value = {} @@ -97,7 +101,7 @@ async def test_device_registry_not_portable( identifiers={("sonos", "RINCON_test")} ) assert reg_device is not None - assert reg_device.suggested_area == "Zone A" + assert reg_device.area_id == area_registry.async_get_area_by_name("Zone A").id async def test_entity_basic( diff --git a/tests/components/squeezebox/snapshots/test_init.ambr b/tests/components/squeezebox/snapshots/test_init.ambr index 3fc65be834a..afd90d026de 100644 --- a/tests/components/squeezebox/snapshots/test_init.ambr +++ b/tests/components/squeezebox/snapshots/test_init.ambr @@ -21,7 +21,6 @@ 'aa:bb:cc:dd:ee:ff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Ralph Irving & Adrian Smith', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) @@ -62,7 +60,6 @@ 'ff:ee:dd:cc:bb:aa', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'https://lyrion.org/ / Ralph Irving & Adrian Smith', @@ -72,7 +69,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '', 'via_device_id': , }) diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index 440f682370b..6e3e5be0459 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -765,9 +765,7 @@ async def test_squeezebox_call_query( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_call_method( @@ -784,9 +782,7 @@ async def test_squeezebox_call_method( }, blocking=True, ) - configured_player.async_query.assert_called_once_with( - "test_command", "param1", "param2" - ) + configured_player.async_query.assert_called_with("test_command", "param1", "param2") async def test_squeezebox_invalid_state( diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index 09c953da06b..27214fde28d 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -30,3 +30,12 @@ def mock_get_status(): """Mock get_status.""" with patch.object(SwitchBotAPI, "get_status") as mock_get_status: yield mock_get_status + + +@pytest.fixture(scope="package", autouse=True) +def mock_after_command_refresh(): + """Mock after command refresh.""" + with patch( + "homeassistant.components.switchbot_cloud.const.AFTER_COMMAND_REFRESH", 0 + ): + yield diff --git a/tests/components/switchbot_cloud/test_light.py b/tests/components/switchbot_cloud/test_light.py new file mode 100644 index 00000000000..e4f39c0d530 --- /dev/null +++ b/tests/components/switchbot_cloud/test_light.py @@ -0,0 +1,300 @@ +"""Test for the Switchbot Light Entity.""" + +from unittest.mock import patch + +from switchbot_api import Device, SwitchBotAPI + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_coordinator_data_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test coordinator data is none.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [None] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_UNKNOWN + + +async def test_strip_light_turn_off( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test strip light turn off.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "off", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + # state = hass.states.get(entity_id) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once() + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + +async def test_rgbww_light_turn_off( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test rgbww light turn_off.""" + + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light 3", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "off", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once() + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + +async def test_strip_light_turn_on( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test strip light turn on.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "on", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "brightness": 99}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "rgb_color": (255, 246, 158)}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "color_temp_kelvin": 3333}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + +async def test_rgbww_light_turn_on( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test rgbww light turn on.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType="Strip Light 3", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "color": "0:0:0", "colorTemperature": 4567}, + {"power": "on", "brightness": 10, "color": "0:0:0", "colorTemperature": 5555}, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + { + "power": "on", + "brightness": 10, + "color": "255:255:255", + "colorTemperature": 5555, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "color_temp_kelvin": 2800}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "brightness": 99}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "rgb_color": (255, 246, 158)}, + blocking=True, + ) + mock_send_command.assert_called() + state = hass.states.get(entity_id) + assert state.state is STATE_ON diff --git a/tests/components/switchbot_cloud/test_vacuum.py b/tests/components/switchbot_cloud/test_vacuum.py new file mode 100644 index 00000000000..daa52f4f183 --- /dev/null +++ b/tests/components/switchbot_cloud/test_vacuum.py @@ -0,0 +1,522 @@ +"""Test for the switchbot_cloud vacuum.""" + +from unittest.mock import patch + +from switchbot_api import ( + Device, + VacuumCleanerV2Commands, + VacuumCleanerV3Commands, + VacuumCleanMode, + VacuumCommands, +) + +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.components.switchbot_cloud.const import VACUUM_FAN_SPEED_QUIET +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + DOMAIN as VACUUM_DOMAIN, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + VacuumActivity, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_coordinator_data_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test coordinator data is none.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + None, + ] + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == STATE_UNKNOWN + + +async def test_k10_plus_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.POW_LEVEL, "command", "0" + ) + + +async def test_k10_plus_return_to_base( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10 plus return to base.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + } + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_RETURN_TO_BASE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.DOCK, "command", "default" + ) + + +async def test_k10_plus_pause( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10 plus pause.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + } + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.STOP, "command", "default" + ) + + +async def test_k10_plus_set_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K10+", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K10+", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCommands.START, "command", "default" + ) + + +async def test_k20_plus_pro_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K10 plus set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "waterLevel": 1, + "times": 1, + }, + ) + + +async def test_k20_plus_pro_return_to_base( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro return to base.""" + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_RETURN_TO_BASE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCleanerV2Commands.DOCK, "command", "default" + ) + + +async def test_k20_plus_pro_pause( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro pause.""" + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Charging", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + state = hass.states.get(entity_id) + + assert state.state == VacuumActivity.DOCKED.value + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", VacuumCleanerV2Commands.PAUSE, "command", "default" + ) + + +async def test_k20_plus_pro_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test K20+ Pro start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="K20+ Pro", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "K20+ Pro", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.START_CLEAN, + "command", + { + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": 1, + "times": 1, + }, + }, + ) + + +async def test_k10_plus_pro_combo_set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test k10+ Pro Combo set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="Robot Vacuum Cleaner K10+ Pro Combo", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "Robot Vacuum Cleaner K10+ Pro Combo", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV2Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "times": 1, + }, + ) + + +async def test_s20_start( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test s20 start.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="S20", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "s20", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV3Commands.START_CLEAN, + "command", + { + "action": VacuumCleanMode.SWEEP.value, + "param": { + "fanLevel": 0, + "waterLevel": 1, + "times": 1, + }, + }, + ) + + +async def test_s20set_fan_speed( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test s20 set fan speed.""" + + mock_list_devices.side_effect = [ + [ + Device( + version="V1.0", + deviceId="vacuum-id-1", + deviceName="vacuum-1", + deviceType="S20", + hubDeviceId="test-hub-id", + ) + ] + ] + mock_get_status.side_effect = [ + { + "deviceType": "S20", + "workingStatus": "Cleaning", + "battery": 50, + "onlineStatus": "online", + }, + ] + + await configure_integration(hass) + entity_id = "vacuum.vacuum_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_SPEED: VACUUM_FAN_SPEED_QUIET}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "vacuum-id-1", + VacuumCleanerV3Commands.CHANGE_PARAM, + "command", + { + "fanLevel": 1, + "waterLevel": 1, + "times": 1, + }, + ) diff --git a/tests/components/tado/fixtures/heating_circuits.json b/tests/components/tado/fixtures/heating_circuits.json deleted file mode 100644 index 723ceb76f95..00000000000 --- a/tests/components/tado/fixtures/heating_circuits.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "number": 1, - "driverSerialNo": "RU1234567890", - "driverShortSerialNo": "RU1234567890" - } -] diff --git a/tests/components/tado/fixtures/zone_control.json b/tests/components/tado/fixtures/zone_control.json deleted file mode 100644 index 584fe9f3c92..00000000000 --- a/tests/components/tado/fixtures/zone_control.json +++ /dev/null @@ -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 - } - ] - } -} diff --git a/tests/components/tado/snapshots/test_binary_sensor.ambr b/tests/components/tado/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..5920e6bbf11 --- /dev/null +++ b/tests/components/tado/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1230 @@ +# serializer version: 1 +# name: test_entities[binary_sensor.air_conditioning_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning Overlay', + 'termination': 'TADO_MODE', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning with fanlevel Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with fanlevel Overlay', + 'termination': 'MANUAL', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with fanlevel Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_fanlevel_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning with fanlevel Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_fanlevel_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Air Conditioning with swing Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with swing Overlay', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Air Conditioning with swing Power', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.air_conditioning_with_swing_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.air_conditioning_with_swing_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Air Conditioning with swing Window', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioning_with_swing_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Baseboard Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_early_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_early_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Early start', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'early_start', + 'unique_id': 'early start 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_early_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Early start', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_early_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Overlay', + 'termination': 'MANUAL', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Baseboard Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baseboard_heater_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'open window 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.baseboard_heater_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Baseboard Heater Window', + }), + 'context': , + 'entity_id': 'binary_sensor.baseboard_heater_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Second Water Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Second Water Heater Overlay', + 'termination': 'TADO_MODE', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.second_water_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.second_water_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Second Water Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.second_water_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.water_heater_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'link 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Water Heater Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.water_heater_overlay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_overlay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overlay', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'overlay', + 'unique_id': 'overlay 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_overlay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Water Heater Overlay', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_overlay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[binary_sensor.water_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.water_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Water Heater Power', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.wr1_connection_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wr1_connection_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection state', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'connection_state', + 'unique_id': 'connection state WR1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.wr1_connection_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'WR1 Connection state', + }), + 'context': , + 'entity_id': 'binary_sensor.wr1_connection_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[binary_sensor.wr4_connection_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wr4_connection_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection state', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'connection_state', + 'unique_id': 'connection state WR4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.wr4_connection_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'WR4 Connection state', + }), + 'context': , + 'entity_id': 'binary_sensor.wr4_connection_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tado/snapshots/test_climate.ambr b/tests/components/tado/snapshots/test_climate.ambr index 6ba35b6f6f2..fb1dd6d46d1 100644 --- a/tests/components/tado/snapshots/test_climate.ambr +++ b/tests/components/tado/snapshots/test_climate.ambr @@ -93,6 +93,429 @@ }), ) # --- +# name: test_entities[climate.air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'target_temp_step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 60.9, + 'current_temperature': 24.8, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'friendly_name': 'Air Conditioning', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 17.8, + }), + 'context': , + 'entity_id': 'climate.air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_entities[climate.air_conditioning_with_fanlevel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'high', + 'medium', + 'auto', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'swing_modes': list([ + 'vertical', + 'horizontal', + 'both', + 'off', + ]), + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning_with_fanlevel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning_with_fanlevel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 70.9, + 'current_temperature': 24.3, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'high', + 'fan_modes': list([ + 'high', + 'medium', + 'auto', + 'low', + ]), + 'friendly_name': 'Air Conditioning with fanlevel', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'swing_mode': 'both', + 'swing_modes': list([ + 'vertical', + 'horizontal', + 'both', + 'off', + ]), + 'target_temp_step': 1.0, + 'temperature': 25.0, + }), + 'context': , + 'entity_id': 'climate.air_conditioning_with_fanlevel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_entities[climate.air_conditioning_with_swing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'swing_modes': list([ + 'on', + 'off', + ]), + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.air_conditioning_with_swing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'AIR_CONDITIONING 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.air_conditioning_with_swing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 42.3, + 'current_temperature': 20.9, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'friendly_name': 'Air Conditioning with swing', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'swing_mode': 'off', + 'swing_modes': list([ + 'on', + 'off', + ]), + 'target_temp_step': 1.0, + 'temperature': 20.0, + }), + 'context': , + 'entity_id': 'climate.air_conditioning_with_swing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_entities[climate.baseboard_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'target_temp_step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.baseboard_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'tado', + 'unique_id': 'HEATING 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.baseboard_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 45.2, + 'current_temperature': 20.6, + 'default_overlay_seconds': None, + 'default_overlay_type': 'MANUAL', + 'friendly_name': 'Baseboard Heater', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 31.0, + 'min_temp': 16.0, + 'offset_celsius': -1.0, + 'offset_fahrenheit': -1.8, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'away', + 'home', + 'auto', + ]), + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 20.5, + }), + 'context': , + 'entity_id': 'climate.baseboard_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- # name: test_heater_set_temperature _Call( tuple( diff --git a/tests/components/tado/snapshots/test_diagnostics.ambr b/tests/components/tado/snapshots/test_diagnostics.ambr index 34d26c222fa..eefb818a88c 100644 --- a/tests/components/tado/snapshots/test_diagnostics.ambr +++ b/tests/components/tado/snapshots/test_diagnostics.ambr @@ -62,13 +62,6 @@ 'presence': 'HOME', 'presenceLocked': False, }), - 'heating_circuits': dict({ - 'RU1234567890': dict({ - 'driverSerialNo': 'RU1234567890', - 'driverShortSerialNo': 'RU1234567890', - 'number': 1, - }), - }), 'weather': dict({ 'outsideTemperature': dict({ '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)", }), }), - '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_device': dict({ diff --git a/tests/components/tado/snapshots/test_sensor.ambr b/tests/components/tado/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2040bd737c8 --- /dev/null +++ b/tests/components/tado/snapshots/test_sensor.ambr @@ -0,0 +1,1240 @@ +# serializer version: 1 +# name: test_entities[sensor.air_conditioning_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning AC', + 'time': '2020-03-05T04:01:07.162Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 3 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning Humidity', + 'state_class': , + 'time': '2020-03-05T03:57:38.850Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.9', + }) +# --- +# name: test_entities[sensor.air_conditioning_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 3 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 3 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-05T03:57:38.850Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.76', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with fanlevel AC', + 'time': '2022-07-13T18: 06: 58.183Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 6 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning with fanlevel Humidity', + 'state_class': , + 'time': '2024-06-28T22: 23: 15.679Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '70.9', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 6 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with fanlevel Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_fanlevel_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 6 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_with_fanlevel_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning with fanlevel Temperature', + 'setting': 0, + 'state_class': , + 'time': '2024-06-28T22: 23: 15.679Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_fanlevel_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.3', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac', + 'unique_id': 'ac 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with swing AC', + 'time': '2020-03-27T23:02:22.260Z', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ON', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 5 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Air Conditioning with swing Humidity', + 'state_class': , + 'time': '2020-03-28T02:09:27.830Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.3', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 5 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Air Conditioning with swing Tado mode', + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_conditioning_with_swing_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 5 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.air_conditioning_with_swing_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Conditioning with swing Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-28T02:09:27.830Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_conditioning_with_swing_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.88', + }) +# --- +# name: test_entities[sensor.baseboard_heater_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating', + 'unique_id': 'heating 1 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.baseboard_heater_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Heating', + 'state_class': , + 'time': '2020-03-10T07:47:45.978Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.baseboard_heater_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'humidity 1 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.baseboard_heater_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Baseboard Heater Humidity', + 'state_class': , + 'time': '2020-03-10T07:44:11.947Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.2', + }) +# --- +# name: test_entities[sensor.baseboard_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 1 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.baseboard_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.baseboard_heater_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.baseboard_heater_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature 1 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.baseboard_heater_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Baseboard Heater Temperature', + 'setting': 0, + 'state_class': , + 'time': '2020-03-10T07:44:11.947Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.baseboard_heater_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.65', + }) +# --- +# name: test_entities[sensor.home_name_automatic_geofencing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_automatic_geofencing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic geofencing', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'automatic_geofencing', + 'unique_id': 'automatic geofencing 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_automatic_geofencing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Automatic geofencing', + }), + 'context': , + 'entity_id': 'sensor.home_name_automatic_geofencing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) +# --- +# name: test_entities[sensor.home_name_geofencing_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_geofencing_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Geofencing mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'geofencing_mode', + 'unique_id': 'geofencing mode 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_geofencing_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Geofencing mode', + }), + 'context': , + 'entity_id': 'sensor.home_name_geofencing_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Home (Auto)', + }) +# --- +# name: test_entities[sensor.home_name_outdoor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_outdoor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outdoor temperature', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'outdoor_temperature', + 'unique_id': 'outdoor temperature 1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.home_name_outdoor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'home name Outdoor temperature', + 'state_class': , + 'time': '2020-12-22T08:13:13.652Z', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_name_outdoor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.46', + }) +# --- +# name: test_entities[sensor.home_name_solar_percentage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_solar_percentage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Solar percentage', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'solar_percentage', + 'unique_id': 'solar percentage 1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.home_name_solar_percentage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Solar percentage', + 'state_class': , + 'time': '2020-12-22T08:13:13.652Z', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_name_solar_percentage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.1', + }) +# --- +# name: test_entities[sensor.home_name_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Tado mode', + }), + 'context': , + 'entity_id': 'sensor.home_name_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.home_name_weather_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_name_weather_condition', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather condition', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_condition', + 'unique_id': 'weather condition 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.home_name_weather_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'home name Weather condition', + 'time': '2020-12-22T08:13:13.652Z', + }), + 'context': , + 'entity_id': 'sensor.home_name_weather_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'fog', + }) +# --- +# name: test_entities[sensor.second_water_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.second_water_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.second_water_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Second Water Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.second_water_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- +# name: test_entities[sensor.water_heater_tado_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_heater_tado_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tado mode', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tado_mode', + 'unique_id': 'tado mode 2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.water_heater_tado_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Heater Tado mode', + }), + 'context': , + 'entity_id': 'sensor.water_heater_tado_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'HOME', + }) +# --- diff --git a/tests/components/tado/snapshots/test_switch.ambr b/tests/components/tado/snapshots/test_switch.ambr new file mode 100644 index 00000000000..c2f00649f1d --- /dev/null +++ b/tests/components/tado/snapshots/test_switch.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_entities[switch.baseboard_heater_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.baseboard_heater_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': '1 1 child-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.baseboard_heater_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Baseboard Heater Child lock', + }), + 'context': , + 'entity_id': 'switch.baseboard_heater_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tado/snapshots/test_water_heater.ambr b/tests/components/tado/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..5e10af60c8d --- /dev/null +++ b/tests/components/tado/snapshots/test_water_heater.ambr @@ -0,0 +1,139 @@ +# serializer version: 1 +# name: test_entities[water_heater.second_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.second_water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '4 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[water_heater.second_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Second Water Heater', + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + 'operation_mode': 'heat', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 30.0, + }), + 'context': , + 'entity_id': 'water_heater.second_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_entities[water_heater.water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tado', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '2 1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[water_heater.water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Water Heater', + 'max_temp': 31.0, + 'min_temp': 16.0, + 'operation_list': list([ + 'auto', + 'heat', + 'off', + ]), + 'operation_mode': 'auto', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 65.0, + }), + 'context': , + 'entity_id': 'water_heater.water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 78cd91c56c6..9a0b94883fa 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -1,69 +1,35 @@ -"""The sensor tests for the tado platform.""" +"""The binary sensor tests for the tado platform.""" -from homeassistant.const import STATE_OFF, STATE_ON +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_air_con_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of aircon sensors.""" + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of binary sensor.""" await async_init_integration(hass) - state = hass.states.get("binary_sensor.air_conditioning_power") - assert state.state == STATE_ON + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - state = hass.states.get("binary_sensor.air_conditioning_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.air_conditioning_overlay") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.air_conditioning_window") - assert state.state == STATE_OFF - - -async def test_heater_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.baseboard_heater_power") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_early_start") - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.baseboard_heater_overlay") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.baseboard_heater_window") - assert state.state == STATE_OFF - - -async def test_water_heater_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of water heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.water_heater_connectivity") - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.water_heater_overlay") - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.water_heater_power") - assert state.state == STATE_ON - - -async def test_home_create_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of home binary sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("binary_sensor.wr1_connection_state") - assert state.state == STATE_ON + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 0699551c9c0..71ee0471e5f 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -1,5 +1,6 @@ -"""The sensor tests for the tado platform.""" +"""The climate tests for the tado platform.""" +from collections.abc import AsyncGenerator from unittest.mock import patch from PyTado.interface.api.my_tado import TadoZone @@ -13,128 +14,33 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.components.tado import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration - -async def test_air_con(hass: HomeAssistant) -> None: - """Test creation of aircon climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.air_conditioning") - assert state.state == "cool" - - expected_attributes = { - "current_humidity": 60.9, - "current_temperature": 24.8, - "fan_mode": "auto", - "fan_modes": ["auto", "high", "medium", "low"], - "friendly_name": "Air Conditioning", - "hvac_action": "cooling", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "supported_features": 409, - "target_temp_step": 1, - "temperature": 17.8, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) +from tests.common import MockConfigEntry, snapshot_platform -async def test_heater(hass: HomeAssistant) -> None: - """Test creation of heater climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.baseboard_heater") - assert state.state == "heat" - - expected_attributes = { - "current_humidity": 45.2, - "current_temperature": 20.6, - "friendly_name": "Baseboard Heater", - "hvac_action": "idle", - "hvac_modes": ["off", "auto", "heat"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "supported_features": 401, - "target_temp_step": 1, - "temperature": 20.5, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.CLIMATE]): + yield -async def test_smartac_with_swing(hass: HomeAssistant) -> None: - """Test creation of smart ac with swing climate.""" - - await async_init_integration(hass) - - state = hass.states.get("climate.air_conditioning_with_swing") - assert state.state == "auto" - - expected_attributes = { - "current_humidity": 42.3, - "current_temperature": 20.9, - "fan_mode": "auto", - "fan_modes": ["auto", "high", "medium", "low"], - "friendly_name": "Air Conditioning with swing", - "hvac_action": "heating", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 30.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "swing_modes": ["on", "off"], - "supported_features": 441, - "target_temp_step": 1.0, - "temperature": 20.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) - - -async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( - hass: HomeAssistant, +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: - """Test creation of smart ac with swing climate.""" + """Test creation of climate entities.""" await async_init_integration(hass) - state = hass.states.get("climate.air_conditioning_with_fanlevel") - assert state.state == "heat" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - expected_attributes = { - "current_humidity": 70.9, - "current_temperature": 24.3, - "fan_mode": "high", - "fan_modes": ["high", "medium", "auto", "low"], - "friendly_name": "Air Conditioning with fanlevel", - "hvac_action": "heating", - "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], - "max_temp": 31.0, - "min_temp": 16.0, - "preset_mode": "auto", - "preset_modes": ["away", "home", "auto"], - "swing_modes": ["vertical", "horizontal", "both", "off"], - "supported_features": 441, - "target_temp_step": 1.0, - "temperature": 25.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_heater_set_temperature( diff --git a/tests/components/tado/test_select.py b/tests/components/tado/test_select.py deleted file mode 100644 index e57b7510d1b..00000000000 --- a/tests/components/tado/test_select.py +++ /dev/null @@ -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 diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 0fa7a9ca370..8445683d11d 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -1,62 +1,35 @@ """The sensor tests for the tado platform.""" +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_air_con_create_sensors(hass: HomeAssistant) -> None: - """Test creation of aircon sensors.""" + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.SENSOR]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of sensor entities.""" await async_init_integration(hass) - state = hass.states.get("sensor.air_conditioning_tado_mode") - assert state.state == "HOME" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - state = hass.states.get("sensor.air_conditioning_temperature") - assert state.state == "24.76" - - state = hass.states.get("sensor.air_conditioning_ac") - assert state.state == "ON" - - state = hass.states.get("sensor.air_conditioning_humidity") - assert state.state == "60.9" - - -async def test_home_create_sensors(hass: HomeAssistant) -> None: - """Test creation of home sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.home_name_outdoor_temperature") - assert state.state == "7.46" - - state = hass.states.get("sensor.home_name_solar_percentage") - assert state.state == "2.1" - - state = hass.states.get("sensor.home_name_weather_condition") - assert state.state == "fog" - - -async def test_heater_create_sensors(hass: HomeAssistant) -> None: - """Test creation of heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.baseboard_heater_tado_mode") - assert state.state == "HOME" - - state = hass.states.get("sensor.baseboard_heater_temperature") - assert state.state == "20.65" - - state = hass.states.get("sensor.baseboard_heater_humidity") - assert state.state == "45.2" - - -async def test_water_heater_create_sensors(hass: HomeAssistant) -> None: - """Test creation of water heater sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.water_heater_tado_mode") - assert state.state == "HOME" + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/tado/test_switch.py b/tests/components/tado/test_switch.py index 2112f3a1ac7..6bfdf1283d1 100644 --- a/tests/components/tado/test_switch.py +++ b/tests/components/tado/test_switch.py @@ -1,28 +1,45 @@ -"""The sensor tests for the tado platform.""" +"""The switch tests for the tado platform.""" +from collections.abc import AsyncGenerator from unittest.mock import patch import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.components.tado import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform + CHILD_LOCK_SWITCH_ENTITY = "switch.baseboard_heater_child_lock" -async def test_child_lock(hass: HomeAssistant) -> None: - """Test creation of child lock entity.""" +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.SWITCH]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test creation of switch entities.""" await async_init_integration(hass) - state = hass.states.get(CHILD_LOCK_SWITCH_ENTITY) - assert state.state == STATE_OFF + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/tado/test_water_heater.py b/tests/components/tado/test_water_heater.py index 223a1fda16a..7c13ba1604e 100644 --- a/tests/components/tado/test_water_heater.py +++ b/tests/components/tado/test_water_heater.py @@ -1,49 +1,35 @@ -"""The sensor tests for the tado platform.""" +"""The water heater tests for the tado platform.""" +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .util import async_init_integration +from tests.common import MockConfigEntry, snapshot_platform -async def test_water_heater_create_sensors(hass: HomeAssistant) -> None: + +@pytest.fixture(autouse=True) +def setup_platforms() -> AsyncGenerator[None]: + """Set up the platforms for the tests.""" + with patch("homeassistant.components.tado.PLATFORMS", [Platform.WATER_HEATER]): + yield + + +async def test_entities( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test creation of water heater.""" await async_init_integration(hass) - state = hass.states.get("water_heater.water_heater") - assert state.state == "auto" + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - expected_attributes = { - "current_temperature": None, - "friendly_name": "Water Heater", - "max_temp": 31.0, - "min_temp": 16.0, - "operation_list": ["auto", "heat", "off"], - "operation_mode": "auto", - "supported_features": 3, - "target_temp_high": None, - "target_temp_low": None, - "temperature": 65.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) - - state = hass.states.get("water_heater.second_water_heater") - assert state.state == "heat" - - expected_attributes = { - "current_temperature": None, - "friendly_name": "Second Water Heater", - "max_temp": 31.0, - "min_temp": 16.0, - "operation_list": ["auto", "heat", "off"], - "operation_mode": "heat", - "supported_features": 3, - "target_temp_high": None, - "target_temp_low": None, - "temperature": 30.0, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 5ef0ab5dbf2..8ee7209acb2 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -20,10 +20,8 @@ async def async_init_integration( me_fixture = "me.json" weather_fixture = "weather.json" home_fixture = "home.json" - home_heating_circuits_fixture = "heating_circuits.json" home_state_fixture = "home_state.json" zones_fixture = "zones.json" - zone_control_fixture = "zone_control.json" zone_states_fixture = "zone_states.json" # WR1 Device @@ -72,10 +70,6 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/", 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( "https://my.tado.com/api/v2/homes/1/weather", 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", 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( "https://login.tado.com/oauth2/token", text=await async_load_fixture(hass, token_fixture, DOMAIN), diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index 5d166018160..2cf93435bbf 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -66,7 +66,6 @@ '_3c_e9_e_6d_21_84_-door1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -76,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -148,7 +146,6 @@ '_3c_e9_e_6d_21_84_-door2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -158,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 0e4bb4e4e41..12b99997db0 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -70,7 +70,6 @@ '_3c_e9_e_6d_21_84_', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -80,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index a1a98b028e3..a14dcfc44f1 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -67,7 +67,6 @@ '_3c_e9_e_6d_21_84_-door1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -77,7 +76,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) @@ -150,7 +148,6 @@ '_3c_e9_e_6d_21_84_-door2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -160,7 +157,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': , }) diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index ffa2c5df7fd..f9132530cee 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -79,7 +79,6 @@ '_3c_e9_e_6d_21_84_', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tailwind', @@ -89,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '10.10', 'via_device_id': None, }) diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 28b5ef7a7ed..38874d08f3a 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '0000-0000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '0000-0000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index a568a7dcd82..a73e5c746aa 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -66,7 +66,6 @@ '98765', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tedee', @@ -76,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index 66c3c43ea86..489cb034ac2 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -96,7 +96,7 @@ def mock_external_calls() -> Generator[None]: max_reaction_count=100, accent_color_id=AccentColor.COLOR_000, ) - test_user = User(123456, "Testbot", True) + test_user = User(123456, "Testbot", True, "mock last name", "mock username") message = Message( message_id=12345, date=datetime.now(), diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 80b9859ceab..eec2bd5ecf7 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -174,6 +174,15 @@ async def test_send_message( assert len(events) == 1 assert events[0].context == context + config_entry = hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, "1234567890:ABC" + ) + assert events[0].data["bot"]["config_entry_id"] == config_entry.entry_id + assert events[0].data["bot"]["id"] == 123456 + assert events[0].data["bot"]["first_name"] == "Testbot" + assert events[0].data["bot"]["last_name"] == "mock last name" + assert events[0].data["bot"]["username"] == "mock username" + assert len(response["chats"]) == 1 assert (response["chats"][0]["message_id"]) == 12345 @@ -479,6 +488,16 @@ async def test_polling_platform_message_text_update( assert len(events) == 1 assert events[0].data["text"] == update_message_text["message"]["text"] + + config_entry = hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, "1234567890:ABC" + ) + assert events[0].data["bot"]["config_entry_id"] == config_entry.entry_id + assert events[0].data["bot"]["id"] == 123456 + assert events[0].data["bot"]["first_name"] == "Testbot" + assert events[0].data["bot"]["last_name"] == "mock last name" + assert events[0].data["bot"]["username"] == "mock username" + assert isinstance(events[0].context, Context) @@ -752,7 +771,7 @@ async def test_send_message_no_chat_id_error( ) assert err.value.translation_key == "missing_allowed_chat_ids" - assert err.value.translation_placeholders["bot_name"] == "Testbot" + assert err.value.translation_placeholders["bot_name"] == "Testbot mock last name" async def test_send_message_config_entry_error( diff --git a/tests/components/template/snapshots/test_cover.ambr b/tests/components/template/snapshots/test_cover.ambr new file mode 100644 index 00000000000..177dc8c883b --- /dev/null +++ b/tests/components/template/snapshots/test_cover.ambr @@ -0,0 +1,16 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/template/snapshots/test_fan.ambr b/tests/components/template/snapshots/test_fan.ambr new file mode 100644 index 00000000000..3026176ef97 --- /dev/null +++ b/tests/components/template/snapshots/test_fan.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/snapshots/test_light.ambr b/tests/components/template/snapshots/test_light.ambr new file mode 100644 index 00000000000..0740d56a72e --- /dev/null +++ b/tests/components/template/snapshots/test_light.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'My template', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/snapshots/test_lock.ambr b/tests/components/template/snapshots/test_lock.ambr new file mode 100644 index 00000000000..250fc6ba8d4 --- /dev/null +++ b/tests/components/template/snapshots/test_lock.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- diff --git a/tests/components/template/snapshots/test_vacuum.ambr b/tests/components/template/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..01cc9c8ba82 --- /dev/null +++ b/tests/components/template/snapshots/test_vacuum.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'docked', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 22acb1b2292..08104025582 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -121,6 +121,44 @@ BINARY_SENSOR_OPTIONS = { }, {}, ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + "open", + {"one": "open", "two": "closed"}, + {}, + { + "device_class": "garage", + "set_cover_position": [ + { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"position": "{{ position }}"}, + } + ], + }, + { + "device_class": "garage", + "set_cover_position": [ + { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"position": "{{ position }}"}, + } + ], + }, + {}, + ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + {}, + ), ( "image", {"url": "{{ states('sensor.one') }}"}, @@ -131,6 +169,26 @@ BINARY_SENSOR_OPTIONS = { {"verify_ssl": True}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + {}, + ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + "locked", + {"one": "locked", "two": "unlocked"}, + {}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + {}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -181,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") @@ -288,6 +356,18 @@ async def test_config_flow( {}, {}, ), + ( + "cover", + {"state": "{{ 'open' }}"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "image", { @@ -296,6 +376,18 @@ async def test_config_flow( {"verify_ssl": True}, {"verify_ssl": True}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -332,6 +424,12 @@ async def test_config_flow( {"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"}, ), + ( + "vacuum", + {"state": "{{ states('vacuum.one') }}"}, + {"start": []}, + {"start": []}, + ), ], ) async def test_config_flow_device( @@ -474,6 +572,26 @@ async def test_config_flow_device( }, "state", ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + {"state": "{{ states('cover.two') }}"}, + ["open", "closed"], + {"one": "open", "two": "closed"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + "state", + ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"state": "{{ states('fan.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + "state", + ), ( "image", { @@ -491,6 +609,26 @@ async def test_config_flow_device( }, "url", ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"state": "{{ states('light.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + "state", + ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"state": "{{ states('lock.two') }}"}, + ["locked", "unlocked"], + {"one": "locked", "two": "unlocked"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + "state", + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -551,6 +689,16 @@ async def test_config_flow_device( {}, "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") @@ -1315,6 +1463,18 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "cover", + {"state": "{{ states('cover.one') }}"}, + {"set_cover_position": []}, + {"set_cover_position": []}, + ), + ( + "fan", + {"state": "{{ states('fan.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "image", { @@ -1324,6 +1484,18 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), + ( + "lock", + {"state": "{{ states('lock.one') }}"}, + {"lock": [], "unlock": []}, + {"lock": [], "unlock": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -1366,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( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 48f45d879cd..692567c7aa8 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import cover, template from homeassistant.components.cover import ( @@ -32,9 +33,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er 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_cover" TEST_ENTITY_ID = f"cover.{TEST_OBJECT_ID}" @@ -237,6 +239,7 @@ async def setup_position_cover( { TEST_OBJECT_ID: { **COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position_template": position_template, } }, @@ -247,6 +250,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -256,6 +260,7 @@ async def setup_position_cover( count, { **NAMED_COVER_ACTIONS, + "set_cover_position": SET_COVER_POSITION, "position": position_template, }, ) @@ -563,6 +568,7 @@ async def test_template_position( position: int | None, expected: str, caplog: pytest.LogCaptureFixture, + calls: list[ServiceCall], ) -> None: """Test the position_template attribute.""" hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) @@ -578,6 +584,19 @@ async def test_template_position( assert state.state == expected assert "ValueError" not in caplog.text + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, "position": 10}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_position") == position + assert state.state == expected + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( @@ -1604,3 +1623,52 @@ async def test_empty_action_config( state.attributes["supported_features"] == CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | supported_feature ) + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a cover from a config entry.""" + + hass.states.async_set( + "cover.test_state", + "open", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('cover.test_state') }}", + "set_cover_position": [], + "template_type": COVER_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("cover.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, + cover.DOMAIN, + {"name": "My template", "state": "{{ 'open' }}", "set_cover_position": []}, + ) + + assert state["state"] == CoverState.OPEN diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index c0af18166df..b9161edf61a 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.components import fan, template @@ -21,10 +22,11 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er 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.fan import common +from tests.typing import WebSocketGenerator TEST_OBJECT_ID = "test_fan" TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}" @@ -1881,3 +1883,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a fan from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "on", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "turn_on": [], + "turn_off": [], + "template_type": fan.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("fan.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, + fan.DOMAIN, + { + "name": "My template", + "state": "{{ 'on' }}", + "turn_on": [], + "turn_off": [], + }, + ) + + assert state["state"] == STATE_ON diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index b42eba0665d..0549f9981e7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import light, template from homeassistant.components.light import ( @@ -30,9 +31,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er 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 # Represent for light's availability _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" @@ -2791,3 +2793,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a light from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "on", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "turn_on": [], + "turn_off": [], + "template_type": light.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("light.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, + light.DOMAIN, + { + "name": "My template", + "state": "{{ 'on' }}", + "turn_on": [], + "turn_off": [], + }, + ) + + assert state["state"] == STATE_ON diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 457c5b7bf5c..823306015bf 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import setup 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.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_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) 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 diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 21dea28b73f..0ae98a23ae4 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_ICON, CONF_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import Context, HomeAssistant, ServiceCall @@ -63,11 +64,11 @@ _VALUE_INPUT_NUMBER_CONFIG = { } TEST_STATE_ENTITY_ID = "number.test_state" - +TEST_AVAILABILITY_ENTITY_ID = "binary_sensor.test_availability" TEST_STATE_TRIGGER = { "trigger": { "trigger": "state", - "entity_id": [TEST_STATE_ENTITY_ID], + "entity_id": [TEST_STATE_ENTITY_ID, TEST_AVAILABILITY_ENTITY_ID], }, "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, "action": [ @@ -191,19 +192,6 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None: async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" - with assert_setup_component(0, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "number": { - "set_value": {"service": "script.set_value"}, - } - } - }, - ) - with assert_setup_component(0, "template"): assert await setup.async_setup_component( hass, @@ -578,6 +566,91 @@ async def test_device_id( assert template_entity.device_id == device_entry.id +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "set_value": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_number") +async def test_optimistic(hass: HomeAssistant) -> None: + """Test configuration with optimistic state.""" + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 + + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 2}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 2 + + +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "set_value": [], + "state": "{{ states('number.test_state') }}", + "availability": "{{ is_state('binary_sensor.test_availability', 'on') }}", + }, + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_number") +async def test_availability(hass: HomeAssistant) -> None: + """Test configuration with optimistic state.""" + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "on") + hass.states.async_set(TEST_STATE_ENTITY_ID, "4.0") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "off") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_STATE_ENTITY_ID, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "on") + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 2 + + @pytest.mark.parametrize( ("count", "number_config"), [ diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 540b4eccd3b..6c7222645b6 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import template, vacuum 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.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.typing import WebSocketGenerator TEST_OBJECT_ID = "test_vacuum" TEST_ENTITY_ID = f"vacuum.{TEST_OBJECT_ID}" @@ -1261,3 +1263,56 @@ async def test_optimistic_option( state = hass.states.get(TEST_ENTITY_ID) 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 diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 6e2a2ab2f6b..7eac7ff28aa 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -132,6 +132,7 @@ async def setup_weather( { "platform": "template", "name": "test", + "unique_id": "abc123", "attribution_template": "{{ states('sensor.attribution') }}", "condition_template": "sunny", "temperature_template": "{{ states('sensor.temperature') | float }}", diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr index c482d33de86..7ce99965900 100644 --- a/tests/components/tesla_fleet/snapshots/test_init.ambr +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'LRWXF7EK4KC700000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRWXF7EK4KC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -83,7 +79,6 @@ 'abd-123', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -116,7 +110,6 @@ 'bcd-234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index f1011034d63..ee27bc9f0af 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '123456', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123456', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'LRW3F7EK4NC700000', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'LRW3F7EK4NC700000', - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -83,7 +79,6 @@ 'abd-123', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '123', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) @@ -116,7 +110,6 @@ 'bcd-234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tesla', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '234', - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/tile/snapshots/test_init.ambr b/tests/components/tile/snapshots/test_init.ambr index ffdf6a6251a..9e2620313a0 100644 --- a/tests/components/tile/snapshots/test_init.ambr +++ b/tests/components/tile/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '19264d2dffdbca32', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tile Inc.', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '01.12.14.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index c8251bccd4f..ed5f935f286 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -420,7 +420,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -430,7 +429,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index 84cc8f73bf3..37cfb4a36c0 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -602,7 +602,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -612,7 +611,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index f50c5d70362..b17b30bbbb4 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -72,7 +72,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -82,7 +81,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index df63291175a..01738bff943 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -82,7 +82,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -92,7 +91,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': , }) diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index ad0321accef..b48ef8d336b 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -186,7 +186,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -196,7 +195,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 5ff1d9c5458..da4dc26317b 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 9fc5181c45d..1db6e3cf57d 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 5c22c2f7d83..05d645552bb 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index 761df4fcf21..45bad203bc9 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 4b04587db05..6d1d2fa3bad 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr index 68d14270b55..e3a7f1d95fe 100644 --- a/tests/components/tplink/snapshots/test_vacuum.ambr +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -21,7 +21,6 @@ '123456789ABCDEFGH', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'TP-Link', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index ab2d28ef645..7d6cd32959c 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -2,28 +2,29 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch from tuya_sharing import CustomerDevice -from homeassistant.components.tuya import ManagerCompat +from homeassistant.components.tuya import DeviceListener, ManagerCompat from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry DEVICE_MOCKS = { - "cl_am43_corded_motor_zigbee_cover": [ + "cl_zah67ekd": [ # https://github.com/home-assistant/core/issues/71242 Platform.COVER, Platform.SELECT, ], - "clkg_curtain_switch": [ + "clkg_nhyj64w2": [ # https://github.com/home-assistant/core/issues/136055 Platform.COVER, Platform.LIGHT, ], - "co2bj_air_detector": [ + "co2bj_yrr3eiyiacm31ski": [ # https://github.com/home-assistant/core/issues/133173 Platform.BINARY_SENSOR, Platform.NUMBER, @@ -31,15 +32,7 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SIREN, ], - "cs_arete_two_12l_dehumidifier_air_purifier": [ - Platform.BINARY_SENSOR, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "cs_emma_dehumidifier": [ + "cs_ka2wfrdoogpvgzfi": [ # https://github.com/home-assistant/core/issues/119865 Platform.BINARY_SENSOR, Platform.FAN, @@ -48,138 +41,359 @@ DEVICE_MOCKS = { Platform.SENSOR, Platform.SWITCH, ], - "cs_smart_dry_plus": [ + "cs_vmxuxszzjwp5smli": [ # https://github.com/home-assistant/core/issues/119865 Platform.FAN, Platform.HUMIDIFIER, ], - "cwjwq_smart_odor_eliminator": [ + "cs_zibqa9dutqyaxym2": [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ], + "cwjwq_agwu93lr": [ # https://github.com/orgs/home-assistant/discussions/79 Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ], - "cwwsq_cleverio_pf100": [ + "cwwsq_wfkzyy0evslzsmoi": [ # https://github.com/home-assistant/core/issues/144745 Platform.NUMBER, Platform.SENSOR, ], - "cwysj_pixi_smart_drinking_fountain": [ + "cwysj_z3rpyvznfcch99aa": [ # https://github.com/home-assistant/core/pull/146599 Platform.SENSOR, Platform.SWITCH, ], - "cz_dual_channel_metering": [ + "cz_0g1fmqh6d5io7lcn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "cz_2jxesipczks0kdct": [ # https://github.com/home-assistant/core/issues/147149 Platform.SENSOR, Platform.SWITCH, ], - "dj_smart_light_bulb": [ - # https://github.com/home-assistant/core/pull/126242 - Platform.LIGHT + "cz_cuhokdii7ojyw8k2": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, ], - "dlq_earu_electric_eawcpt": [ + "cz_dntgh2ngvshfxpsz": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "cz_hj0a5c7ckzzexu8l": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SENSOR, + Platform.SWITCH, + ], + "cz_t0a4hwsf8anfsadp": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SELECT, + Platform.SWITCH, + ], + "dc_l3bpgg8ibsagon4x": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_8szt7whdvwpmxglk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_8y0aquaa8v6tho8w": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_baf9tt9lb8t5uc7z": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_d4g0fbsoaal841o6": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_djnozmdyqyriow8z": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ekwolitfjhxn55js": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_fuupmcr2mb1odkja": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_hp6orhaqm6as3jnv": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_hpc8ddyfv85haxa7": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_iayz2jmtlipjnxj7": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_idnfq7xbx8qewyoa": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ilddqqih3tucdk68": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_j1bgp31cffutizub": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_lmnt3uyltk1xffrt": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_mki13ie507rlry4r": [ + # https://github.com/home-assistant/core/pull/126242 + Platform.LIGHT, + ], + "dj_nbumqpv8vz61enji": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_nlxvjzy1hoeiqsg6": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_oe0cpnjg": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_riwp3k79": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_tmsloaroqavbucgn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_ufq2xwuzd4nb0qdr": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_vqwcnabamzrc2kab": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_xokdfs6kh5ednakk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zakhnlpdiu0ycdxn": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zav1pa32pyxray78": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dj_zputiamzanuk6yky": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + ], + "dlq_0tnvg2xaisqdadcf": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, Platform.SWITCH, ], - "dlq_metering_3pn_wifi": [ + "dlq_kxdr6su0c55p7bbo": [ # https://github.com/home-assistant/core/issues/143499 Platform.SENSOR, ], - "gyd_night_light": [ + "gyd_lgekqfxdabipm3tn": [ # https://github.com/home-assistant/core/issues/133173 Platform.LIGHT, ], - "kg_smart_valve": [ + "hps_2aaelwxk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.NUMBER, + ], + "kg_gbm9ata1zrzaez4a": [ # https://github.com/home-assistant/core/issues/148347 Platform.SWITCH, ], - "kj_bladeless_tower_fan": [ + "kj_yrzylxax1qspdgpp": [ # https://github.com/orgs/home-assistant/discussions/61 Platform.FAN, Platform.SELECT, Platform.SWITCH, ], - "ks_tower_fan": [ + "ks_j9fa8ahzac8uvlfl": [ # https://github.com/orgs/home-assistant/discussions/329 Platform.FAN, Platform.LIGHT, Platform.SWITCH, ], - "kt_serenelife_slpac905wuk_air_conditioner": [ + "kt_5wnlzekkstwcdsvm": [ # https://github.com/home-assistant/core/pull/148646 Platform.CLIMATE, ], - "mal_alarm_host": [ + "mal_gyitctrjj1kefxp2": [ # Alarm Host support Platform.ALARM_CONTROL_PANEL, Platform.NUMBER, Platform.SWITCH, ], - "mcs_door_sensor": [ + "mcs_7jIGJAymiH8OsFFb": [ # https://github.com/home-assistant/core/issues/108301 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "qccdz_ac_charging_control": [ + "pc_t2afic7i3v1bwhfp": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "pc_trjopo1vdlt9q1tg": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.SWITCH, + ], + "pir_3amxzozho9xp4mkh": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "pir_fcdjzz3s": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "pir_wqz93nrdomectyoz": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "qccdz_7bvgooyjhiua1yyq": [ # https://github.com/home-assistant/core/issues/136207 Platform.SWITCH, ], - "qxj_temp_humidity_external_probe": [ - # https://github.com/home-assistant/core/issues/136472 - Platform.SENSOR, - ], - "qxj_weather_station": [ + "qxj_fsea1lat3vuktbt6": [ # https://github.com/orgs/home-assistant/discussions/318 Platform.SENSOR, ], - "rqbj_gas_sensor": [ + "qxj_is2indt9nlth6esa": [ + # https://github.com/home-assistant/core/issues/136472 + Platform.SENSOR, + ], + "rqbj_4iqe2hsfyd86kwwc": [ # https://github.com/orgs/home-assistant/discussions/100 Platform.BINARY_SENSOR, Platform.SENSOR, ], - "sfkzq_valve_controller": [ + "sfkzq_o6dagifntoafakst": [ # https://github.com/home-assistant/core/issues/148116 Platform.SWITCH, ], - "tdq_4_443": [ + "sgbj_ulv4nnue7gqp0rjk": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.NUMBER, + Platform.SELECT, + Platform.SIREN, + ], + "sp_drezasavompxpcgm": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SELECT, + Platform.SWITCH, + ], + "sp_rjKXWRohlvOTyLBu": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SELECT, + Platform.SWITCH, + ], + "tdq_cq1p0nt0a4rixnex": [ # https://github.com/home-assistant/core/issues/146845 Platform.SELECT, Platform.SWITCH, ], - "wk_air_conditioner": [ + "tyndj_pyakuuoc": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + ], + "wfcon_b25mh8sxawsgndck": [ + # https://github.com/home-assistant/core/issues/149704 + ], + "wk_aqoouq7x": [ # https://github.com/home-assistant/core/issues/146263 Platform.CLIMATE, Platform.SWITCH, ], - "ydkt_dolceclima_unsupported": [ - # https://github.com/orgs/home-assistant/discussions/288 - # unsupported device - no platforms - ], - "wk_wifi_smart_gas_boiler_thermostat": [ + "wk_fi6dne5tu4t1nm6j": [ # https://github.com/orgs/home-assistant/discussions/243 Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ], - "wsdcg_temperature_humidity": [ + "wsdcg_g2y6z3p3ja2qhyav": [ # https://github.com/home-assistant/core/issues/102769 Platform.SENSOR, ], - "wxkg_wireless_switch": [ + "wxkg_l8yaz4um5b3pwyvf": [ # https://github.com/home-assistant/core/issues/93975 Platform.EVENT, Platform.SENSOR, ], - "zndb_smart_meter": [ + "ydkt_jevroj5aguwdbs2e": [ + # https://github.com/orgs/home-assistant/discussions/288 + # unsupported device - no platforms + ], + "ywbj_gf9dejhmzffgdyfj": [ + # https://github.com/home-assistant/core/issues/149704 + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + "ywcgq_wtzwyhkev3b4ubns": [ + # https://community.home-assistant.io/t/something-is-wrong-with-tuya-tank-level-sensors-with-the-new-official-integration/689321 + # not (yet) supported + ], + "zndb_ze8faryrxr0glqnn": [ # https://github.com/home-assistant/core/issues/138372 Platform.SENSOR, ], } +class MockDeviceListener(DeviceListener): + """Mocked DeviceListener for testing.""" + + async def async_send_device_update( + self, + hass: HomeAssistant, + device: CustomerDevice, + updated_status_properties: dict[str, Any] | None = None, + ) -> None: + """Mock update device method.""" + property_list: list[str] = [] + if updated_status_properties: + for key, value in updated_status_properties.items(): + if key not in device.status: + raise ValueError( + f"Property {key} not found in device status: {device.status}" + ) + device.status[key] = value + property_list.append(key) + self.update_device(device, property_list) + await hass.async_block_till_done() + + async def initialize_entry( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index cac9359a8d3..73752590637 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -21,6 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import json_dumps from homeassistant.util import dt as dt_util +from . import MockDeviceListener + from tests.common import MockConfigEntry, async_load_json_object_fixture @@ -184,3 +186,13 @@ async def mock_device(hass: HomeAssistant, mock_device_code: str) -> CustomerDev if device.status_range[key].type == "Json": device.status[key] = json_dumps(value) return device + + +@pytest.fixture +def mock_listener( + hass: HomeAssistant, mock_manager: ManagerCompat +) -> MockDeviceListener: + """Create a DeviceListener for testing.""" + listener = MockDeviceListener(hass, mock_manager) + mock_manager.add_device_listener(listener) + return listener diff --git a/tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json b/tests/components/tuya/fixtures/cl_zah67ekd.json similarity index 100% rename from tests/components/tuya/fixtures/cl_am43_corded_motor_zigbee_cover.json rename to tests/components/tuya/fixtures/cl_zah67ekd.json diff --git a/tests/components/tuya/fixtures/clkg_curtain_switch.json b/tests/components/tuya/fixtures/clkg_nhyj64w2.json similarity index 100% rename from tests/components/tuya/fixtures/clkg_curtain_switch.json rename to tests/components/tuya/fixtures/clkg_nhyj64w2.json diff --git a/tests/components/tuya/fixtures/co2bj_air_detector.json b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json similarity index 100% rename from tests/components/tuya/fixtures/co2bj_air_detector.json rename to tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json diff --git a/tests/components/tuya/fixtures/cs_emma_dehumidifier.json b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json similarity index 100% rename from tests/components/tuya/fixtures/cs_emma_dehumidifier.json rename to tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json diff --git a/tests/components/tuya/fixtures/cs_smart_dry_plus.json b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json similarity index 100% rename from tests/components/tuya/fixtures/cs_smart_dry_plus.json rename to tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json diff --git a/tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json similarity index 100% rename from tests/components/tuya/fixtures/cs_arete_two_12l_dehumidifier_air_purifier.json rename to tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json diff --git a/tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json similarity index 100% rename from tests/components/tuya/fixtures/cwjwq_smart_odor_eliminator.json rename to tests/components/tuya/fixtures/cwjwq_agwu93lr.json diff --git a/tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json similarity index 100% rename from tests/components/tuya/fixtures/cwwsq_cleverio_pf100.json rename to tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json diff --git a/tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json similarity index 100% rename from tests/components/tuya/fixtures/cwysj_pixi_smart_drinking_fountain.json rename to tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json diff --git a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json new file mode 100644 index 00000000000..760972e7fb0 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "01155072c4dd573f92b8", + "name": "Apollo light", + "category": "cz", + "product_id": "0g1fmqh6d5io7lcn", + "product_name": "Mini Smart Plug", + "online": false, + "sub": false, + "time_zone": "-07:00", + "active_time": "2024-06-16T17:19:42+00:00", + "create_time": "2024-06-16T17:19:42+00:00", + "update_time": "2024-06-16T17:19:42+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "秒", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "秒", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_dual_channel_metering.json b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json similarity index 100% rename from tests/components/tuya/fixtures/cz_dual_channel_metering.json rename to tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json diff --git a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json new file mode 100644 index 00000000000..f259ebd7d6c --- /dev/null +++ b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "53703774d8f15ba9efd3", + "name": "Buitenverlichting", + "category": "cz", + "product_id": "cuhokdii7ojyw8k2", + "product_name": "Smart Plug-EU", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-09-02T12:52:48+00:00", + "create_time": "2021-09-02T12:52:48+00:00", + "update_time": "2021-09-02T12:52:48+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json new file mode 100644 index 00000000000..a92d2d370d0 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json @@ -0,0 +1,35 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf7a2cdaf3ce28d2f7uqnh", + "name": "fakkel veranda ", + "category": "cz", + "product_id": "dntgh2ngvshfxpsz", + "product_name": "Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T07:59:28+00:00", + "create_time": "2023-01-18T07:59:28+00:00", + "update_time": "2023-01-18T07:59:28+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json new file mode 100644 index 00000000000..0638bb02d1e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json @@ -0,0 +1,100 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "051724052462ab286504", + "name": "droger", + "category": "cz", + "product_id": "hj0a5c7ckzzexu8l", + "product_name": "Smart plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-02-20T21:27:14+00:00", + "create_time": "2020-02-20T21:27:14+00:00", + "update_time": "2020-02-20T21:27:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 100, + "cur_current": 2754, + "cur_power": 5935, + "cur_voltage": 2224 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json new file mode 100644 index 00000000000..b7f913a7153 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json @@ -0,0 +1,118 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf4c0c538bfe408aa9gr2e", + "name": "wallwasher front", + "category": "cz", + "product_id": "t0a4hwsf8anfsadp", + "product_name": "Smart Plug ", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-12-11T15:51:53+00:00", + "create_time": "2022-12-11T15:51:53+00:00", + "update_time": "2022-12-11T15:51:53+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "relay_status": "power_on", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json new file mode 100644 index 00000000000..b3759178618 --- /dev/null +++ b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json @@ -0,0 +1,149 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfd9f45c6b882c9f46dxfc", + "name": "LSC Party String Light RGBIC+CCT ", + "category": "dc", + "product_id": "l3bpgg8ibsagon4x", + "product_name": "LSC Party String Light RGBIC+CCT ", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-07-18T20:38:14+00:00", + "create_time": "2024-07-18T20:38:14+00:00", + "update_time": "2024-07-18T20:38:14+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value": 1000, + "temp_value": 0, + "colour_data": { + "h": 229, + "s": 1000, + "v": 1000 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json new file mode 100644 index 00000000000..6cd0ca55379 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json @@ -0,0 +1,495 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "eb10549aadfc74b7c8q2ti", + "name": "Porch light E", + "category": "dj", + "product_id": "8szt7whdvwpmxglk", + "product_name": "Smart Light Bulb", + "online": true, + "sub": false, + "time_zone": "-06:00", + "active_time": "2024-06-19T00:38:29+00:00", + "create_time": "2024-06-19T00:38:29+00:00", + "update_time": "2024-06-19T00:38:29+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "colour_data_v2": { + "h": 245, + "s": 780, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 1000, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json new file mode 100644 index 00000000000..ec8f6a0a4d5 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json @@ -0,0 +1,338 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf71858c3d27943679dsx9", + "name": "dressoir spot", + "category": "dj", + "product_id": "8y0aquaa8v6tho8w", + "product_name": "A60 Clear", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T07:49:40+00:00", + "create_time": "2023-01-18T07:49:40+00:00", + "update_time": "2023-01-18T07:49:40+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "remote_switch": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json new file mode 100644 index 00000000000..211c0bc12cf --- /dev/null +++ b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "40611462e09806c73134", + "name": "Pokerlamp 2", + "category": "dj", + "product_id": "baf9tt9lb8t5uc7z", + "product_name": "LED SMART", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-10-30T17:22:29+00:00", + "create_time": "2021-10-30T17:22:29+00:00", + "update_time": "2021-10-30T17:22:29+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "bright_value": 45, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json new file mode 100644 index 00000000000..22650f7ae37 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf671413db4cee1f9bqdcx", + "name": "WC D1", + "category": "dj", + "product_id": "d4g0fbsoaal841o6", + "product_name": "A60 GOLD", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-30T11:36:31+00:00", + "create_time": "2021-06-30T11:36:31+00:00", + "update_time": "2021-06-30T11:36:31+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json new file mode 100644 index 00000000000..67df13c674f --- /dev/null +++ b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json @@ -0,0 +1,484 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8885f3d18a73e395bfac", + "name": "Fakkel 8", + "category": "dj", + "product_id": "djnozmdyqyriow8z", + "product_name": "Candle RGB-CCT", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-30T12:13:49+00:00", + "create_time": "2021-06-30T12:13:49+00:00", + "update_time": "2021-06-30T12:13:49+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 280, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 56, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json new file mode 100644 index 00000000000..90cad22fd09 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json @@ -0,0 +1,559 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfb99bba00c9c90ba8gzgl", + "name": "ab6", + "category": "dj", + "product_id": "ekwolitfjhxn55js", + "product_name": "LSC Smart Connect GU10 RGB+CCT", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-10-30T21:26:33+00:00", + "create_time": "2024-10-30T21:26:33+00:00", + "update_time": "2024-10-30T21:26:33+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 3, + "s": 994, + "v": 443 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6AAA", + "do_not_disturb": false, + "remote_switch": true, + "cycle_timing": "AAAA", + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json new file mode 100644 index 00000000000..5b189b6a3e4 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json @@ -0,0 +1,338 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0914a82b06ecf151xsf5", + "name": "Slaapkamer", + "category": "dj", + "product_id": "fuupmcr2mb1odkja", + "product_name": "ST64 Clear", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-28T01:25:04+00:00", + "create_time": "2023-01-28T01:25:04+00:00", + "update_time": "2023-01-28T01:25:04+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json new file mode 100644 index 00000000000..e8166a192dc --- /dev/null +++ b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json @@ -0,0 +1,510 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "00450321483fda81c529", + "name": "Master bedroom TV lights", + "category": "dj", + "product_id": "hp6orhaqm6as3jnv", + "product_name": "LED Strip Lights", + "online": true, + "sub": false, + "time_zone": "-07:00", + "active_time": "2024-06-19T03:35:54+00:00", + "create_time": "2024-06-19T03:35:54+00:00", + "update_time": "2024-06-19T03:35:54+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value": 96, + "temp_value": 223, + "colour_data": { + "h": 27.0, + "s": 255.0, + "v": 52.0 + }, + "scene_data": { + "h": 16.0, + "s": 255.0, + "v": 210.9 + }, + "flash_scene_1": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_2": { + "bright": 255, + "frequency": 128, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + }, + "flash_scene_3": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_4": { + "bright": 255, + "frequency": 5, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 60.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 300.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json new file mode 100644 index 00000000000..893aafa3759 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json @@ -0,0 +1,156 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "63362034840d8eb9029f", + "name": "Garage", + "category": "dj", + "product_id": "hpc8ddyfv85haxa7", + "product_name": "RGB Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-12-21T14:43:57+00:00", + "create_time": "2020-12-21T14:43:57+00:00", + "update_time": "2020-12-21T14:43:57+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value": 255, + "temp_value": 255, + "colour_data_v2": { + "h": 16384, + "s": 65280, + "v": 65535 + }, + "switch_1": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json new file mode 100644 index 00000000000..f9062d9146d --- /dev/null +++ b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json @@ -0,0 +1,529 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0fc1d7d4caa71a59us7c", + "name": "LED Porch 2", + "category": "dj", + "product_id": "iayz2jmtlipjnxj7", + "product_name": "LED Strip RGB+W", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-07T10:55:19+00:00", + "create_time": "2021-06-07T10:55:19+00:00", + "update_time": "2021-06-07T10:55:19+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 839, + "colour_data_v2": { + "h": 13, + "s": 992, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json new file mode 100644 index 00000000000..295157d8370 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json @@ -0,0 +1,523 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf599f5cffe1a5985depyk", + "name": "AB1", + "category": "dj", + "product_id": "idnfq7xbx8qewyoa", + "product_name": "Smart Lamp", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-08-16T12:51:52+00:00", + "create_time": "2021-08-16T12:51:52+00:00", + "update_time": "2021-08-16T12:51:52+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "scene", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 6, + "s": 978, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 6, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "jump", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json new file mode 100644 index 00000000000..1181b650f3e --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "84178216d8f15be52dc4", + "name": "Ieskas", + "category": "dj", + "product_id": "ilddqqih3tucdk68", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:07:13+00:00", + "create_time": "2025-05-28T20:07:13+00:00", + "update_time": "2025-05-28T20:07:13+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "bright_value": 255, + "temp_value": 158 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json new file mode 100644 index 00000000000..d95179c921f --- /dev/null +++ b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json @@ -0,0 +1,434 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfe49d7b6cd80536efdldi", + "name": "Ceiling Portal", + "category": "dj", + "product_id": "j1bgp31cffutizub", + "product_name": "LSC Smart Ceiling Light", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-31T12:27:35+00:00", + "create_time": "2022-01-31T12:27:35+00:00", + "update_time": "2022-01-31T12:27:35+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 950, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json new file mode 100644 index 00000000000..93a802a7ee3 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "07608286600194e94248", + "name": "DirectietKamer", + "category": "dj", + "product_id": "lmnt3uyltk1xffrt", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:00:48+00:00", + "create_time": "2025-05-28T20:00:48+00:00", + "update_time": "2025-05-28T20:00:48+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 255, + "temp_value": 255 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_smart_light_bulb.json b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json similarity index 100% rename from tests/components/tuya/fixtures/dj_smart_light_bulb.json rename to tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json diff --git a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json new file mode 100644 index 00000000000..bc919dd92d2 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json @@ -0,0 +1,559 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf77c04cbd6a52a7be16ll", + "name": "b2", + "category": "dj", + "product_id": "nbumqpv8vz61enji", + "product_name": "LSC smart GU10", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-10-30T21:35:27+00:00", + "create_time": "2024-10-30T21:35:27+00:00", + "update_time": "2024-10-30T21:35:27+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 10, + "temp_value_v2": 150, + "colour_data_v2": { + "h": 119, + "s": 935, + "v": 132 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6ACW", + "do_not_disturb": true, + "remote_switch": true, + "cycle_timing": "AAAA", + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json new file mode 100644 index 00000000000..c519f1aa593 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "40350105dc4f229a464e", + "name": "hall 💡 ", + "category": "dj", + "product_id": "nlxvjzy1hoeiqsg6", + "product_name": "LED SMART", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-06-23T21:37:40+00:00", + "create_time": "2020-06-23T21:37:40+00:00", + "update_time": "2020-06-23T21:37:40+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 135, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_oe0cpnjg.json b/tests/components/tuya/fixtures/dj_oe0cpnjg.json new file mode 100644 index 00000000000..646ce8a93d7 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_oe0cpnjg.json @@ -0,0 +1,226 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8d8af3ddfe75b0195r0h", + "name": "Front right Lighting trap", + "category": "dj", + "product_id": "oe0cpnjg", + "product_name": "Smart Lighting", + "online": true, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-10-03T13:23:20+00:00", + "create_time": "2023-10-03T13:23:20+00:00", + "update_time": "2023-10-03T13:23:20+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 985, + "colour_data_v2": "", + "music_data": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_riwp3k79.json b/tests/components/tuya/fixtures/dj_riwp3k79.json new file mode 100644 index 00000000000..f1a3579e660 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_riwp3k79.json @@ -0,0 +1,402 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf46b2b81ca41ce0c1xpsw", + "name": "LED KEUKEN 2", + "category": "dj", + "product_id": "riwp3k79", + "product_name": "atmosphere", + "online": true, + "sub": true, + "time_zone": "+08:00", + "active_time": "2020-12-29T16:16:11+00:00", + "create_time": "2020-12-29T16:16:11+00:00", + "update_time": "2020-12-29T16:16:11+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 27, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 6, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 100, + "unit_switch_duration": 100, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 100, + "unit_switch_duration": 100, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json new file mode 100644 index 00000000000..20c91ad7739 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf252b8ee16b2e78bdoxlp", + "name": "Pokerlamp 1", + "category": "dj", + "product_id": "tmsloaroqavbucgn", + "product_name": "G95-Filament", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-06-29T16:12:54+00:00", + "create_time": "2021-06-29T16:12:54+00:00", + "update_time": "2021-06-29T16:12:54+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 400, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json new file mode 100644 index 00000000000..7ea5905411d --- /dev/null +++ b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json @@ -0,0 +1,335 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf8edbd51a52c01a4bfgqf", + "name": "Sjiethoes", + "category": "dj", + "product_id": "ufq2xwuzd4nb0qdr", + "product_name": "Smart Ceiling Lamp", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-29T09:40:53+00:00", + "create_time": "2025-04-29T09:40:53+00:00", + "update_time": "2025-04-29T09:40:53+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 46, + "s": 1000, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json new file mode 100644 index 00000000000..4d6749ea0b4 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json @@ -0,0 +1,532 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfd56f4718874ee8830xdw", + "name": "Strip 2", + "category": "dj", + "product_id": "vqwcnabamzrc2kab", + "product_name": "Light Strip-RGBCW ", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-10-22T13:55:55+00:00", + "create_time": "2021-10-22T13:55:55+00:00", + "update_time": "2021-10-22T13:55:55+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 218, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 8, + "scene_units": [ + { + "bright": 0, + "h": 0, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 120, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 240, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 61, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 174, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + }, + { + "bright": 0, + "h": 275, + "s": 1000, + "temperature": 0, + "unit_change_mode": "gradient", + "unit_gradient_duration": 70, + "unit_switch_duration": 70, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json new file mode 100644 index 00000000000..cce66d90b0c --- /dev/null +++ b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json @@ -0,0 +1,377 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfc1ef4da4accc0731oggw", + "name": "ERKER 1-Gold ", + "category": "dj", + "product_id": "xokdfs6kh5ednakk", + "product_name": "LSC-G125-Gold ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-30T22:02:31+00:00", + "create_time": "2022-01-30T22:02:31+00:00", + "update_time": "2022-01-30T22:02:31+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "remote_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "remote_switch": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json new file mode 100644 index 00000000000..d1c23663144 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "03010850c44f33966362", + "name": "Stoel", + "category": "dj", + "product_id": "zakhnlpdiu0ycdxn", + "product_name": "LED SMART", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-08-10T18:55:12+00:00", + "create_time": "2023-08-10T18:55:12+00:00", + "update_time": "2023-08-10T18:55:12+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 71, + "temp_value": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json new file mode 100644 index 00000000000..624f7fb4347 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json @@ -0,0 +1,322 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "500425642462ab50909b", + "name": "Gengske 💡 ", + "category": "dj", + "product_id": "zav1pa32pyxray78", + "product_name": "Ceiling Light RGBTW", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-05-28T20:00:48+00:00", + "create_time": "2025-05-28T20:00:48+00:00", + "update_time": "2025-05-28T20:00:48+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "temp_value_v2": 380, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 102 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json new file mode 100644 index 00000000000..cede2b65682 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json @@ -0,0 +1,413 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf74164049de868395pbci", + "name": "Floodlight", + "category": "dj", + "product_id": "zputiamzanuk6yky", + "product_name": "LSC Floodlight", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-06-09T08:14:06+00:00", + "create_time": "2025-06-09T08:14:06+00:00", + "update_time": "2025-06-09T08:14:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value_v2": 1000, + "colour_data_v2": { + "h": 295, + "s": 920, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 1000, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAGwG/A+gD6APo", + "do_not_disturb": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json b/tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_earu_electric_eawcpt.json rename to tests/components/tuya/fixtures/dlq_0tnvg2xaisqdadcf.json diff --git a/tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json similarity index 100% rename from tests/components/tuya/fixtures/dlq_metering_3pn_wifi.json rename to tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json diff --git a/tests/components/tuya/fixtures/gyd_night_light.json b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json similarity index 100% rename from tests/components/tuya/fixtures/gyd_night_light.json rename to tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json diff --git a/tests/components/tuya/fixtures/hps_2aaelwxk.json b/tests/components/tuya/fixtures/hps_2aaelwxk.json new file mode 100644 index 00000000000..4e5066e77f4 --- /dev/null +++ b/tests/components/tuya/fixtures/hps_2aaelwxk.json @@ -0,0 +1,186 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf78687ad321a3aeb8a73m", + "name": "Human presence Office", + "category": "hps", + "product_id": "2aaelwxk", + "product_name": "Human presence sensor", + "online": true, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-12-31T14:40:17+00:00", + "create_time": "2023-12-31T14:40:17+00:00", + "update_time": "2023-12-31T14:40:17+00:00", + "function": { + "sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "near_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "presence_time": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 28800, + "scale": 0, + "step": 1 + } + }, + "motionless_far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 600, + "scale": 0, + "step": 1 + } + }, + "motionless_sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "indicator": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "presence_state": { + "type": "Enum", + "value": { + "range": ["none", "presence"] + } + }, + "sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "near_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "human_motion_state": { + "type": "Enum", + "value": { + "range": ["none", "large_move", "small_move"] + } + }, + "presence_time": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 28800, + "scale": 0, + "step": 1 + } + }, + "motionless_far_detection": { + "type": "Integer", + "value": { + "unit": "cm", + "min": 0, + "max": 600, + "scale": 0, + "step": 1 + } + }, + "motionless_sensitivity": { + "type": "Integer", + "value": { + "unit": "x", + "min": 0, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "illuminance_value": { + "type": "Integer", + "value": { + "unit": "lux", + "min": 0, + "max": 6000, + "scale": 0, + "step": 1 + } + }, + "indicator": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "presence_state": "none", + "sensitivity": 3, + "near_detection": 40, + "far_detection": 220, + "human_motion_state": "none", + "presence_time": 30, + "motionless_far_detection": 30, + "motionless_sensitivity": 7, + "illuminance_value": 0, + "indicator": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kg_smart_valve.json b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json similarity index 100% rename from tests/components/tuya/fixtures/kg_smart_valve.json rename to tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json diff --git a/tests/components/tuya/fixtures/kj_bladeless_tower_fan.json b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json similarity index 100% rename from tests/components/tuya/fixtures/kj_bladeless_tower_fan.json rename to tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json diff --git a/tests/components/tuya/fixtures/ks_tower_fan.json b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json similarity index 100% rename from tests/components/tuya/fixtures/ks_tower_fan.json rename to tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json diff --git a/tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json similarity index 100% rename from tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json rename to tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json diff --git a/tests/components/tuya/fixtures/mal_alarm_host.json b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json similarity index 100% rename from tests/components/tuya/fixtures/mal_alarm_host.json rename to tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json diff --git a/tests/components/tuya/fixtures/mcs_door_sensor.json b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json similarity index 100% rename from tests/components/tuya/fixtures/mcs_door_sensor.json rename to tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json diff --git a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json new file mode 100644 index 00000000000..4ed7ecf0373 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json @@ -0,0 +1,44 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf2206da15147500969d6e", + "name": "Bubbelbad", + "category": "pc", + "product_id": "t2afic7i3v1bwhfp", + "product_name": "Garden Spike(EU)", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-02-02T15:10:03+00:00", + "create_time": "2022-02-02T15:10:03+00:00", + "update_time": "2022-02-02T15:10:03+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true, + "switch_2": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json new file mode 100644 index 00000000000..99929616ec7 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json @@ -0,0 +1,86 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "15727703c4dd5709cd78", + "name": "Terras", + "category": "pc", + "product_id": "trjopo1vdlt9q1tg", + "product_name": "Garden Spike(FR)", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-18T08:42:44+00:00", + "create_time": "2023-01-18T08:42:44+00:00", + "update_time": "2023-01-18T08:42:44+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "switch_2": false, + "countdown_1": 0, + "countdown_2": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json new file mode 100644 index 00000000000..98843da5614 --- /dev/null +++ b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json @@ -0,0 +1,44 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "73486068483fda10d633", + "name": "rat trap hedge", + "category": "pir", + "product_id": "3amxzozho9xp4mkh", + "product_name": "Smart Motion Sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-11-17T11:36:06+00:00", + "create_time": "2021-11-17T11:36:06+00:00", + "update_time": "2021-11-17T11:36:06+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "pir": "pir", + "battery_state": "low", + "temper_alarm": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_fcdjzz3s.json b/tests/components/tuya/fixtures/pir_fcdjzz3s.json new file mode 100644 index 00000000000..65740a4106c --- /dev/null +++ b/tests/components/tuya/fixtures/pir_fcdjzz3s.json @@ -0,0 +1,48 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf445324326cbde7c5rg7b", + "name": "Motion sensor lidl zigbee", + "category": "pir", + "product_id": "fcdjzz3s", + "product_name": "Motion sensor", + "online": false, + "sub": true, + "time_zone": "+01:00", + "active_time": "2022-09-09T07:24:07+00:00", + "create_time": "2022-09-09T07:24:07+00:00", + "update_time": "2022-09-09T07:24:07+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "pir": "nopir", + "battery_percentage": 85, + "temper_alarm": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json new file mode 100644 index 00000000000..e4122ee5f9d --- /dev/null +++ b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json @@ -0,0 +1,39 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "20401777500291cfe3a2", + "name": "PIR outside stairs", + "category": "pir", + "product_id": "wqz93nrdomectyoz", + "product_name": "Smart PIR sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-26T09:56:39+00:00", + "create_time": "2023-01-26T09:56:39+00:00", + "update_time": "2023-01-26T09:56:39+00:00", + "function": {}, + "status_range": { + "pir": { + "type": "Enum", + "value": { + "range": ["pir"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "pir": "pir", + "battery_state": "middle" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/qccdz_ac_charging_control.json b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json similarity index 100% rename from tests/components/tuya/fixtures/qccdz_ac_charging_control.json rename to tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json diff --git a/tests/components/tuya/fixtures/qxj_weather_station.json b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_weather_station.json rename to tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json diff --git a/tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json similarity index 100% rename from tests/components/tuya/fixtures/qxj_temp_humidity_external_probe.json rename to tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json diff --git a/tests/components/tuya/fixtures/rqbj_gas_sensor.json b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json similarity index 100% rename from tests/components/tuya/fixtures/rqbj_gas_sensor.json rename to tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json diff --git a/tests/components/tuya/fixtures/sfkzq_valve_controller.json b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json similarity index 100% rename from tests/components/tuya/fixtures/sfkzq_valve_controller.json rename to tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json diff --git a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json new file mode 100644 index 00000000000..a3068983c87 --- /dev/null +++ b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json @@ -0,0 +1,69 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf0984adfeffe10d5a3ofd", + "name": "Siren veranda ", + "category": "sgbj", + "product_id": "ulv4nnue7gqp0rjk", + "product_name": "Siren Sensor", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2020-05-05T20:15:05+00:00", + "create_time": "2020-05-05T20:15:05+00:00", + "update_time": "2020-05-05T20:15:05+00:00", + "function": { + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 30, + "scale": 0, + "step": 1 + } + }, + "alarm_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 30, + "scale": 0, + "step": 1 + } + }, + "alarm_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "alarm_volume": "middle", + "alarm_time": 10, + "alarm_switch": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json new file mode 100644 index 00000000000..a6543eac5ea --- /dev/null +++ b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json @@ -0,0 +1,182 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf7b8e59f8cd49f425mmfm", + "name": "CAM GARAGE", + "category": "sp", + "product_id": "drezasavompxpcgm", + "product_name": "Indoor camera ", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2021-07-26T13:26:21+00:00", + "create_time": "2021-07-26T13:26:21+00:00", + "update_time": "2021-07-26T13:26:21+00:00", + "function": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status_range": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 5, + "scale": 1, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "unit": "", + "min": -20000, + "max": 20000, + "scale": 1, + "step": 1 + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "decibel_upload": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status": { + "basic_indicator": true, + "basic_flip": false, + "basic_osd": false, + "motion_sensitivity": 0, + "basic_nightvision": 0, + "sd_storge": "0|0|0", + "sd_status": 5, + "sd_format": false, + "movement_detect_pic": "**REDACTED**", + "sd_format_state": -20000, + "motion_switch": true, + "decibel_switch": false, + "decibel_sensitivity": 0, + "decibel_upload": 1696802404, + "record_switch": false, + "record_mode": 1 + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json new file mode 100644 index 00000000000..9a7bb9f1eca --- /dev/null +++ b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json @@ -0,0 +1,213 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf9d5b7ea61ea4c9a6rom9", + "name": "CAM PORCH", + "category": "sp", + "product_id": "rjKXWRohlvOTyLBu", + "product_name": "Indoor cam Pan/Tilt ", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2020-07-04T07:41:28+00:00", + "create_time": "2020-07-04T07:41:28+00:00", + "update_time": "2020-07-04T07:41:28+00:00", + "function": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_timer_setting": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "motion_timer_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status_range": { + "basic_indicator": { + "type": "Boolean", + "value": {} + }, + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 5, + "scale": 1, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "motion_timer_setting": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "unit": "", + "min": -20000, + "max": 20000, + "scale": 1, + "step": 1 + } + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "motion_timer_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "decibel_upload": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status": { + "basic_indicator": false, + "basic_flip": false, + "basic_osd": true, + "motion_sensitivity": 2, + "sd_storge": "100|0|100", + "sd_status": 5, + "sd_format": true, + "motion_timer_setting": "00:00|06:00", + "movement_detect_pic": "**REDACTED**", + "ptz_stop": true, + "sd_format_state": 100, + "ptz_control": 6, + "motion_switch": false, + "motion_timer_switch": true, + "decibel_switch": false, + "decibel_sensitivity": 1, + "decibel_upload": 1750049151, + "record_switch": false, + "record_mode": 1 + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/tdq_4_443.json b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json similarity index 100% rename from tests/components/tuya/fixtures/tdq_4_443.json rename to tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json diff --git a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json new file mode 100644 index 00000000000..973cecabc0b --- /dev/null +++ b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json @@ -0,0 +1,145 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "1753247726209KOaaPc", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bfdb773e4ae317e3915h2i", + "name": "Solar zijpad", + "category": "tyndj", + "product_id": "pyakuuoc", + "product_name": "Solar flood light App panel", + "online": false, + "sub": true, + "time_zone": "+08:00", + "active_time": "2023-03-08T13:24:06+00:00", + "create_time": "2023-03-08T13:24:06+00:00", + "update_time": "2023-03-08T13:24:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "countdown": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "switch_save_energy": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "device_mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value": 10, + "scene_data": "", + "countdown": 0, + "switch_save_energy": false, + "battery_percentage": 0, + "device_mode": "manual", + "battery_state": "low" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json new file mode 100644 index 00000000000..2fa798b2f60 --- /dev/null +++ b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json @@ -0,0 +1,23 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf63312cdd4722555bmsuv", + "name": "ZigBee Gateway", + "category": "wfcon", + "product_id": "b25mh8sxawsgndck", + "product_name": "ZigBee Gateway", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-12-29T15:54:11+00:00", + "create_time": "2020-12-29T15:54:11+00:00", + "update_time": "2020-12-29T15:54:11+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_air_conditioner.json b/tests/components/tuya/fixtures/wk_aqoouq7x.json similarity index 100% rename from tests/components/tuya/fixtures/wk_air_conditioner.json rename to tests/components/tuya/fixtures/wk_aqoouq7x.json diff --git a/tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json similarity index 100% rename from tests/components/tuya/fixtures/wk_wifi_smart_gas_boiler_thermostat.json rename to tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json diff --git a/tests/components/tuya/fixtures/wsdcg_temperature_humidity.json b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json similarity index 100% rename from tests/components/tuya/fixtures/wsdcg_temperature_humidity.json rename to tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json diff --git a/tests/components/tuya/fixtures/wxkg_wireless_switch.json b/tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json similarity index 100% rename from tests/components/tuya/fixtures/wxkg_wireless_switch.json rename to tests/components/tuya/fixtures/wxkg_l8yaz4um5b3pwyvf.json diff --git a/tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json similarity index 100% rename from tests/components/tuya/fixtures/ydkt_dolceclima_unsupported.json rename to tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json diff --git a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json new file mode 100644 index 00000000000..f80b0cd5cd1 --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json @@ -0,0 +1,65 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "8670375210521cf1349c", + "name": " Smoke detector upstairs ", + "category": "ywbj", + "product_id": "gf9dejhmzffgdyfj", + "product_name": "Smart Smoke Alarm", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2021-11-09T13:21:37+00:00", + "create_time": "2021-11-09T13:21:37+00:00", + "update_time": "2021-11-09T13:21:37+00:00", + "function": { + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "lifecycle": { + "type": "Boolean", + "value": {} + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "smoke_sensor_status": "normal", + "lifecycle": true, + "battery_state": "low", + "battery_percentage": 16, + "muffling": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json new file mode 100644 index 00000000000..f724ffe164f --- /dev/null +++ b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json @@ -0,0 +1,139 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "REDACTED", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "bf27a4********368f4w", + "name": "Nivel del tanque A", + "category": "ywcgq", + "product_id": "wtzwyhkev3b4ubns", + "product_name": "Tank A Level", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-01-05T10:22:24+00:00", + "create_time": "2024-01-05T10:22:24+00:00", + "update_time": "2024-01-05T10:22:24+00:00", + "function": { + "max_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mini_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "installation_height": { + "type": "Integer", + "value": { + "unit": "m", + "min": 200, + "max": 2500, + "scale": 3, + "step": 1 + } + }, + "liquid_depth_max": { + "type": "Integer", + "value": { + "unit": "m", + "min": 100, + "max": 2400, + "scale": 3, + "step": 1 + } + } + }, + "status_range": { + "liquid_state": { + "type": "Enum", + "value": { + "range": ["normal", "lower_alarm", "upper_alarm"] + } + }, + "liquid_depth": { + "type": "Integer", + "value": { + "unit": "m", + "min": 0, + "max": 10000, + "scale": 2, + "step": 1 + } + }, + "max_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mini_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "installation_height": { + "type": "Integer", + "value": { + "unit": "m", + "min": 200, + "max": 2500, + "scale": 3, + "step": 1 + } + }, + "liquid_depth_max": { + "type": "Integer", + "value": { + "unit": "m", + "min": 100, + "max": 2400, + "scale": 3, + "step": 1 + } + }, + "liquid_level_percent": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "liquid_state": "normal", + "liquid_depth": 77, + "max_set": 100, + "mini_set": 10, + "installation_height": 980, + "liquid_depth_max": 140, + "liquid_level_percent": 97 + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/zndb_smart_meter.json b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json similarity index 100% rename from tests/components/tuya/fixtures/zndb_smart_meter.json rename to tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json diff --git a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr index 97076d5e467..73072dcb516 100644 --- a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][alarm_control_panel.multifunction_alarm-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 267f61aabd0..7cb613ebbf2 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][binary_sensor.aqi_safety-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'safety', @@ -48,154 +48,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_defrost', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Defrost', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'defrost', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Defrost', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_defrost', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_tank_full', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Tank full', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'tankfull', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Tank full', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_tank_full', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.dehumidifier_wet', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wet', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wet', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Dehumidifier Wet', - }), - 'context': , - 'entity_id': 'binary_sensor.dehumidifier_wet', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -230,7 +83,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_defrost-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -244,7 +97,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +132,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][binary_sensor.dehumidifer_tank_full-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -293,7 +146,203 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dehumidifier_defrost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Defrost', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'defrost', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Defrost', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_defrost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dehumidifier_tank_full', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tank full', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tankfull', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Tank full', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_tank_full', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dehumidifier_wet', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wet', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wet', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Dehumidifier Wet', + }), + 'context': , + 'entity_id': 'binary_sensor.dehumidifier_wet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Occupancy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mpresence_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'occupancy', + 'friendly_name': 'Human presence Office Occupancy', + }), + 'context': , + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -328,7 +377,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', @@ -342,7 +391,252 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.73486068483fda10d633pir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'rat trap hedge Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.73486068483fda10d633temper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'rat trap hedge Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf445324326cbde7c5rg7bpir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'Motion sensor lidl zigbee Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf445324326cbde7c5rg7btemper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Motion sensor lidl zigbee Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.20401777500291cfe3a2pir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'PIR outside stairs Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -377,7 +671,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][binary_sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'gas', @@ -391,3 +685,52 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector_upstairs_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.8670375210521cf1349csmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': ' Smoke detector upstairs Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector_upstairs_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 6e93a1b263c..cb535cc5c07 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-entry] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -46,7 +46,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kt_serenelife_slpac905wuk_air_conditioner][climate.air_conditioner-state] +# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 22.0, @@ -74,7 +74,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-entry] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -124,7 +124,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][climate.clima_cucina-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 27.0, @@ -155,7 +155,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -198,7 +198,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][climate.wifi_smart_gas_boiler_thermostat-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 24.9, diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 6ae4781c7c1..aa592b25520 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][cover.kitchen_blinds_curtain-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 48, @@ -50,7 +50,7 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][cover.tapparelle_studio_curtain-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 0, diff --git a/tests/components/tuya/snapshots/test_diagnostics.ambr b/tests/components/tuya/snapshots/test_diagnostics.ambr index 5fc3796d109..93cc0cd0b6d 100644 --- a/tests/components/tuya/snapshots/test_diagnostics.ambr +++ b/tests/components/tuya/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_device_diagnostics[rqbj_gas_sensor] +# name: test_device_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'active_time': '2025-06-24T20:33:10+00:00', 'category': 'rqbj', @@ -88,7 +88,7 @@ 'update_time': '2025-06-24T20:33:10+00:00', }) # --- -# name: test_entry_diagnostics[rqbj_gas_sensor] +# name: test_entry_diagnostics[rqbj_4iqe2hsfyd86kwwc] dict({ 'devices': list([ dict({ diff --git a/tests/components/tuya/snapshots/test_event.ambr b/tests/components/tuya/snapshots/test_event.ambr index 085ebd3ec8b..ea19ff486da 100644 --- a/tests/components/tuya/snapshots/test_event.ambr +++ b/tests/components/tuya/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_1-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -58,7 +58,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][event.bathroom_smart_switch_button_2-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index ff795c150c9..69eb1b467e9 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -1,55 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][fan.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier', - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -87,7 +37,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][fan.dehumidifer-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer', @@ -103,7 +53,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -139,7 +89,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][fan.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifier ', @@ -153,7 +103,57 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -192,7 +192,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][fan.bree-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree', @@ -210,7 +210,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -251,7 +251,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart', diff --git a/tests/components/tuya/snapshots/test_humidifier.ambr b/tests/components/tuya/snapshots/test_humidifier.ambr index 3389f927eb4..25bb1799dc8 100644 --- a/tests/components/tuya/snapshots/test_humidifier.ambr +++ b/tests/components/tuya/snapshots/test_humidifier.ambr @@ -1,5 +1,115 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_humidity': 80, + 'min_humidity': 25, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.dehumidifer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.mock_device_idswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'dehumidifier', + 'friendly_name': 'Dehumidifer', + 'max_humidity': 80, + 'min_humidity': 25, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.dehumidifer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_humidity': 100, + 'min_humidity': 0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.mock_device_idswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'dehumidifier', + 'friendly_name': 'Dehumidifier ', + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -37,7 +147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][humidifier.dehumidifier-state] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 47, @@ -56,113 +166,3 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 80, - 'min_humidity': 25, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dehumidifer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][humidifier.dehumidifer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifer', - 'max_humidity': 80, - 'min_humidity': 25, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dehumidifer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 100, - 'min_humidity': 0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_smart_dry_plus][humidifier.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifier ', - 'max_humidity': 100, - 'min_humidity': 0, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 084e9a84401..83548abf0c3 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_unsupported_device[ydkt_dolceclima_unsupported] +# name: test_unsupported_device[ydkt_jevroj5aguwdbs2e] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -18,7 +18,6 @@ 'mock_device_id', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Tuya', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 5fcf58dda6d..b5dca58f8e7 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-entry] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -38,7 +38,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_curtain_switch][light.tapparelle_studio_backlight-state] +# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , @@ -56,7 +56,1142 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-entry] +# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfd9f45c6b882c9f46dxfcswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LSC Party String Light RGBIC+CCT ', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.porch_light_e', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.eb10549aadfc74b7c8q2tiswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Porch light E', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.porch_light_e', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dressoir_spot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf71858c3d27943679dsx9switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'dressoir spot', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.dressoir_spot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.pokerlamp_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.40611462e09806c73134switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pokerlamp 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.pokerlamp_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.wc_d1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf671413db4cee1f9bqdcxswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'WC D1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.wc_d1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.fakkel_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8885f3d18a73e395bfacswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 70, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'Fakkel 8', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.fakkel_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ab6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfb99bba00c9c90ba8gzglswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ab6', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.ab6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.slaapkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf0914a82b06ecf151xsf5switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Slaapkamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.slaapkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.master_bedroom_tv_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.00450321483fda81c529switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 51, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Master bedroom TV lights', + 'hs_color': tuple( + 26.072, + 100.0, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 111, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.632, + 0.358, + ), + }), + 'context': , + 'entity_id': 'light.master_bedroom_tv_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.63362034840d8eb9029fswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Garage', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.garage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage_light_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_light', + 'unique_id': 'tuya.63362034840d8eb9029fswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Garage Light 1', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.garage_light_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.led_porch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf0fc1d7d4caa71a59us7cswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LED Porch 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.led_porch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ab1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf599f5cffe1a5985depykswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'AB1', + 'hs_color': tuple( + 6.0, + 97.8, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 31, + 6, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.693, + 0.304, + ), + }), + 'context': , + 'entity_id': 'light.ab1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ieskas', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.84178216d8f15be52dc4switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 285, + 'color_temp_kelvin': 3508, + 'friendly_name': 'Ieskas', + 'hs_color': tuple( + 27.165, + 44.6, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 193, + 141, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.453, + 0.374, + ), + }), + 'context': , + 'entity_id': 'light.ieskas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_portal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfe49d7b6cd80536efdldiswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Ceiling Portal', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.ceiling_portal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.directietkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.07608286600194e94248switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'DirectietKamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.directietkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -96,7 +1231,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_smart_light_bulb][light.garage_light-state] +# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 138, @@ -119,7 +1254,775 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-entry] +# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.b2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf77c04cbd6a52a7be16llswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'b2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.b2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hall', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.40350105dc4f229a464eswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'hall 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.hall', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.front_right_lighting_trap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8d8af3ddfe75b0195r0hswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Front right Lighting trap', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.front_right_lighting_trap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.led_keuken_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf46b2b81ca41ce0c1xpswswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'LED KEUKEN 2', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.led_keuken_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.pokerlamp_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf252b8ee16b2e78bdoxlpswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pokerlamp 1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.pokerlamp_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.sjiethoes', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf8edbd51a52c01a4bfgqfswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sjiethoes', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.sjiethoes', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.strip_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfd56f4718874ee8830xdwswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Strip 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.strip_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.erker_1_gold', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfc1ef4da4accc0731oggwswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'ERKER 1-Gold ', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.erker_1_gold', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.stoel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.03010850c44f33966362switch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Stoel', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.stoel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.gengske', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.500425642462ab50909bswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Gengske 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.gengske', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.floodlight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf74164049de868395pbciswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Floodlight', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.floodlight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -163,7 +2066,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[gyd_night_light][light.colorful_pir_night_light-state] +# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, @@ -192,7 +2095,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -231,7 +2134,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , @@ -249,3 +2152,173 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_garage_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'CAM GARAGE Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_garage_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_porch_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'CAM PORCH Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_porch_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.solar_zijpad', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.solar_zijpad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 1c8af00baff..9a04b9dd78c 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][number.aqi_alarm_duration-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -58,7 +58,7 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][number.cleverio_pf100_feed-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Feed', @@ -116,7 +116,183 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-entry] +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_far_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Far detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'far_detection', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mfar_detection', + 'unit_of_measurement': 'cm', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Human presence Office Far detection', + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'cm', + }), + 'context': , + 'entity_id': 'number.human_presence_office_far_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '220.0', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_near_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Near detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'near_detection', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73mnear_detection', + 'unit_of_measurement': 'cm', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Human presence Office Near detection', + 'max': 1000.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'cm', + }), + 'context': , + 'entity_id': 'number.human_presence_office_near_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.human_presence_office_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sensitivity', + 'unique_id': 'tuya.bf78687ad321a3aeb8a73msensitivity', + 'unit_of_measurement': 'x', + }) +# --- +# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Human presence Office Sensitivity', + 'max': 10.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'x', + }), + 'context': , + 'entity_id': 'number.human_presence_office_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -156,7 +332,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_alarm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -175,7 +351,7 @@ 'state': '20.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -215,7 +391,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_arm_delay-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -234,7 +410,7 @@ 'state': '15.0', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -274,7 +450,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][number.multifunction_alarm_siren_duration-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -293,7 +469,65 @@ 'state': '3.0', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 30.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.siren_veranda_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time', + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_time', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda Time', + 'max': 30.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.siren_veranda_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -333,7 +567,7 @@ 'unit_of_measurement': '℃', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Temperature correction', diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 0f530184122..943e230b7cd 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-entry] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_am43_corded_motor_zigbee_cover][select.kitchen_blinds_motor_mode-state] +# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Kitchen Blinds Motor mode', @@ -56,7 +56,7 @@ 'state': 'forward', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,7 +98,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][select.aqi_volume-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Volume', @@ -117,68 +117,7 @@ 'state': 'low', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.dehumidifier_countdown', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Countdown', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'countdown', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][select.dehumidifier_countdown-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Countdown', - 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - ]), - }), - 'context': , - 'entity_id': 'select.dehumidifier_countdown', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'cancel', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -220,7 +159,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][select.dehumidifer_countdown-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Countdown', @@ -239,7 +178,68 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.dehumidifier_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + ]), + }), + 'context': , + 'entity_id': 'select.dehumidifier_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +279,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][select.smart_odor_eliminator_pro_odor_elimination_mode-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Odor elimination mode', @@ -296,7 +296,125 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-entry] +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2elight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2erelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -340,7 +458,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][select.bree_countdown-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Countdown', @@ -361,7 +479,473 @@ 'state': 'cancel', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-entry] +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.siren_veranda_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda Volume', + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'context': , + 'entity_id': 'select.siren_veranda_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_night_vision', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Night vision', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'basic_nightvision', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_nightvision', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Night vision', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_night_vision', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -402,7 +986,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][select.4_433_power_on_behavior-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '4-433 Power on behavior', diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 57e73eccda5..e8b9900185e 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -36,7 +36,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_battery-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -52,7 +52,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -89,7 +89,7 @@ 'unit_of_measurement': 'mg/m3', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_formaldehyde-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Formaldehyde', @@ -104,7 +104,7 @@ 'state': '0.002', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -141,7 +141,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_humidity-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -157,7 +157,7 @@ 'state': '53.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -197,7 +197,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_temperature-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -213,7 +213,7 @@ 'state': '26.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -250,7 +250,7 @@ 'unit_of_measurement': 'mg/m³', }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][sensor.aqi_volatile_organic_compounds-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds', @@ -266,60 +266,7 @@ 'state': '0.018', }) # --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.dehumidifier_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][sensor.dehumidifier_humidity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Dehumidifier Humidity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.dehumidifier_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '47.0', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -356,7 +303,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][sensor.dehumidifer_humidity-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -372,7 +319,60 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dehumidifier_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Dehumidifier Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dehumidifier_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.0', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -409,7 +409,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_battery-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -425,7 +425,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-entry] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -460,7 +460,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][sensor.smart_odor_eliminator_pro_status-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Status', @@ -473,7 +473,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-entry] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -510,7 +510,7 @@ 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_cleverio_pf100][sensor.cleverio_pf100_last_amount-state] +# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Last amount', @@ -525,7 +525,7 @@ 'state': '2.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +565,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_filter_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -581,7 +581,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -621,7 +621,7 @@ 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_uv_runtime-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -637,7 +637,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -672,7 +672,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_level-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water level', @@ -685,7 +685,7 @@ 'state': 'level_3', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -725,7 +725,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -741,7 +741,7 @@ 'state': '18965.0', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -781,7 +781,7 @@ 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -797,7 +797,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -840,7 +840,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_current-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -856,7 +856,7 @@ 'state': '0.083', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -896,7 +896,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_power-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -912,7 +912,7 @@ 'state': '6.4', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -955,7 +955,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][sensor.hvac_meter_voltage-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -971,7 +971,181 @@ 'state': '121.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.051724052462ab286504cur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'droger Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.754', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.051724052462ab286504cur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'droger Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.droger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '593.5', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.051724052462ab286504cur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'droger Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '222.4', + }) +# --- +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1014,7 +1188,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1030,7 +1204,7 @@ 'state': '2.198', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1070,7 +1244,7 @@ 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1086,7 +1260,7 @@ 'state': '495.3', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1129,7 +1303,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1145,7 +1319,7 @@ 'state': '231.4', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1185,7 +1359,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1201,7 +1375,7 @@ 'state': '0.637', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1241,7 +1415,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1257,7 +1431,7 @@ 'state': '0.108', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1297,7 +1471,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1313,7 +1487,7 @@ 'state': '221.1', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1353,7 +1527,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1369,7 +1543,7 @@ 'state': '11.203', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1409,7 +1583,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1425,7 +1599,7 @@ 'state': '2.41', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1465,7 +1639,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1481,7 +1655,7 @@ 'state': '218.7', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1521,7 +1695,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_current-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1537,7 +1711,7 @@ 'state': '0.913', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1577,7 +1751,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_power-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1593,7 +1767,7 @@ 'state': '0.092', }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1633,7 +1807,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[dlq_metering_3pn_wifi][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] +# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -1649,7 +1823,7 @@ 'state': '220.4', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-entry] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1686,7 +1860,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[mcs_door_sensor][sensor.door_garage_battery-state] +# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -1702,7 +1876,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-entry] +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1715,7 +1889,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.frysen_battery_state', + 'entity_id': 'sensor.rat_trap_hedge_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1733,24 +1907,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unique_id': 'tuya.73486068483fda10d633battery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-state] +# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Frysen Battery state', + 'friendly_name': 'rat trap hedge Battery state', }), 'context': , - 'entity_id': 'sensor.frysen_battery_state', + 'entity_id': 'sensor.rat_trap_hedge_battery_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'high', + 'state': 'low', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-entry] +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1764,8 +1938,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frysen_humidity', + 'entity_category': , + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1775,50 +1949,48 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Humidity', + 'original_name': 'Battery', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'translation_key': 'battery', + 'unique_id': 'tuya.bf445324326cbde7c5rg7bbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-state] +# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Frysen Humidity', + 'device_class': 'battery', + 'friendly_name': 'Motion sensor lidl zigbee Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.frysen_humidity', + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '38.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-entry] +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frysen_probe_temperature', + 'entity_category': , + 'entity_id': 'sensor.pir_outside_stairs_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1827,95 +1999,33 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Probe temperature', + 'original_name': 'Battery state', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', - 'unit_of_measurement': , + 'translation_key': 'battery_state', + 'unique_id': 'tuya.20401777500291cfe3a2battery_state', + 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-state] +# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frysen Probe temperature', - 'state_class': , - 'unit_of_measurement': , + 'friendly_name': 'PIR outside stairs Battery state', }), 'context': , - 'entity_id': 'sensor.frysen_probe_temperature', + 'entity_id': 'sensor.pir_outside_stairs_battery_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-13.0', + 'state': 'middle', }) # --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frysen_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'temperature', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frysen Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.frysen_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '22.2', - }) -# --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1950,7 +2060,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state', @@ -1963,7 +2073,7 @@ 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2000,7 +2110,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2016,7 +2126,7 @@ 'state': '52.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2053,7 +2163,7 @@ 'unit_of_measurement': 'lx', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'illuminance', @@ -2069,7 +2179,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2109,7 +2219,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2125,7 +2235,7 @@ 'state': '-40.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2165,7 +2275,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] +# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2181,7 +2291,220 @@ 'state': '24.0', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.frysen_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Frysen Battery state', + }), + 'context': , + 'entity_id': 'sensor.frysen_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Frysen Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.frysen_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.0', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_probe_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_external', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frysen Probe temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frysen_probe_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-13.0', + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frysen_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frysen Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frysen_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2218,7 +2541,7 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Gas sensor Gas', @@ -2233,7 +2556,108 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Solar zijpad Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Battery state', + }), + 'context': , + 'entity_id': 'sensor.solar_zijpad_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2270,7 +2694,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][sensor.wifi_smart_gas_boiler_thermostat_battery-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2286,7 +2710,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2323,7 +2747,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_battery-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2339,7 +2763,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2376,7 +2800,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_humidity-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2392,7 +2816,7 @@ 'state': '47.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-entry] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2432,7 +2856,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[wsdcg_temperature_humidity][sensor.np_downstairs_north_temperature-state] +# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2448,7 +2872,7 @@ 'state': '18.5', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-entry] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2485,7 +2909,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wxkg_wireless_switch][sensor.bathroom_smart_switch_battery-state] +# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2501,7 +2925,108 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-entry] +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.8670375210521cf1349cbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': ' Smoke detector upstairs Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.0', + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.8670375210521cf1349cbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': ' Smoke detector upstairs Battery state', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2541,7 +3066,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_current-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -2557,7 +3082,7 @@ 'state': '5.62', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2597,7 +3122,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_power-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2613,7 +3138,7 @@ 'state': '1.185', }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2653,7 +3178,7 @@ 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_smart_meter][sensor.meter_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', diff --git a/tests/components/tuya/snapshots/test_siren.ambr b/tests/components/tuya/snapshots/test_siren.ambr index 8a6faa31c43..5c46c2bbd19 100644 --- a/tests/components/tuya/snapshots/test_siren.ambr +++ b/tests/components/tuya/snapshots/test_siren.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-entry] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_air_detector][siren.aqi-state] +# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI', @@ -48,3 +48,52 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'siren', + 'entity_category': None, + 'entity_id': 'siren.siren_veranda', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda ', + 'supported_features': , + }), + 'context': , + 'entity_id': 'siren.siren_veranda', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 92243414892..1b90c21bb46 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1,54 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:account-lock', - 'original_name': 'Child lock', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][switch.dehumidifier_child_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Child lock', - 'icon': 'mdi:account-lock', - }), - 'context': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -83,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_child_lock-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Child lock', @@ -97,7 +48,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-entry] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -132,7 +83,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_emma_dehumidifier][switch.dehumidifer_ionizer-state] +# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Ionizer', @@ -146,7 +97,56 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-entry] +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:account-lock', + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier Child lock', + 'icon': 'mdi:account-lock', + }), + 'context': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -181,7 +181,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_smart_odor_eliminator][switch.smart_odor_eliminator_pro_switch-state] +# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Switch', @@ -194,7 +194,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -229,7 +229,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_filter_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Filter reset', @@ -242,7 +242,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -277,7 +277,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_power-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Power', @@ -290,7 +290,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -325,7 +325,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Reset of water usage days', @@ -338,7 +338,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -373,7 +373,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_uv_sterilization-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain UV sterilization', @@ -386,7 +386,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -421,7 +421,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwysj_pixi_smart_drinking_fountain][switch.pixi_smart_drinking_fountain_water_pump_reset-state] +# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'PIXI Smart Drinking Fountain Water pump reset', @@ -434,7 +434,56 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-entry] +# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.apollo_light_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.01155072c4dd573f92b8switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Apollo light Socket 1', + }), + 'context': , + 'entity_id': 'switch.apollo_light_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -464,12 +513,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'socket_1', + 'translation_key': 'indexed_socket', 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_1-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -483,7 +532,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-entry] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -513,12 +562,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'socket_2', + 'translation_key': 'indexed_socket', 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_dual_channel_metering][switch.hvac_meter_socket_2-state] +# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -532,7 +581,251 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] +# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.buitenverlichting_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.53703774d8f15ba9efd3switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Buitenverlichting Socket 1', + }), + 'context': , + 'entity_id': 'switch.buitenverlichting_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf7a2cdaf3ce28d2f7uqnhswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'fakkel veranda Socket 1', + }), + 'context': , + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.droger_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.051724052462ab286504switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'droger Socket 1', + }), + 'context': , + 'entity_id': 'switch.droger_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2echild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Child lock', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.wallwasher_front_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2eswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'wallwasher front Socket 1', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -567,7 +860,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Child lock', @@ -580,7 +873,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -615,7 +908,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dlq_earu_electric_eawcpt][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] +# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': '一路带计量磁保持通断器 Switch', @@ -628,7 +921,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-entry] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -658,12 +951,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_1', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.0665305284f3ebe9fdc1switch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kg_smart_valve][switch.qt_switch_switch_1-state] +# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -677,7 +970,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-entry] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -712,7 +1005,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_bladeless_tower_fan][switch.bree_power-state] +# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Power', @@ -725,7 +1018,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-entry] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -760,7 +1053,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-state] +# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', @@ -773,7 +1066,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -808,7 +1101,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Arm beep', @@ -821,7 +1114,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-entry] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -856,7 +1149,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_siren-state] +# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Multifunction alarm Siren', @@ -869,7 +1162,203 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-entry] +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bubbelbad_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf2206da15147500969d6eswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bubbelbad Socket 1', + }), + 'context': , + 'entity_id': 'switch.bubbelbad_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bubbelbad_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bf2206da15147500969d6eswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bubbelbad Socket 2', + }), + 'context': , + 'entity_id': 'switch.bubbelbad_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.15727703c4dd5709cd78switch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 1', + }), + 'context': , + 'entity_id': 'switch.terras_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.15727703c4dd5709cd78switch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 2', + }), + 'context': , + 'entity_id': 'switch.terras_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -904,7 +1393,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qccdz_ac_charging_control][switch.ac_charging_control_box_switch-state] +# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AC charging control box Switch', @@ -917,7 +1406,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-entry] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -952,7 +1441,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sfkzq_valve_controller][switch.sprinkler_cesare_switch-state] +# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Sprinkler Cesare Switch', @@ -965,7 +1454,487 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-entry] +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Flip', + }), + 'context': , + 'entity_id': 'switch.cam_garage_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_garage_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_garage_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_garage_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_garage_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Flip', + }), + 'context': , + 'entity_id': 'switch.cam_porch_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_porch_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_porch_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_porch_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_porch_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -995,12 +1964,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_1', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_1-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1014,7 +1983,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1044,12 +2013,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_2', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_2-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1063,7 +2032,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1093,12 +2062,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_3', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_3', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_3-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1112,7 +2081,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-entry] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1142,12 +2111,12 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch_4', + 'translation_key': 'indexed_switch', 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_4', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_4_443][switch.4_433_switch_4-state] +# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1161,7 +2130,55 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-entry] +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy saving', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saving', + 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_save_energy', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad Energy saving', + }), + 'context': , + 'entity_id': 'switch.solar_zijpad_energy_saving', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1196,7 +2213,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_air_conditioner][switch.clima_cucina_child_lock-state] +# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Clima cucina Child lock', @@ -1209,7 +2226,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1244,7 +2261,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_wifi_smart_gas_boiler_thermostat][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] +# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Child lock', diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index f59e325b6cc..85dd644b79c 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import DEVICE_MOCKS, MockDeviceListener, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -60,7 +60,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) @pytest.mark.parametrize( ("fault_value", "tankfull", "defrost", "wet"), @@ -78,16 +78,23 @@ async def test_bitmap( mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, mock_device: CustomerDevice, + mock_listener: MockDeviceListener, fault_value: int, tankfull: str, defrost: str, wet: str, ) -> None: - """Test BITMAP fault sensor on cs_arete_two_12l_dehumidifier_air_purifier.""" - mock_device.status["fault"] = fault_value - + """Test BITMAP fault sensor on cs_zibqa9dutqyaxym2.""" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == "off" + assert hass.states.get("binary_sensor.dehumidifier_defrost").state == "off" + assert hass.states.get("binary_sensor.dehumidifier_wet").state == "off" + + await mock_listener.async_send_device_update( + hass, mock_device, {"fault": fault_value} + ) + assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == tankfull assert hass.states.get("binary_sensor.dehumidifier_defrost").state == defrost assert hass.states.get("binary_sensor.dehumidifier_wet").state == wet diff --git a/tests/components/tuya/test_climate.py b/tests/components/tuya/test_climate.py index e8aee3f4f96..01fdf469e27 100644 --- a/tests/components/tuya/test_climate.py +++ b/tests/components/tuya/test_climate.py @@ -66,7 +66,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_temperature( hass: HomeAssistant, @@ -96,7 +96,7 @@ async def test_set_temperature( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_windspeed( hass: HomeAssistant, @@ -127,7 +127,7 @@ async def test_fan_mode_windspeed( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_fan_mode_no_valid_code( hass: HomeAssistant, @@ -161,7 +161,7 @@ async def test_fan_mode_no_valid_code( @pytest.mark.parametrize( "mock_device_code", - ["kt_serenelife_slpac905wuk_air_conditioner"], + ["kt_5wnlzekkstwcdsvm"], ) async def test_set_humidity_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_cover.py b/tests/components/tuya/test_cover.py index 24e43dcccec..20d84878a58 100644 --- a/tests/components/tuya/test_cover.py +++ b/tests/components/tuya/test_cover.py @@ -67,7 +67,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_open_service( @@ -101,7 +101,7 @@ async def test_open_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_close_service( @@ -135,7 +135,7 @@ async def test_close_service( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_position( hass: HomeAssistant, @@ -168,7 +168,7 @@ async def test_set_position( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) @pytest.mark.parametrize( ("percent_control", "percent_state"), @@ -202,7 +202,7 @@ async def test_percent_state_on_cover( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_set_tilt_position_not_supported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_diagnostics.py b/tests/components/tuya/test_diagnostics.py index 2009f117efb..f07c2faa229 100644 --- a/tests/components/tuya/test_diagnostics.py +++ b/tests/components/tuya/test_diagnostics.py @@ -22,7 +22,7 @@ from tests.components.diagnostics import ( from tests.typing import ClientSessionGenerator -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_entry_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, @@ -43,7 +43,7 @@ async def test_entry_diagnostics( ) -@pytest.mark.parametrize("mock_device_code", ["rqbj_gas_sensor"]) +@pytest.mark.parametrize("mock_device_code", ["rqbj_4iqe2hsfyd86kwwc"]) async def test_device_diagnostics( hass: HomeAssistant, mock_manager: ManagerCompat, diff --git a/tests/components/tuya/test_humidifier.py b/tests/components/tuya/test_humidifier.py index d4996bcd32a..bd3604b25dd 100644 --- a/tests/components/tuya/test_humidifier.py +++ b/tests/components/tuya/test_humidifier.py @@ -65,7 +65,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_on( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_turn_on( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_off( hass: HomeAssistant, @@ -119,7 +119,7 @@ async def test_turn_off( @pytest.mark.parametrize( "mock_device_code", - ["cs_arete_two_12l_dehumidifier_air_purifier"], + ["cs_zibqa9dutqyaxym2"], ) async def test_set_humidity( hass: HomeAssistant, @@ -149,7 +149,7 @@ async def test_set_humidity( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_on_unsupported( hass: HomeAssistant, @@ -179,7 +179,7 @@ async def test_turn_on_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_turn_off_unsupported( hass: HomeAssistant, @@ -209,7 +209,7 @@ async def test_turn_off_unsupported( @pytest.mark.parametrize( "mock_device_code", - ["cs_smart_dry_plus"], + ["cs_vmxuxszzjwp5smli"], ) async def test_set_humidity_unsupported( hass: HomeAssistant, diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index 8fbf6fb4e3b..ab96f58ecd0 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -15,7 +15,7 @@ from . import initialize_entry from tests.common import MockConfigEntry -@pytest.mark.parametrize("mock_device_code", ["ydkt_dolceclima_unsupported"]) +@pytest.mark.parametrize("mock_device_code", ["ydkt_jevroj5aguwdbs2e"]) async def test_unsupported_device( hass: HomeAssistant, mock_manager: ManagerCompat, @@ -24,7 +24,6 @@ async def test_unsupported_device( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, - caplog: pytest.LogCaptureFixture, ) -> None: """Test unsupported device.""" @@ -39,11 +38,3 @@ async def test_unsupported_device( assert not er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ) - - # Information log entry added - assert ( - "Device DOLCECLIMA 10 HP WIFI (mock_device_id) has been ignored" - " as it does not provide any standard instructions (status, status_range" - " and function are all empty) - see " - "https://github.com/tuya/tuya-device-sharing-sdk/issues/11" in caplog.text - ) diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index 0d4706a5563..e3586613876 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -64,7 +64,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_on_white( hass: HomeAssistant, @@ -98,7 +98,7 @@ async def test_turn_on_white( @pytest.mark.parametrize( "mock_device_code", - ["dj_smart_light_bulb"], + ["dj_mki13ie507rlry4r"], ) async def test_turn_off( hass: HomeAssistant, diff --git a/tests/components/tuya/test_number.py b/tests/components/tuya/test_number.py index b6c7b1f6de5..f28d6414170 100644 --- a/tests/components/tuya/test_number.py +++ b/tests/components/tuya/test_number.py @@ -59,7 +59,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value( hass: HomeAssistant, @@ -89,7 +89,7 @@ async def test_set_value( @pytest.mark.parametrize( "mock_device_code", - ["mal_alarm_host"], + ["mal_gyitctrjj1kefxp2"], ) async def test_set_value_no_function( hass: HomeAssistant, diff --git a/tests/components/tuya/test_select.py b/tests/components/tuya/test_select.py index cd1d926ff76..475fab30b90 100644 --- a/tests/components/tuya/test_select.py +++ b/tests/components/tuya/test_select.py @@ -62,7 +62,7 @@ async def test_platform_setup_no_discovery( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_option( hass: HomeAssistant, @@ -92,7 +92,7 @@ async def test_select_option( @pytest.mark.parametrize( "mock_device_code", - ["cl_am43_corded_motor_zigbee_cover"], + ["cl_zah67ekd"], ) async def test_select_invalid_option( hass: HomeAssistant, diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 915c0f5080e..15fcd7cee09 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -97,7 +97,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -107,7 +106,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 9e8bb6f7381..3b4e21be1e1 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -66,7 +66,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -76,7 +75,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -148,7 +146,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -158,7 +155,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -230,7 +226,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -240,7 +235,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -312,7 +306,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -322,7 +315,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -394,7 +386,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Twente Milieu', @@ -404,7 +395,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 5c9ed6d4683..c57f2987c5b 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -59,7 +59,6 @@ 'entry_type': , 'hw_version': None, 'id': , - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -69,7 +68,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/uptime_kuma/conftest.py b/tests/components/uptime_kuma/conftest.py index 7895f068b31..a092c2e85ba 100644 --- a/tests/components/uptime_kuma/conftest.py +++ b/tests/components/uptime_kuma/conftest.py @@ -10,9 +10,20 @@ from pythonkuma.update import LatestRelease from homeassistant.components.uptime_kuma.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from tests.common import MockConfigEntry +ADDON_SERVICE_INFO = HassioServiceInfo( + config={ + "addon": "Uptime Kuma", + CONF_URL: "http://localhost:3001/", + }, + name="Uptime Kuma", + slug="a0d7b954_uptime-kuma", + uuid="1234", +) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: diff --git a/tests/components/uptime_kuma/test_config_flow.py b/tests/components/uptime_kuma/test_config_flow.py index ab695107b9b..b8b40a5b759 100644 --- a/tests/components/uptime_kuma/test_config_flow.py +++ b/tests/components/uptime_kuma/test_config_flow.py @@ -6,11 +6,13 @@ import pytest from pythonkuma import UptimeKumaAuthenticationException, UptimeKumaConnectionException from homeassistant.components.uptime_kuma.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import ADDON_SERVICE_INFO + from tests.common import MockConfigEntry @@ -280,3 +282,201 @@ async def test_flow_reconfigure_errors( } assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pythonkuma: AsyncMock, +) -> None: + """Test config flow initiated by Supervisor.""" + mock_pythonkuma.metrics.side_effect = [UptimeKumaAuthenticationException, None] + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Uptime Kuma"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_confirm_only( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test config flow initiated by Supervisor. + + Config flow will first try to configure without authentication and if it + fails will show the form. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Uptime Kuma"} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_already_configured( + hass: HomeAssistant, +) -> None: + """Test config flow initiated by Supervisor.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (UptimeKumaConnectionException, "cannot_connect"), + (UptimeKumaAuthenticationException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_hassio_addon_discovery_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pythonkuma: AsyncMock, + raise_error: Exception, + text_error: str, +) -> None: + """Test we handle errors and recover.""" + mock_pythonkuma.metrics.side_effect = UptimeKumaAuthenticationException + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + mock_pythonkuma.metrics.side_effect = raise_error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": text_error} + + mock_pythonkuma.metrics.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "apikey"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "a0d7b954_uptime-kuma" + assert result["data"] == { + CONF_URL: "http://localhost:3001/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_ignored( + hass: HomeAssistant, +) -> None: + """Test we abort discovery flow if discovery was ignored.""" + + MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IGNORE, + data={}, + entry_id="123456789", + unique_id="1234", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_hassio_addon_discovery_update_info( + hass: HomeAssistant, +) -> None: + """Test we abort discovery flow if already configured and we update from discovery info.""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="a0d7b954_uptime-kuma", + data={ + CONF_URL: "http://localhost:80/", + CONF_VERIFY_SSL: True, + CONF_API_KEY: "apikey", + }, + entry_id="123456789", + unique_id="1234", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_SERVICE_INFO, + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert entry.data[CONF_URL] == "http://localhost:3001/" diff --git a/tests/components/uptime_kuma/test_init.py b/tests/components/uptime_kuma/test_init.py index 6e2ef43b14d..61d196f0263 100644 --- a/tests/components/uptime_kuma/test_init.py +++ b/tests/components/uptime_kuma/test_init.py @@ -8,8 +8,11 @@ from pythonkuma import UptimeKumaAuthenticationException, UptimeKumaException from homeassistant.components.uptime_kuma.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator @pytest.mark.usefixtures("mock_pythonkuma") @@ -77,3 +80,85 @@ async def test_config_reauth_flow( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == config_entry.entry_id + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_stale_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we can remove a device that is not in the coordinator data.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "123456789_1")} + ) + + config_entry.runtime_data.data.pop(1) + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, "123456789_1")}) is None + ) + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_current_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we cannot remove a device if it is still active.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "123456789_1")} + ) + + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] is False + assert device_registry.async_get_device(identifiers={(DOMAIN, "123456789_1")}) + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_remove_entry_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test we cannot remove the device with the update entity.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "123456789")}) + + response = await ws_client.remove_device(device_entry.id, config_entry.entry_id) + + assert response["success"] is False + assert device_registry.async_get_device(identifiers={(DOMAIN, "123456789")}) diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index f7cbeb7a052..d909480c8ea 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -97,6 +97,7 @@ def mock_module_subdevices() -> AsyncMock: """Mock a velbus module.""" module = AsyncMock(spec=Module) module.get_type_name.return_value = "VMB2BLE" + module.get_type.return_value = "123" module.get_addresses.return_value = [88] module.get_name.return_value = "Kitchen" module.get_serial.return_value = "a1b2c3d4e5f6" @@ -138,7 +139,7 @@ def mock_temperature() -> AsyncMock: channel.get_module_sw_version.return_value = "3.0.0" channel.get_module_serial.return_value = "asdfghjk" channel.get_module_type.return_value = 1 - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.is_counter_channel.return_value = False channel.get_class.return_value = "temperature" channel.get_unit.return_value = "°C" @@ -184,7 +185,7 @@ def mock_select() -> AsyncMock: channel.get_full_name.return_value = "Kitchen" channel.get_module_sw_version.return_value = "1.1.1" channel.get_module_serial.return_value = "qwerty1234567" - channel.is_sub_device.return_value = False + channel.is_sub_device.return_value = True channel.get_options.return_value = ["none", "summer", "winter", "holiday"] channel.get_selected_program.return_value = "winter" return channel diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr index 1e17753a02f..0383abc0313 100644 --- a/tests/components/velbus/snapshots/test_init.ambr +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -18,7 +18,6 @@ '1', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -28,165 +27,9 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88-9', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMB2BLE', - 'model_id': '10', - 'name': 'Basement', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': '1234', - 'suggested_area': None, - 'sw_version': '1.0.1', - 'via_device_id': , - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88-11', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMB2BLE', - 'model_id': '10', - 'name': 'Basement', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': '12345', - 'suggested_area': None, - 'sw_version': '1.0.1', - 'via_device_id': , - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88-10', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMBDN1', - 'model_id': '9', - 'name': 'Dimmer full name', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': 'a1b2c3d4e5f6g7', - 'suggested_area': None, - 'sw_version': '1.0.0', - 'via_device_id': , - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88-2', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMB7IN', - 'model_id': '4', - 'name': 'Input', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, - 'sw_version': '1.0.0', - 'via_device_id': , - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'velbus', - '88', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Velleman', - 'model': 'VMB4GPO', - 'model_id': '1', - 'name': 'Living room', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': 'asdfghjk', - 'suggested_area': None, - 'sw_version': '3.0.0', - 'via_device_id': None, - }), DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -204,7 +47,6 @@ '2', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -214,10 +56,183 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'a1b2c3d4e5f6', - 'suggested_area': None, 'sw_version': '1.0.0', 'via_device_id': None, }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '123', + 'name': 'Kitchen (VMB2BLE)', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'sw_version': '2.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-10', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMBDN1', + 'model_id': '9', + 'name': 'Dimmer full name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6g7', + 'sw_version': '1.0.0', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-11', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '12345', + 'sw_version': '1.0.1', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-2', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB7IN', + 'model_id': '4', + 'name': 'Input', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'sw_version': '1.0.0', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-3', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB4GPO', + 'model_id': '1', + 'name': 'Living room', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'asdfghjk', + 'sw_version': '3.0.0', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-33', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB4RYNO', + 'model_id': '3', + 'name': 'Kitchen', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'qwerty1234567', + 'sw_version': '1.1.1', + 'via_device_id': , + }), DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -235,7 +250,6 @@ '88-55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Velleman', @@ -245,7 +259,35 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': 'qwerty123', - 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-9', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234', 'sw_version': '1.0.1', 'via_device_id': , }), diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 2d28ba81cb1..fc9046f977f 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -176,7 +176,8 @@ async def test_device_registry( device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - assert device_entries == snapshot + # Sort by identifier to ensure consistent order in snapshot + assert sorted(device_entries, key=lambda x: list(x.identifiers)[0][1]) == snapshot device_parent = device_registry.async_get_device(identifiers={(DOMAIN, "88")}) assert device_parent.via_device_id is None diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index fe330b82ca7..86cfa8198ba 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -119,7 +117,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -129,7 +126,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -219,7 +215,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -229,7 +224,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -321,7 +315,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -331,7 +324,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -423,7 +415,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -433,7 +424,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -462,7 +452,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -472,7 +461,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -501,7 +489,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -511,7 +498,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -540,7 +526,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -550,7 +535,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -579,7 +563,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -589,7 +572,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -618,7 +600,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -628,7 +609,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -724,7 +704,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -734,7 +713,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -763,7 +741,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -773,7 +750,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 20bf56ef9c4..df2dad8825d 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -57,7 +55,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -67,7 +64,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -96,7 +92,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -106,7 +101,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -135,7 +129,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -145,7 +138,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -174,7 +166,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -184,7 +175,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -267,7 +257,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -277,7 +266,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -362,7 +350,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -372,7 +359,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -401,7 +387,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -411,7 +396,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -440,7 +424,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -450,7 +433,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -479,7 +461,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -489,7 +470,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -518,7 +498,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -528,7 +507,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -626,7 +604,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -636,7 +613,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index a47de22f68b..143520b68c2 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -153,7 +151,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -163,7 +160,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -242,7 +238,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -252,7 +247,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -428,7 +422,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -438,7 +431,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -614,7 +606,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -624,7 +615,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -653,7 +643,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -663,7 +652,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -692,7 +680,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -702,7 +689,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -782,7 +768,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -792,7 +777,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -872,7 +856,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -882,7 +865,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1235,7 +1217,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1245,7 +1226,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1274,7 +1254,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1284,7 +1263,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -1313,7 +1291,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -1323,7 +1300,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index edd2eee8b1f..e7917397063 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -18,7 +18,6 @@ 'air-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -28,7 +27,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -103,7 +101,6 @@ 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -113,7 +110,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -188,7 +184,6 @@ '400s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -198,7 +193,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -273,7 +267,6 @@ '600s-purifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -283,7 +276,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -358,7 +350,6 @@ 'dimmable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -368,7 +359,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -397,7 +387,6 @@ 'dimmable-switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -407,7 +396,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -436,7 +424,6 @@ '200s-humidifier4321', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -446,7 +433,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -521,7 +507,6 @@ '600s-humidifier', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -531,7 +516,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -606,7 +590,6 @@ 'outlet', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -616,7 +599,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -692,7 +674,6 @@ 'smarttowerfan', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -702,7 +683,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -777,7 +757,6 @@ 'tunable-bulb', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -787,7 +766,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), @@ -816,7 +794,6 @@ 'switch', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'VeSync', @@ -826,7 +803,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }), diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index a34d885b77b..cd6280077a2 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -489,6 +489,466 @@ 'state': '2024-02-07T23:01:15+00:00', }) # --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_duration_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_duration_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation duration yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_yesterday_final', + 'unique_id': '24432_precip_minutes_local_yesterday_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_duration_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation duration yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_duration_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_day_final', + 'unique_id': '24432_precip_accum_local_day_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Nearcast precipitation yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_yesterday_final', + 'unique_id': '24432_precip_accum_local_yesterday_final', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_nearcast_precipitation_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Nearcast precipitation yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_nearcast_precipitation_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_duration_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation duration today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_day', + 'unique_id': '24432_precip_minutes_local_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation duration today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_duration_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_duration_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation duration yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_minutes_local_yesterday', + 'unique_id': '24432_precip_minutes_local_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_duration_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation duration yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_duration_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation today', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_day', + 'unique_id': '24432_precip_accum_local_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_type_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'rain', + 'snow', + 'sleet', + 'storm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_type_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation type yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_analysis_type_yesterday', + 'unique_id': '24432_precip_analysis_type_yesterday', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_type_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'device_class': 'enum', + 'friendly_name': 'My Home Station Precipitation type yesterday', + 'options': list([ + 'none', + 'rain', + 'snow', + 'sleet', + 'storm', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_type_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_precipitation_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation yesterday', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_local_yesterday', + 'unique_id': '24432_precip_accum_local_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_precipitation_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Precipitation yesterday', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_precipitation_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.my_home_station_pressure_barometric-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -609,6 +1069,62 @@ 'state': '1006.2', }) # --- +# name: test_all_entities[sensor.my_home_station_rain_last_hour-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_home_station_rain_last_hour', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Rain last hour', + 'platform': 'weatherflow_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precip_accum_last_1hr', + 'unique_id': '24432_precip_accum_last_1hr', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_home_station_rain_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Weather data delivered by WeatherFlow/Tempest API', + 'friendly_name': 'My Home Station Rain last hour', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_home_station_rain_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.my_home_station_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/webostv/snapshots/test_media_player.ambr b/tests/components/webostv/snapshots/test_media_player.ambr index 9c097b166ec..7c0bdfb0d13 100644 --- a/tests/components/webostv/snapshots/test_media_player.ambr +++ b/tests/components/webostv/snapshots/test_media_player.ambr @@ -53,7 +53,6 @@ 'some-fake-uuid', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'LG', @@ -63,7 +62,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '1234567890', - 'suggested_area': None, 'sw_version': 'major.minor', 'via_device_id': None, }) diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 67f6baf45bb..30cdb9080f8 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -65,7 +65,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -75,7 +74,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -147,7 +145,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -157,7 +154,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -233,7 +229,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -243,7 +238,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -315,7 +309,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -325,7 +318,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -397,7 +389,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -407,7 +398,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -478,7 +468,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -488,7 +477,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -559,7 +547,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -569,7 +556,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -640,7 +626,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -650,7 +635,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -721,7 +705,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -731,7 +714,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -854,7 +836,6 @@ 'home-assistant.io', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': None, @@ -864,7 +845,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/withings/snapshots/test_init.ambr b/tests/components/withings/snapshots/test_init.ambr index ec711def829..31c23987680 100644 --- a/tests/components/withings/snapshots/test_init.ambr +++ b/tests/components/withings/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '12345', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Withings', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) @@ -50,7 +48,6 @@ 'f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Withings', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index d8a29ed7c48..b7bb8a1eea1 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -70,7 +70,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -80,7 +79,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 877c8baa93e..8f94c270984 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -78,7 +78,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -88,7 +87,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -172,7 +170,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -182,7 +179,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6cfbe1de5d4..a981b741852 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -80,7 +80,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -90,7 +89,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -312,7 +310,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -322,7 +319,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -406,7 +402,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -416,7 +411,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) @@ -500,7 +494,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -510,7 +503,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.99.0b1', 'via_device_id': None, }) diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index c32bc314cc0..43e91f7b485 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -71,7 +71,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -81,7 +80,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -156,7 +154,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -166,7 +163,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -242,7 +238,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -252,7 +247,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) @@ -328,7 +322,6 @@ 'aabbccddeeff', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WLED', @@ -338,7 +331,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': '0.14.4', 'via_device_id': None, }) diff --git a/tests/components/wmspro/snapshots/test_cover.ambr b/tests/components/wmspro/snapshots/test_cover.ambr index 53b2f6205cb..8590c4ba725 100644 --- a/tests/components/wmspro/snapshots/test_cover.ambr +++ b/tests/components/wmspro/snapshots/test_cover.ambr @@ -17,7 +17,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_init.ambr b/tests/components/wmspro/snapshots/test_init.ambr index 147d66f2b69..ee485fe3980 100644 --- a/tests/components/wmspro/snapshots/test_init.ambr +++ b/tests/components/wmspro/snapshots/test_init.ambr @@ -17,7 +17,6 @@ '19239', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -50,7 +48,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -60,7 +57,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -83,7 +79,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -93,7 +88,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -116,7 +110,6 @@ '19239', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -126,7 +119,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '19239', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -149,7 +141,6 @@ '58717', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -159,7 +150,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '58717', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -182,7 +172,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -192,7 +181,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -215,7 +203,6 @@ '116682', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -225,7 +212,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '116682', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -248,7 +234,6 @@ '172555', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -258,7 +243,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '172555', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -281,7 +265,6 @@ '18894', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -291,7 +274,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '18894', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -314,7 +296,6 @@ '230952', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -324,7 +305,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '230952', - 'suggested_area': 'Wohnbereich', 'sw_version': None, 'via_device_id': , }) @@ -347,7 +327,6 @@ '284942', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -357,7 +336,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '284942', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) @@ -380,7 +358,6 @@ '328518', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -390,7 +367,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '328518', - 'suggested_area': 'Alle', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_light.ambr b/tests/components/wmspro/snapshots/test_light.ambr index d6ccebfb5ea..9efbadff951 100644 --- a/tests/components/wmspro/snapshots/test_light.ambr +++ b/tests/components/wmspro/snapshots/test_light.ambr @@ -17,7 +17,6 @@ '97358', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '97358', - 'suggested_area': 'Terrasse', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wmspro/snapshots/test_scene.ambr b/tests/components/wmspro/snapshots/test_scene.ambr index b5dddb368c9..b9053992ddc 100644 --- a/tests/components/wmspro/snapshots/test_scene.ambr +++ b/tests/components/wmspro/snapshots/test_scene.ambr @@ -31,7 +31,6 @@ '42581', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WAREMA Renkhoff SE', @@ -41,7 +40,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': '42581', - 'suggested_area': 'Raum 0', 'sw_version': None, 'via_device_id': , }) diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index c5b23cc8e79..d66c1d2285b 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -17,7 +17,6 @@ '1234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'WOLF GmbH', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': None, 'sw_version': None, 'via_device_id': None, }) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 24423264f93..d03f2622c71 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -8,13 +8,14 @@ from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import area_registry as ar, device_registry as dr async def test_device_registry_info( hass: HomeAssistant, satellite_device: SatelliteDevice, satellite_config_entry: ConfigEntry, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test info in device registry.""" @@ -26,7 +27,7 @@ async def test_device_registry_info( ) assert device is not None assert device.name == "Test Satellite" - assert device.suggested_area == "Office" + assert device.area_id == area_registry.async_get_area_by_name("Office").id # Check associated entities assist_in_progress_id = satellite_device.get_assist_in_progress_entity_id(hass) diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr index 9db0d760efb..226d0bdbba9 100644 --- a/tests/components/yale/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -17,7 +17,6 @@ 'tmt100', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Yale Home Inc.', @@ -27,7 +26,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'tmt100 Name', 'sw_version': '3.1.0-HYDRC75+201909251139', 'via_device_id': None, }) diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr index 00653a9b0c1..3f89fe08525 100644 --- a/tests/components/yale/snapshots/test_lock.ambr +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -21,7 +21,6 @@ 'online_with_doorsense', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'Yale Home Inc.', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'online_with_doorsense Name', 'sw_version': 'undefined-4.3.0-1.8.14', 'via_device_id': None, }) diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index c8cbc407106..04d190b170c 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -47,6 +47,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import find_entity_id, update_attribute_cache @@ -156,7 +157,6 @@ async def setup_test_data( ) ) zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) - zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) return zha_device_proxy, cluster, fw_image, installed_fw_version @@ -643,3 +643,26 @@ async def test_update_release_notes( assert "Some lengthy release notes" in result["result"] assert OTA_MESSAGE_RELIABILITY in result["result"] assert OTA_MESSAGE_BATTERY_POWERED in result["result"] + + +async def test_update_version_sync_device_registry( + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test firmware version syncing between the ZHA device and Home Assistant.""" + await setup_zha() + zha_device, _, _, _ = await setup_test_data(hass, zigpy_device_mock) + + zha_device.device.async_update_firmware_version("0x12345678") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0x12345678" + + zha_device.device.async_update_firmware_version("0xabcd1234") + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.device.ieee))} + ) + assert reg_device.sw_version == "0xabcd1234" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 3c07869d5b7..eef92a7eb0a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -565,12 +565,6 @@ def mock_listen_block_fixture() -> asyncio.Event: return asyncio.Event() -@pytest.fixture(name="listen_result") -def listen_result_fixture() -> asyncio.Future[None]: - """Mock a listen result.""" - return asyncio.Future() - - @pytest.fixture(name="client") def mock_client_fixture( controller_state: dict[str, Any], @@ -578,7 +572,6 @@ def mock_client_fixture( version_state: dict[str, Any], log_config_state: dict[str, Any], listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ): """Mock a client.""" with patch( @@ -587,15 +580,16 @@ def mock_client_fixture( client = client_class.return_value async def connect(): + listen_block.clear() await asyncio.sleep(0) client.connected = True async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() await listen_block.wait() - await listen_result async def disconnect(): + listen_block.set() client.connected = False client.connect = AsyncMock(side_effect=connect) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 6359f4bf5e7..0b83d08072c 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,5 +1,6 @@ """Test the Z-Wave JS Websocket API.""" +import asyncio from copy import deepcopy from http import HTTPStatus from io import BytesIO @@ -5109,17 +5110,12 @@ async def test_hard_reset_controller( ws_client = await hass_ws_client(hass) assert entry.unique_id == "3245146787" - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_driver_hard_reset() -> None: client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + client.driver.async_hard_reset = AsyncMock(side_effect=mock_driver_hard_reset) await ws_client.send_json_auto_id( { @@ -5128,6 +5124,7 @@ async def test_hard_reset_controller( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5135,16 +5132,10 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() # Test client connect error when getting the server version. @@ -5158,6 +5149,7 @@ async def test_hard_reset_controller( ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5165,33 +5157,24 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] - - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) + assert client.driver.async_hard_reset.call_count == 1 assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller reset" ) in caplog.text - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() + get_server_version.side_effect = None # Test sending command with driver not ready and timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_driver_hard_reset_no_driver_ready() -> None: + pass - client.async_send_command.side_effect = async_send_command_no_driver_ready + client.driver.async_hard_reset.side_effect = mock_driver_hard_reset_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5201,6 +5184,7 @@ async def test_hard_reset_controller( } ) msg = await ws_client.receive_json() + await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} @@ -5208,32 +5192,29 @@ async def test_hard_reset_controller( assert device is not None assert msg["result"] == device.id assert msg["success"] + assert client.driver.async_hard_reset.call_count == 1 - assert client.async_send_command.call_count == 3 - # The first call is the relevant hard reset command. - # 25 is the require_schema parameter. - assert client.async_send_command.call_args_list[0] == call( - {"command": "driver.hard_reset"}, 25 - ) - - client.async_send_command.reset_mock() + client.driver.async_hard_reset.reset_mock() # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.driver.Driver.async_hard_reset", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json_auto_id( - { - TYPE: "zwave_js/hard_reset_controller", - ENTRY_ID: entry.entry_id, - } - ) - msg = await ws_client.receive_json() + client.driver.async_hard_reset.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "zwave_error: Z-Wave error 1 - error message" + assert client.driver.async_hard_reset.call_count == 1 + + client.driver.async_hard_reset.side_effect = None # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -5578,17 +5559,24 @@ async def test_restore_nvm( # Set up mocks for the controller events controller = client.driver.controller - async def async_send_command_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" + async def mock_restore_nvm_base64( + self, base64_data: str, options: dict[str, bool] | None = None + ) -> None: + controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 150, "total": 200}, + ) + controller.data["homeId"] = 3245146787 client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) - return {} - client.async_send_command.side_effect = async_send_command_driver_ready + controller.async_restore_nvm_base64 = AsyncMock(side_effect=mock_restore_nvm_base64) # Send the subscription request await ws_client.send_json_auto_id( @@ -5599,7 +5587,19 @@ async def test_restore_nvm( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5609,53 +5609,18 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - # Simulate progress events - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 25, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 25 - assert msg["event"]["total"] == 100 - - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 50, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 50 - assert msg["event"]["total"] == 100 - await hass.async_block_till_done() # Verify the restore was called # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert entry.unique_id == "1234" - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test client connect error when getting the server version. @@ -5670,7 +5635,19 @@ async def test_restore_nvm( } ) - # Verify the finished event first + # Verify the convert progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm convert progress" + assert msg["event"]["bytesRead"] == 100 + assert msg["event"]["total"] == 200 + + # Verify the restore progress event + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 150 + assert msg["event"]["total"] == 200 + + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5680,47 +5657,46 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + await hass.async_block_till_done() + + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) assert ( - "Failed to get server version, cannot update config entry" + "Failed to get server version, cannot update config entry " "unique id with new home id, after controller NVM restore" ) in caplog.text - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() + get_server_version.side_effect = None - # Test sending command with driver not ready and timeout. + # Test sending command without driver ready event causing timeout. - async def async_send_command_no_driver_ready( - message: dict[str, Any], - require_schema: int | None = None, - ) -> dict: - """Send a command and get a response.""" - return {} + async def mock_restore_nvm_without_driver_ready( + data: bytes, options: dict[str, bool] | None = None + ): + controller.data["homeId"] = 3245146787 - client.async_send_command.side_effect = async_send_command_no_driver_ready + controller.async_restore_nvm_base64.side_effect = ( + mock_restore_nvm_without_driver_ready + ) with patch( - "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT", new=0, ): # Send the subscription request await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) - # Verify the finished event first + # Verify the finished event msg = await ws_client.receive_json() assert msg["type"] == "event" @@ -5734,37 +5710,41 @@ async def test_restore_nvm( await hass.async_block_till_done() # Verify the restore was called - # The first call is the relevant one for nvm restore. - assert client.async_send_command.call_count == 3 - assert client.async_send_command.call_args_list[0] == call( - { - "command": "controller.restore_nvm", - "nvmData": "dGVzdA==", - "migrateOptions": {"preserveRoutes": False}, - }, - require_schema=42, + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, ) - client.async_send_command.reset_mock() + controller.async_restore_nvm_base64.reset_mock() # Test restore failure - with patch( - f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - # Send the subscription request - await ws_client.send_json_auto_id( - { - "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, - "data": "dGVzdA==", # base64 encoded "test" - } - ) + controller.async_restore_nvm_base64.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) - # Verify error response - msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": entry.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify error response + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + + await hass.async_block_till_done() + + # Verify the restore was called + assert controller.async_restore_nvm_base64.call_count == 1 + assert controller.async_restore_nvm_base64.call_args == call( + "dGVzdA==", + {"preserveRoutes": False}, + ) # Test entry_id not found await ws_client.send_json_auto_id( @@ -5779,13 +5759,13 @@ async def test_restore_nvm( assert msg["error"]["code"] == "not_found" # Test config entry not loaded - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json_auto_id( { "type": "zwave_js/restore_nvm", - "entry_id": integration.entry_id, + "entry_id": entry.entry_id, "data": "dGVzdA==", # base64 encoded "test" } ) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 15ec6959caf..52b840fb690 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1101,7 +1101,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -1111,7 +1111,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3897,7 +3897,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") with patch( - ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + ("homeassistant.components.zwave_js.helpers.DRIVER_READY_EVENT_TIMEOUT"), new=0, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -3907,7 +3907,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 4 + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d9b3f392dd6..3c39868ff93 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -196,19 +196,24 @@ async def test_listen_done_during_setup_before_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup before forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running + async def connect(): + await asyncio.sleep(0) + client.connected = True + async def listen(driver_ready: asyncio.Event) -> None: await listen_block.wait() await listen_result async_fire_time_changed(hass, fire_all=True) + client.connect.side_effect = connect client.listen.side_effect = listen hass.set_state(core_state) listen_block.set() @@ -229,9 +234,9 @@ async def test_not_connected_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], ) -> None: """Test we handle not connected client during setup after forward entry.""" + listen_result = asyncio.Future[None]() async def send_command_side_effect(*args: Any, **kwargs: Any) -> None: """Mock send command.""" @@ -277,12 +282,12 @@ async def test_listen_done_during_setup_after_forward_entry( hass: HomeAssistant, client: MagicMock, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, ) -> None: """Test listen task finishing during setup after forward entry.""" + listen_result = asyncio.Future[None]() assert hass.state is CoreState.running original_send_command_side_effect = client.async_send_command.side_effect @@ -320,16 +325,14 @@ async def test_listen_done_during_setup_after_forward_entry( @pytest.mark.parametrize( - ("core_state", "final_config_entry_state", "disconnect_call_count"), + ("core_state", "disconnect_call_count"), [ ( CoreState.running, - ConfigEntryState.SETUP_RETRY, - 2, - ), # the reload will cause a disconnect call too + 1, + ), # the reload will cause a disconnect ( CoreState.stopping, - ConfigEntryState.LOADED, 0, ), # the home assistant stop event will handle the disconnect ], @@ -345,19 +348,33 @@ async def test_listen_done_during_setup_after_forward_entry( async def test_listen_done_after_setup( hass: HomeAssistant, client: MagicMock, - integration: MockConfigEntry, listen_block: asyncio.Event, - listen_result: asyncio.Future[None], core_state: CoreState, listen_future_result_method: str, listen_future_result: Exception | None, - final_config_entry_state: ConfigEntryState, disconnect_call_count: int, ) -> None: """Test listen task finishing after setup.""" - config_entry = integration - assert config_entry.state is ConfigEntryState.LOADED + listen_result = asyncio.Future[None]() + + async def listen(driver_ready: asyncio.Event) -> None: + driver_ready.set() + await listen_block.wait() + await listen_result + + client.listen.side_effect = listen + + config_entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org", "data_collection_opted_in": True}, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.state is CoreState.running + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == 0 hass.set_state(core_state) @@ -365,10 +382,51 @@ async def test_listen_done_after_setup( getattr(listen_result, listen_future_result_method)(listen_future_result) await hass.async_block_till_done() - assert config_entry.state is final_config_entry_state + assert config_entry.state is ConfigEntryState.LOADED assert client.disconnect.call_count == disconnect_call_count +async def test_listen_ending_before_cancelling_listen( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending during unloading before cancelling the listen task.""" + config_entry = integration + + # We can't easily simulate the race condition where the listen task ends + # before getting cancelled by the config entry during unloading. + # Use mock_state to provoke the correct condition. + config_entry.mock_state(hass, ConfigEntryState.UNLOAD_IN_PROGRESS, None) + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS + assert not any(record.levelno == logging.ERROR for record in caplog.records) + + +async def test_listen_ending_unrecoverable_config_entry_state( + hass: HomeAssistant, + integration: MockConfigEntry, + listen_block: asyncio.Event, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test listen ending when the config entry has an unrecoverable state.""" + config_entry = integration + + with patch.object( + hass.config_entries, "async_unload_platforms", return_value=False + ): + await hass.config_entries.async_unload(config_entry.entry_id) + + listen_block.set() + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.FAILED_UNLOAD + assert "Disconnected from server. Cannot recover entry" in caplog.text + + @pytest.mark.usefixtures("client") @pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) async def test_new_entity_on_value_added( @@ -2204,3 +2262,38 @@ async def test_entity_available_when_node_dead( state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state assert state.state != STATE_UNAVAILABLE + + +async def test_driver_ready_event( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test receiving a driver ready event.""" + config_entry = integration + assert config_entry.state is ConfigEntryState.LOADED + + config_entry_state_changes: list[ConfigEntryState] = [] + + def on_config_entry_state_change() -> None: + """Collect config entry state changes.""" + config_entry_state_changes.append(config_entry.state) + + config_entry.async_on_state_change(on_config_entry_state_change) + + driver_ready = Event( + type="driver ready", + data={ + "source": "driver", + "event": "driver ready", + }, + ) + + client.driver.receive_event(driver_ready) + await hass.async_block_till_done() + + assert len(config_entry_state_changes) == 4 + assert config_entry_state_changes[0] == ConfigEntryState.UNLOAD_IN_PROGRESS + assert config_entry_state_changes[1] == ConfigEntryState.NOT_LOADED + assert config_entry_state_changes[2] == ConfigEntryState.SETUP_IN_PROGRESS + assert config_entry_state_changes[3] == ConfigEntryState.LOADED diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 17f154f4f78..fbe0a8bbea7 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -1,12 +1,17 @@ """Test the Z-Wave JS update entities.""" import asyncio +from copy import deepcopy from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand +from zwave_js_server.model.driver.firmware import DriverFirmwareUpdateStatus +from zwave_js_server.model.node import Node from zwave_js_server.model.node.firmware import NodeFirmwareUpdateStatus from homeassistant.components.update import ( @@ -22,11 +27,16 @@ from homeassistant.components.update import ( SERVICE_SKIP, ) from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE -from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from tests.common import ( @@ -37,7 +47,8 @@ from tests.common import ( ) from tests.typing import WebSocketGenerator -UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +NODE_UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +CONTROLLER_UPDATE_ENTITY = "update.z_stick_gen5_usb_controller_firmware" LATEST_VERSION_FIRMWARE = { "version": "11.2.4", "changelog": "blah 2", @@ -112,26 +123,54 @@ FIRMWARE_UPDATES = { } +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.UPDATE] + + +@pytest.fixture(name="controller_state", autouse=True) +def controller_state_fixture( + controller_state: dict[str, Any], +) -> dict[str, Any]: + """Load the controller state fixture data.""" + controller_state = deepcopy(controller_state) + # Set the minimum SDK version that supports firmware updates for controllers. + controller_state["controller"]["sdkVersion"] = "6.50.0" + return controller_state + + +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_states( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, caplog: pytest.LogCaptureFixture, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity states.""" ws_client = await hass_ws_client(hass) - assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + assert client.driver.controller.sdk_version == "6.50.0" + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -139,7 +178,7 @@ async def test_update_entity_states( { "id": 1, "type": "update/release_notes", - "entity_id": UPDATE_ENTITY, + "entity_id": entity_id, } ) result = await ws_client.receive_json() @@ -150,12 +189,12 @@ async def test_update_entity_states( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes assert not attrs[ATTR_AUTO_UPDATE] - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert attrs[ATTR_RELEASE_URL] is None @@ -165,7 +204,7 @@ async def test_update_entity_states( { "id": 2, "type": "update/release_notes", - "entity_id": UPDATE_ENTITY, + "entity_id": entity_id, } ) result = await ws_client.receive_json() @@ -176,7 +215,7 @@ async def test_update_entity_states( DOMAIN, SERVICE_REFRESH_VALUE, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) @@ -188,31 +227,21 @@ async def test_update_entity_states( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF - # Assert a node firmware update entity is not created for the controller - driver = client.driver - node = driver.controller.nodes[1] - assert node.is_controller_node - assert ( - entity_registry.async_get_entity_id( - DOMAIN, - "sensor", - f"{get_valueless_base_unique_id(driver, node)}.firmware_update", - ) - is None - ) - - client.async_send_command.reset_mock() - +@pytest.mark.parametrize( + "entity_id", + [CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY], +) async def test_update_entity_install_raises( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, ) -> None: """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES @@ -228,7 +257,7 @@ async def test_update_entity_install_raises( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) @@ -236,9 +265,9 @@ async def test_update_entity_install_raises( async def test_update_entity_sleep( hass: HomeAssistant, - client, - zen_31, - integration, + client: MagicMock, + zen_31: Node, + integration: MockConfigEntry, ) -> None: """Test update occurs when device is asleep after it wakes up.""" event = Event( @@ -253,8 +282,15 @@ async def test_update_entity_sleep( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - # Because node is asleep we shouldn't attempt to check for firmware updates - assert len(client.async_send_command.call_args_list) == 0 + # Two nodes in total, the controller node and the zen_31 node. + # The zen_31 node is asleep, + # so we should only check for updates for the controller node. + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == 1 + + client.async_send_command.reset_mock() event = Event( "wake up", @@ -263,19 +299,20 @@ async def test_update_entity_sleep( zen_31.receive_event(event) await hass.async_block_till_done() - # Now that the node is up we can check for updates - assert len(client.async_send_command.call_args_list) > 0 - - args = client.async_send_command.call_args_list[0][0][0] + # Now that the zen_31 node is awake we can check for updates for it. + # The controller node has already been checked, + # so won't get another check now. + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + assert args["nodeId"] == 94 async def test_update_entity_dead( hass: HomeAssistant, - client, - zen_31, - integration, + client: MagicMock, + zen_31: Node, + integration: MockConfigEntry, ) -> None: """Test update occurs even when device is dead.""" event = Event( @@ -290,18 +327,24 @@ async def test_update_entity_dead( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - # Checking for firmware updates should proceed even for dead nodes - assert len(client.async_send_command.call_args_list) > 0 + # Two nodes in total, the controller node and the zen_31 node. + # Checking for firmware updates should proceed even for dead nodes. + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] + ) - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + node_ids = (1, 94) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id async def test_update_entity_ha_not_running( hass: HomeAssistant, - client, - zen_31, + client: MagicMock, + zen_31: Node, hass_ws_client: WebSocketGenerator, ) -> None: """Test update occurs only after HA is running.""" @@ -314,81 +357,170 @@ async def test_update_entity_ha_not_running( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + client.async_send_command.reset_mock() + assert client.async_send_command.call_count == 0 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + assert client.async_send_command.call_count == 0 - # Update should be delayed by a day because HA is not running + # Update should be delayed by a day because Home Assistant is not running hass.set_state(CoreState.starting) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 4 + assert client.async_send_command.call_count == 0 hass.set_state(CoreState.running) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 5 - args = client.async_send_command.call_args_list[4][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + # Two nodes in total, the controller node and the zen_31 node. + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] + ) + + node_ids = (1, 94) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id async def test_update_entity_update_failure( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, ) -> None: """Test update entity update failed.""" - assert len(client.async_send_command.call_args_list) == 0 + assert client.async_send_command.call_count == 0 client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) - assert state - assert state.state == STATE_OFF - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.get_available_firmware_updates" - assert ( - args["nodeId"] - == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + assert client.async_send_command.call_count == 2 + calls = sorted( + client.async_send_command.call_args_list, key=lambda call: call[0][0]["nodeId"] ) + node_ids = (1, 26) + for node_id, call in zip(node_ids, calls, strict=True): + args = call[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == node_id + +@pytest.mark.parametrize( + ( + "entity_id", + "installed_version", + "install_result", + "progress_event", + "finished_event", + ), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 255, "success": True}, + Event( + type="firmware update progress", + data={ + "source": "driver", + "event": "firmware update progress", + "progress": { + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "driver", + "event": "firmware update finished", + "result": { + "status": DriverFirmwareUpdateStatus.OK, + "success": True, + }, + }, + ), + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": 254, "success": True, "reInterview": False}, + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": 26, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": 26, + "result": { + "status": NodeFirmwareUpdateStatus.OK_NO_RESTART, + "success": True, + "reInterview": False, + }, + }, + ), + ), + ], +) async def test_update_entity_progress( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + progress_event: Event, + finished_event: Event, ) -> None: """Test update entity progress.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints client.async_send_command.return_value = FIRMWARE_UPDATES + driver = client.driver async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "result": {"status": 2, "success": False, "reInterview": False} - } + client.async_send_command.return_value = {"result": install_result} # Test successful install call without a version install_task = hass.async_create_task( @@ -396,64 +528,36 @@ async def test_update_entity_progress( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] is None - event = Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "progress": { - "currentFile": 1, - "totalFiles": 1, - "sentFragments": 1, - "totalFragments": 20, - "progress": 5.0, - }, - }, - ) - node.receive_event(event) + driver.receive_event(progress_event) + await asyncio.sleep(0.05) # Validate that the progress is updated - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] == 5 - event = Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "result": { - "status": NodeFirmwareUpdateStatus.OK_NO_RESTART, - "success": True, - "reInterview": False, - }, - }, - ) - - node.receive_event(event) + driver.receive_event(finished_event) await hass.async_block_till_done() # Validate that progress is reset and entity reflects new version - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is False @@ -465,31 +569,106 @@ async def test_update_entity_progress( await install_task +@pytest.mark.parametrize( + ( + "entity_id", + "installed_version", + "install_result", + "progress_event", + "finished_event", + ), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 0, "success": False}, + Event( + type="firmware update progress", + data={ + "source": "driver", + "event": "firmware update progress", + "progress": { + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "driver", + "event": "firmware update finished", + "result": { + "status": DriverFirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + }, + }, + ), + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": -1, "success": False, "reInterview": False}, + Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": 26, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, + }, + ), + Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": 26, + "result": { + "status": NodeFirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + "reInterview": False, + }, + }, + ), + ), + ], +) async def test_update_entity_install_failed( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, caplog: pytest.LogCaptureFixture, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + progress_event: Event, + finished_event: Event, ) -> None: """Test update entity install returns error status.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints + driver = client.driver client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "result": {"status": 2, "success": False, "reInterview": False} - } + client.async_send_command.return_value = {"result": install_result} # Test install call - we expect it to finish fail install_task = hass.async_create_task( @@ -497,63 +676,35 @@ async def test_update_entity_install_failed( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - event = Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "progress": { - "currentFile": 1, - "totalFiles": 1, - "sentFragments": 1, - "totalFragments": 20, - "progress": 5.0, - }, - }, - ) - node.receive_event(event) + driver.receive_event(progress_event) + await asyncio.sleep(0.05) # Validate that the progress is updated - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] == 5 - event = Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "result": { - "status": NodeFirmwareUpdateStatus.ERROR_TIMEOUT, - "success": False, - "reInterview": False, - }, - }, - ) - - node.receive_event(event) + driver.receive_event(finished_event) await hass.async_block_till_done() # Validate that progress is reset and entity reflects old version - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_UPDATE_PERCENTAGE] is None - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON @@ -562,21 +713,30 @@ async def test_update_entity_install_failed( await install_task +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_reload( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, + integration: MockConfigEntry, + entity_id: str, + installed_version: str, ) -> None: """Test update entity maintains state after reload.""" - assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + config_entry = integration + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -585,12 +745,12 @@ async def test_update_entity_reload( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON attrs = state.attributes assert not attrs[ATTR_AUTO_UPDATE] - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert attrs[ATTR_INSTALLED_VERSION] == installed_version assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_UPDATE_PERCENTAGE] is None assert attrs[ATTR_LATEST_VERSION] == "11.2.4" @@ -600,24 +760,24 @@ async def test_update_entity_reload( UPDATE_DOMAIN, SERVICE_SKIP, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" - await hass.config_entries.async_reload(integration.entry_id) + await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() # Trigger another update and make sure the skipped version is still skipped async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4)) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" @@ -625,9 +785,9 @@ async def test_update_entity_reload( async def test_update_entity_delay( hass: HomeAssistant, - client, - ge_in_wall_dimmer_switch, - zen_31, + client: MagicMock, + ge_in_wall_dimmer_switch: Node, + zen_31: Node, hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, ) -> None: @@ -641,12 +801,13 @@ async def test_update_entity_delay( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 6 + client.async_send_command.reset_mock() + assert client.async_send_command.call_count == 0 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 6 + assert client.async_send_command.call_count == 0 update_interval = timedelta(minutes=5) freezer.tick(update_interval) @@ -655,8 +816,8 @@ async def test_update_entity_delay( nodes: set[int] = set() - assert len(client.async_send_command.call_args_list) == 7 - args = client.async_send_command.call_args_list[6][0][0] + assert client.async_send_command.call_count == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" nodes.add(args["nodeId"]) @@ -664,30 +825,45 @@ async def test_update_entity_delay( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 8 - args = client.async_send_command.call_args_list[7][0][0] + assert client.async_send_command.call_count == 2 + args = client.async_send_command.call_args[0][0] assert args["command"] == "controller.get_available_firmware_updates" nodes.add(args["nodeId"]) - assert len(nodes) == 2 - assert nodes == {ge_in_wall_dimmer_switch.node_id, zen_31.node_id} + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert client.async_send_command.call_count == 3 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "controller.get_available_firmware_updates" + nodes.add(args["nodeId"]) + + assert len(nodes) == 3 + assert nodes == {1, ge_in_wall_dimmer_switch.node_id, zen_31.node_id} +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_partial_restore_data( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity with partial restore data resets state.""" mock_restore_cache( hass, [ State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: "11.2.4", }, @@ -699,16 +875,22 @@ async def test_update_entity_partial_restore_data( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_partial_restore_data_2( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test second scenario where update entity has partial restore data.""" mock_restore_cache_with_extra_data( @@ -716,10 +898,10 @@ async def test_update_entity_partial_restore_data_2( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_ON, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "10.8", ATTR_SKIPPED_VERSION: None, }, @@ -733,18 +915,24 @@ async def test_update_entity_partial_restore_data_2( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN assert state.attributes[ATTR_SKIPPED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] is None +@pytest.mark.parametrize( + ("entity_id", "installed_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2"), (NODE_UPDATE_ENTITY, "10.7")], +) async def test_update_entity_full_restore_data_skipped_version( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, ) -> None: """Test update entity with full restore data (skipped version) restores state.""" mock_restore_cache_with_extra_data( @@ -752,10 +940,10 @@ async def test_update_entity_full_restore_data_skipped_version( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: "11.2.4", }, @@ -769,18 +957,44 @@ async def test_update_entity_full_restore_data_skipped_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" assert state.attributes[ATTR_LATEST_VERSION] == "11.2.4" +@pytest.mark.parametrize( + ("entity_id", "installed_version", "install_result", "install_command_params"), + [ + ( + CONTROLLER_UPDATE_ENTITY, + "1.2", + {"status": 255, "success": True}, + { + "command": "driver.firmware_update_otw", + }, + ), + ( + NODE_UPDATE_ENTITY, + "10.7", + {"status": 255, "success": True, "reInterview": False}, + { + "command": "controller.firmware_update_ota", + "nodeId": 26, + }, + ), + ], +) async def test_update_entity_full_restore_data_update_available( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + install_result: dict[str, Any], + install_command_params: dict[str, Any], ) -> None: """Test update entity with full restore data (update available) restores state.""" mock_restore_cache_with_extra_data( @@ -788,10 +1002,10 @@ async def test_update_entity_full_restore_data_update_available( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: "11.2.4", ATTR_SKIPPED_VERSION: None, }, @@ -805,15 +1019,14 @@ async def test_update_entity_full_restore_data_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON assert state.attributes[ATTR_SKIPPED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] == "11.2.4" - client.async_send_command.return_value = { - "result": {"status": 255, "success": True, "reInterview": False} - } + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"result": install_result} # Test successful install call without a version install_task = hass.async_create_task( @@ -821,25 +1034,24 @@ async def test_update_entity_full_restore_data_update_available( UPDATE_DOMAIN, SERVICE_INSTALL, { - ATTR_ENTITY_ID: UPDATE_ENTITY, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) ) # Sleep so that task starts - await asyncio.sleep(0.1) + await asyncio.sleep(0.05) - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True assert attrs[ATTR_UPDATE_PERCENTAGE] is None - assert len(client.async_send_command.call_args_list) == 5 - assert client.async_send_command.call_args_list[4][0][0] == { - "command": "controller.firmware_update_ota", - "nodeId": climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + assert client.async_send_command.call_count == 1 + assert client.async_send_command.call_args[0][0] == { + **install_command_params, "updateInfo": { "version": "11.2.4", "changelog": "blah 2", @@ -862,11 +1074,18 @@ async def test_update_entity_full_restore_data_update_available( install_task.cancel() +@pytest.mark.parametrize( + ("entity_id", "installed_version", "latest_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2", "1.2"), (NODE_UPDATE_ENTITY, "10.7", "10.7")], +) async def test_update_entity_full_restore_data_no_update_available( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + latest_version: str, ) -> None: """Test entity with full restore data (no update available) restores state.""" mock_restore_cache_with_extra_data( @@ -874,11 +1093,11 @@ async def test_update_entity_full_restore_data_no_update_available( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", - ATTR_LATEST_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, + ATTR_LATEST_VERSION: latest_version, ATTR_SKIPPED_VERSION: None, }, ), @@ -891,18 +1110,25 @@ async def test_update_entity_full_restore_data_no_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] is None - assert state.attributes[ATTR_LATEST_VERSION] == "10.7" + assert state.attributes[ATTR_LATEST_VERSION] == latest_version +@pytest.mark.parametrize( + ("entity_id", "installed_version", "latest_version"), + [(CONTROLLER_UPDATE_ENTITY, "1.2", "1.2"), (NODE_UPDATE_ENTITY, "10.7", "10.7")], +) async def test_update_entity_no_latest_version( hass: HomeAssistant, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, + client: MagicMock, + climate_radio_thermostat_ct100_plus_different_endpoints: Node, hass_ws_client: WebSocketGenerator, + entity_id: str, + installed_version: str, + latest_version: str, ) -> None: """Test entity with no `latest_version` attr restores state.""" mock_restore_cache_with_extra_data( @@ -910,10 +1136,10 @@ async def test_update_entity_no_latest_version( [ ( State( - UPDATE_ENTITY, + entity_id, STATE_OFF, { - ATTR_INSTALLED_VERSION: "10.7", + ATTR_INSTALLED_VERSION: installed_version, ATTR_LATEST_VERSION: None, ATTR_SKIPPED_VERSION: None, }, @@ -927,24 +1153,33 @@ async def test_update_entity_no_latest_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(UPDATE_ENTITY) + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_SKIPPED_VERSION] is None - assert state.attributes[ATTR_LATEST_VERSION] == "10.7" + assert state.attributes[ATTR_LATEST_VERSION] == latest_version async def test_update_entity_unload_asleep_node( - hass: HomeAssistant, client, wallmote_central_scene, integration + hass: HomeAssistant, + client: MagicMock, + wallmote_central_scene: Node, + integration: MockConfigEntry, ) -> None: """Test unloading config entry after attempting an update for an asleep node.""" - assert len(client.async_send_command.call_args_list) == 0 + config_entry = integration + assert client.async_send_command.call_count == 0 + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"updates": []} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 - assert len(wallmote_central_scene._listeners["wake up"]) == 2 + # Once call completed for the (awake) controller node. + assert client.async_send_command.call_count == 1 + assert len(wallmote_central_scene._listeners["wake up"]) == 1 - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) + assert client.async_send_command.call_count == 1 assert len(wallmote_central_scene._listeners["wake up"]) == 0 diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index b9259596c65..dcd35a3aca7 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -1,11 +1,17 @@ """Tests for hassfest requirements.""" from pathlib import Path +from unittest.mock import patch import pytest from script.hassfest.model import Config, Integration -from script.hassfest.requirements import validate_requirements_format +from script.hassfest.requirements import ( + PACKAGE_CHECK_PREPARE_UPDATE, + PACKAGE_CHECK_VERSION_RANGE, + check_dependency_version_range, + validate_requirements_format, +) @pytest.fixture @@ -105,3 +111,41 @@ def test_validate_requirements_format_github_custom(integration: Integration) -> integration.path = Path("") assert validate_requirements_format(integration) assert len(integration.errors) == 0 + + +@pytest.mark.parametrize( + ("version", "result"), + [ + (">2", True), + (">=2.0", True), + (">=2.0,<4", True), + ("<4", True), + ("<=3.0", True), + (">=2.0,<4;python_version<'3.14'", True), + ("<3", False), + ("==2.*", False), + ("~=2.0", False), + ("<=2.100", False), + (">2,<3", False), + (">=2.0,<3", False), + (">=2.0,<3;python_version<'3.14'", False), + ], +) +def test_dependency_version_range_prepare_update( + version: str, result: bool, integration: Integration +) -> None: + """Test dependency version range check for prepare update is working correctly.""" + with ( + patch.dict(PACKAGE_CHECK_VERSION_RANGE, {"numpy-test": "SemVer"}, clear=True), + patch.dict(PACKAGE_CHECK_PREPARE_UPDATE, {"numpy-test": 3}, clear=True), + ): + assert ( + check_dependency_version_range( + integration, + "test", + pkg="numpy-test", + version=version, + package_exceptions=set(), + ) + == result + ) diff --git a/tests/helpers/snapshots/test_entity_platform.ambr b/tests/helpers/snapshots/test_entity_platform.ambr index 55ff772e08e..2da81a95602 100644 --- a/tests/helpers/snapshots/test_entity_platform.ambr +++ b/tests/helpers/snapshots/test_entity_platform.ambr @@ -21,7 +21,6 @@ '1234', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'test-manuf', @@ -31,7 +30,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) @@ -58,7 +56,6 @@ 'efgh', ), }), - 'is_new': False, 'labels': set({ }), 'manufacturer': 'test-manuf', @@ -68,7 +65,6 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'suggested_area': 'Heliport', 'sw_version': 'test-sw', 'via_device_id': , }) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 23a451dd06c..d056c25fc3b 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -107,7 +107,6 @@ async def test_get_or_create_returns_same_entry( assert entry3.model == "model" assert entry3.name == "name" assert entry3.sw_version == "sw-version" - assert entry3.suggested_area == "Game Room" assert entry3.area_id == game_room_area.id await hass.async_block_till_done() @@ -409,7 +408,6 @@ async def test_loading_from_storage( name="name", primary_config_entry=mock_config_entry.entry_id, serial_number="serial_no", - suggested_area=None, # Not stored sw_version="version", ) assert isinstance(entry.config_entries, set) @@ -1652,7 +1650,7 @@ async def test_removing_config_entries( assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -1725,12 +1723,12 @@ async def test_deleted_device_removing_config_entries( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[4].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } device_registry.async_clear_config_entry(config_entry_1.entry_id) @@ -1976,7 +1974,7 @@ async def test_removing_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } @@ -2106,7 +2104,7 @@ async def test_deleted_device_removing_config_subentries( assert update_events[4].data == { "action": "remove", "device_id": entry.id, - "device": entry4, + "device": entry4.dict_repr, } device_registry.async_clear_config_subentry(config_entry_1.entry_id, None) @@ -2509,13 +2507,13 @@ async def test_loading_saving_data( # Ensure a save/load cycle does not keep suggested area new_kitchen_light = registry2.async_get_device(identifiers={("hue", "999")}) - assert orig_kitchen_light.suggested_area == "Kitchen" + assert orig_kitchen_light.area_id == "kitchen" - orig_kitchen_light_witout_suggested_area = device_registry.async_update_device( + orig_kitchen_light_without_suggested_area = device_registry.async_update_device( orig_kitchen_light.id, suggested_area=None ) - assert orig_kitchen_light_witout_suggested_area.suggested_area is None - assert orig_kitchen_light_witout_suggested_area == new_kitchen_light + assert orig_kitchen_light_without_suggested_area.area_id == "kitchen" + assert orig_kitchen_light_without_suggested_area == new_kitchen_light async def test_no_unnecessary_changes( @@ -2930,7 +2928,7 @@ async def test_update_remove_config_entries( assert update_events[6].data == { "action": "remove", "device_id": entry3.id, - "device": entry3, + "device": entry3.dict_repr, } @@ -3208,25 +3206,39 @@ async def test_update_remove_config_subentries( assert update_events[7].data == { "action": "remove", "device_id": entry_id, - "device": entry_before_remove, + "device": entry_before_remove.dict_repr, } +@pytest.mark.parametrize( + ("initial_area", "device_area_id", "number_of_areas"), + [ + (None, None, 0), + ("Living Room", "living_room", 1), + ], +) async def test_update_suggested_area( hass: HomeAssistant, device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry, mock_config_entry: MockConfigEntry, + initial_area: str | None, + device_area_id: str | None, + number_of_areas: int, ) -> None: - """Verify that we can update the suggested area version of a device.""" + """Verify that we can update the suggested area of a device. + + Updating the suggested area of a device should not create a new area, nor should + it change the area_id of the device. + """ update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, + suggested_area=initial_area, ) - assert not entry.suggested_area - assert entry.area_id is None + assert entry.area_id == device_area_id suggested_area = "Pool" @@ -3235,27 +3247,24 @@ async def test_update_suggested_area( entry.id, suggested_area=suggested_area ) - assert mock_save.call_count == 1 + # Check the device registry was not saved + assert mock_save.call_count == 0 assert updated_entry != entry - assert updated_entry.suggested_area == suggested_area + assert updated_entry.area_id == device_area_id - pool_area = area_registry.async_get_area_by_name("Pool") - assert pool_area is not None - assert updated_entry.area_id == pool_area.id - assert len(area_registry.areas) == 1 + # Check we did not create an area + pool_area = area_registry.async_get_area_by_name(suggested_area) + assert pool_area is None + assert updated_entry.area_id == device_area_id + assert len(area_registry.areas) == number_of_areas await hass.async_block_till_done() - assert len(update_events) == 2 + assert len(update_events) == 1 assert update_events[0].data == { "action": "create", "device_id": entry.id, } - assert update_events[1].data == { - "action": "update", - "device_id": entry.id, - "changes": {"area_id": None, "suggested_area": None}, - } # Do not save or fire the event if the suggested # area does not result in a change of area @@ -3264,10 +3273,10 @@ async def test_update_suggested_area( updated_entry = device_registry.async_update_device( entry.id, suggested_area="Other" ) - assert len(update_events) == 2 + assert len(update_events) == 1 assert mock_save_2.call_count == 0 assert updated_entry != entry - assert updated_entry.suggested_area == "Other" + assert updated_entry.area_id == device_area_id async def test_cleanup_device_registry( @@ -3401,11 +3410,13 @@ async def test_cleanup_entity_registry_change( assert len(mock_call.mock_calls) == 2 +@pytest.mark.parametrize("initial_area", [None, "12345A"]) @pytest.mark.usefixtures("freezer") async def test_restore_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry_with_subentries: MockConfigEntry, + initial_area: str | None, ) -> None: """Make sure device id is stable.""" entry_id = mock_config_entry_with_subentries.entry_id @@ -3432,7 +3443,7 @@ async def test_restore_device( # Apply user customizations entry = device_registry.async_update_device( entry.id, - area_id="12345A", + area_id=initial_area, disabled_by=dr.DeviceEntryDisabler.USER, labels={"label1", "label2"}, name_by_user="Test Friendly Name", @@ -3475,7 +3486,6 @@ async def test_restore_device( name=None, primary_config_entry=entry_id, serial_number=None, - suggested_area=None, sw_version=None, ) # This will restore the original device, user customizations of @@ -3498,7 +3508,7 @@ async def test_restore_device( via_device="via_device_id_new", ) assert entry3 == dr.DeviceEntry( - area_id="12345A", + area_id=initial_area, config_entries={entry_id}, config_entries_subentries={entry_id: {subentry_id}}, configuration_url="http://config_url_new.bla", @@ -3551,7 +3561,7 @@ async def test_restore_device( assert update_events[2].data == { "action": "remove", "device_id": entry.id, - "device": entry, + "device": entry.dict_repr, } assert update_events[3].data == { "action": "create", @@ -3874,7 +3884,7 @@ async def test_restore_shared_device( assert update_events[3].data == { "action": "remove", "device_id": entry.id, - "device": updated_device, + "device": updated_device.dict_repr, } assert update_events[4].data == { "action": "create", @@ -3883,7 +3893,7 @@ async def test_restore_shared_device( assert update_events[5].data == { "action": "remove", "device_id": entry.id, - "device": entry2, + "device": entry2.dict_repr, } assert update_events[6].data == { "action": "create", @@ -4905,3 +4915,44 @@ async def test_connections_validator() -> None: """Test checking connections validator.""" with pytest.raises(ValueError, match="Invalid mac address format"): dr.DeviceEntry(connections={(dr.CONNECTION_NETWORK_MAC, "123456ABCDEF")}) + + +async def test_suggested_area_deprecation( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Make sure we do not duplicate entries.""" + entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + + game_room_area = area_registry.async_get_area_by_name("Game Room") + assert game_room_area is not None + assert len(area_registry.areas) == 1 + + assert len(device_registry.devices) == 1 + assert entry.area_id == game_room_area.id + assert entry.suggested_area == "Game Room" + + assert ( + "The deprecated function suggested_area was called. It will be removed in " + "HA Core 2026.9. Use code which ignores suggested_area instead" + ) in caplog.text + + device_registry.async_update_device(entry.id, suggested_area="TV Room") + + assert ( + "Detected code that passes a suggested_area to device_registry.async_update " + "device. This will stop working in Home Assistant 2026.9.0, please report " + "this issue" + ) in caplog.text diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index ba9db9cb053..050420d0195 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -569,7 +569,15 @@ async def test_async_get_all_descriptions( ) -> None: """Test async_get_all_descriptions.""" tag_trigger_descriptions = """ - tag: {} + tag: + fields: + entity: + selector: + entity: + filter: + domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME """ assert await async_setup_component(hass, DOMAIN_SUN, {}) @@ -611,9 +619,16 @@ async def test_async_get_all_descriptions( "fields": { "event": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "offset": {"selector": {"time": None}}, + "offset": {"selector": {"time": {}}}, } } } @@ -639,13 +654,35 @@ async def test_async_get_all_descriptions( "fields": { "event": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "offset": {"selector": {"time": None}}, + "offset": {"selector": {"time": {}}}, } }, DOMAIN_TAG: { - "fields": {}, + "fields": { + "entity": { + "selector": { + "entity": { + "filter": [ + { + "domain": ["alarm_control_panel"], + "supported_features": [1], + } + ], + "multiple": False, + "reorder": False, + }, + }, + }, + } }, } diff --git a/tests/syrupy.py b/tests/syrupy.py index e028d5839cb..919ba1a6cea 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -173,6 +173,9 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): if serialized["primary_config_entry"] is not None: serialized["primary_config_entry"] = ANY serialized.pop("_cache") + # This can be removed when suggested_area is removed from DeviceEntry + serialized.pop("_suggested_area") + serialized.pop("is_new") return cls._remove_created_and_modified_at(serialized) @classmethod diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index a5908f0feab..0faa4dd1a80 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -135,6 +135,19 @@ async def test_show_form(manager: MockFlowManager) -> None: async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) -> None: """Test that we can show a form with suggested values.""" + + def compare_schemas(schema: vol.Schema, expected_schema: vol.Schema) -> None: + """Compare two schemas.""" + assert schema.schema is not expected_schema.schema + + assert list(schema.schema) == list(expected_schema.schema) + + for key, validator in schema.schema.items(): + if isinstance(validator, data_entry_flow.section): + assert validator.schema == expected_schema.schema[key].schema + continue + assert validator == expected_schema.schema[key] + schema = vol.Schema( { vol.Required("username"): str, @@ -155,20 +168,25 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) async def async_step_init(self, user_input=None): data_schema = self.add_suggested_values_to_schema( schema, - { - "username": "doej", - "password": "verySecret1", - "section_1": {"full_name": "John Doe"}, - }, + user_input, ) return self.async_show_form( step_id="init", data_schema=data_schema, ) - form = await manager.async_init("test") + form = await manager.async_init( + "test", + data={ + "username": "doej", + "password": "verySecret1", + "section_1": {"full_name": "John Doe"}, + }, + ) assert form["type"] == data_entry_flow.FlowResultType.FORM - assert form["data_schema"].schema == schema.schema + assert form["data_schema"].schema is not schema.schema + assert form["data_schema"].schema != schema.schema + compare_schemas(form["data_schema"], schema) markers = list(form["data_schema"].schema) assert len(markers) == 3 assert markers[0] == "username" @@ -178,15 +196,41 @@ async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) assert markers[2] == "section_1" section_validator = form["data_schema"].schema["section_1"] assert isinstance(section_validator, data_entry_flow.section) - # The section class was not replaced - assert section_validator is schema.schema["section_1"] - # The section schema was not replaced - assert section_validator.schema is schema.schema["section_1"].schema + # The section instance was copied + assert section_validator is not schema.schema["section_1"] + # The section schema instance was copied + assert section_validator.schema is not schema.schema["section_1"].schema + assert section_validator.schema == schema.schema["section_1"].schema section_markers = list(section_validator.schema.schema) assert len(section_markers) == 1 assert section_markers[0] == "full_name" assert section_markers[0].description == {"suggested_value": "John Doe"} + # Test again without suggested values to make sure we're not mutating the schema + form = await manager.async_init( + "test", + ) + assert form["type"] == data_entry_flow.FlowResultType.FORM + assert form["data_schema"].schema is not schema.schema + assert form["data_schema"].schema == schema.schema + markers = list(form["data_schema"].schema) + assert len(markers) == 3 + assert markers[0] == "username" + assert markers[0].description is None + assert markers[1] == "password" + assert markers[1].description is None + assert markers[2] == "section_1" + section_validator = form["data_schema"].schema["section_1"] + assert isinstance(section_validator, data_entry_flow.section) + # The section class is not replaced if there is no suggested value for the section + assert section_validator is schema.schema["section_1"] + # The section schema is not replaced if there is no suggested value for the section + assert section_validator.schema is schema.schema["section_1"].schema + section_markers = list(section_validator.schema.schema) + assert len(section_markers) == 1 + assert section_markers[0] == "full_name" + assert section_markers[0].description is None + async def test_abort_removes_instance(manager: MockFlowManager) -> None: """Test that abort removes the flow from progress.""" diff --git a/tests/util/test_resource.py b/tests/util/test_resource.py new file mode 100644 index 00000000000..a32ceb1062c --- /dev/null +++ b/tests/util/test_resource.py @@ -0,0 +1,153 @@ +"""Test the resource utility module.""" + +import os +import resource +from unittest.mock import call, patch + +import pytest + +from homeassistant.util.resource import ( + DEFAULT_SOFT_FILE_LIMIT, + set_open_file_descriptor_limit, +) + + +@pytest.mark.parametrize( + ("original_soft", "expected_calls", "should_log_already_sufficient"), + [ + ( + 1024, + [call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))], + False, + ), + ( + DEFAULT_SOFT_FILE_LIMIT - 1, + [call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))], + False, + ), + (DEFAULT_SOFT_FILE_LIMIT, [], True), + (DEFAULT_SOFT_FILE_LIMIT + 1, [], True), + ], +) +def test_set_open_file_descriptor_limit_default( + caplog: pytest.LogCaptureFixture, + original_soft: int, + expected_calls: list, + should_log_already_sufficient: bool, +) -> None: + """Test setting file limit with default value.""" + original_hard = 524288 + with ( + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + assert mock_setrlimit.call_args_list == expected_calls + assert ( + f"Current soft limit ({original_soft}) is already" in caplog.text + ) is should_log_already_sufficient + + +@pytest.mark.parametrize( + ( + "original_soft", + "custom_limit", + "expected_calls", + "should_log_already_sufficient", + ), + [ + (1499, 1500, [call(resource.RLIMIT_NOFILE, (1500, 524288))], False), + (1500, 1500, [], True), + (1501, 1500, [], True), + ], +) +def test_set_open_file_descriptor_limit_environment_variable( + caplog: pytest.LogCaptureFixture, + original_soft: int, + custom_limit: int, + expected_calls: list, + should_log_already_sufficient: bool, +) -> None: + """Test setting file limit from environment variable.""" + original_hard = 524288 + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(custom_limit)}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + assert mock_setrlimit.call_args_list == expected_calls + assert ( + f"Current soft limit ({original_soft}) is already" in caplog.text + ) is should_log_already_sufficient + + +def test_set_open_file_descriptor_limit_exceeds_hard_limit( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setting file limit that exceeds hard limit.""" + original_soft, original_hard = (1024, 524288) + excessive_limit = original_hard + 1 + + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(excessive_limit)}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(original_soft, original_hard), + ), + patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit, + ): + set_open_file_descriptor_limit() + + mock_setrlimit.assert_called_once_with( + resource.RLIMIT_NOFILE, (original_hard, original_hard) + ) + assert ( + f"Requested soft limit ({excessive_limit}) exceeds hard limit ({original_hard})" + in caplog.text + ) + + +def test_set_open_file_descriptor_limit_os_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling OSError when setting file limit.""" + with ( + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(1024, 524288), + ), + patch( + "homeassistant.util.resource.resource.setrlimit", + side_effect=OSError("Permission denied"), + ), + ): + set_open_file_descriptor_limit() + + assert "Failed to set file descriptor limit" in caplog.text + assert "Permission denied" in caplog.text + + +def test_set_open_file_descriptor_limit_value_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling ValueError when setting file limit.""" + with ( + patch.dict(os.environ, {"SOFT_FILE_LIMIT": "invalid_value"}), + patch( + "homeassistant.util.resource.resource.getrlimit", + return_value=(1024, 524288), + ), + ): + set_open_file_descriptor_limit() + + assert "Invalid file descriptor limit value" in caplog.text + assert "'invalid_value'" in caplog.text