diff --git a/homeassistant/components/agent_dvr/strings.json b/homeassistant/components/agent_dvr/strings.json index c5093a6f0982cc..1738dc54d2e0a1 100644 --- a/homeassistant/components/agent_dvr/strings.json +++ b/homeassistant/components/agent_dvr/strings.json @@ -4,7 +4,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "step": { diff --git a/homeassistant/components/aidot/strings.json b/homeassistant/components/aidot/strings.json index 9427624784530f..1d27987383a739 100644 --- a/homeassistant/components/aidot/strings.json +++ b/homeassistant/components/aidot/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index e3a1b848369ba4..94cb3b94793c22 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "Please ensure you reconfigure against the same device." diff --git a/homeassistant/components/airq/strings.json b/homeassistant/components/airq/strings.json index a8221f17dfcda9..98926534a190fa 100644 --- a/homeassistant/components/airq/strings.json +++ b/homeassistant/components/airq/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date." }, "error": { diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index 2b867f5265d5d4..ccc467160c1738 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index be4e58edde143f..33f762d56ff271 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", diff --git a/homeassistant/components/amberelectric/helpers.py b/homeassistant/components/amberelectric/helpers.py index 3ce85ea29bcf33..9b26ab542bb5b3 100644 --- a/homeassistant/components/amberelectric/helpers.py +++ b/homeassistant/components/amberelectric/helpers.py @@ -24,5 +24,5 @@ def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None: def format_cents_to_dollars(cents: float) -> float: - """Return a formatted conversion from cents to dollars.""" - return round(cents / 100, 2) + """Convert cents per kWh to dollars per kWh.""" + return cents / 100 diff --git a/homeassistant/components/androidtv_remote/strings.json b/homeassistant/components/androidtv_remote/strings.json index e71f0ed7644b75..8f27e64ca29984 100644 --- a/homeassistant/components/androidtv_remote/strings.json +++ b/homeassistant/components/androidtv_remote/strings.json @@ -8,7 +8,6 @@ "unique_id_mismatch": "Please ensure you reconfigure against the same device." }, "error": { - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json index b9f0d33055ce0c..fd752b6d3083a7 100644 --- a/homeassistant/components/arcam_fmj/strings.json +++ b/homeassistant/components/arcam_fmj/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "Please ensure you reconfigure against the same device." diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 6b79542b60a78a..535301e3c9f222 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_invalid_user": "Reauthenticate must use the same account.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" diff --git a/homeassistant/components/avea/strings.json b/homeassistant/components/avea/strings.json index 99d731b07d5dc8..91a7db51a3a14a 100644 --- a/homeassistant/components/avea/strings.json +++ b/homeassistant/components/avea/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "error": { diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index e9c92c15c08f38..67dc5ab7002de0 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -11,7 +11,6 @@ }, "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, diff --git a/homeassistant/components/backup/services.py b/homeassistant/components/backup/services.py index 192a2f3c171464..25cf0b19859c87 100644 --- a/homeassistant/components/backup/services.py +++ b/homeassistant/components/backup/services.py @@ -1,6 +1,6 @@ """The Backup integration.""" -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service import async_register_admin_service @@ -28,6 +28,7 @@ async def _async_handle_create_automatic_service(call: ServiceCall) -> None: await call.hass.data[DATA_MANAGER].async_create_automatic_backup() +@callback def async_setup_services(hass: HomeAssistant) -> None: """Register services.""" if not is_hassio(hass): diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index c209d057623ab2..62968cf5ddb9bc 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -12,8 +12,7 @@ }, "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "api_exception": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json index 599089047fc727..e6de948768fa77 100644 --- a/homeassistant/components/bluemaestro/strings.json +++ b/homeassistant/components/bluemaestro/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/bluesound/strings.json b/homeassistant/components/bluesound/strings.json index 7fa227cea20540..8a600d9e890e9e 100644 --- a/homeassistant/components/bluesound/strings.json +++ b/homeassistant/components/bluesound/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_ipv4_address": "No IPv4 address found." }, diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index d14ab49511186c..a5b488e8ef5378 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -10,7 +10,6 @@ "unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account." }, "error": { - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json index 4b0d0a3159548b..905da7e92ce541 100644 --- a/homeassistant/components/broadlink/strings.json +++ b/homeassistant/components/broadlink/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "not_supported": "Device not supported", diff --git a/homeassistant/components/bthome/strings.json b/homeassistant/components/bthome/strings.json index 4eaa7363f706dd..dfee8e5dce2bf4 100644 --- a/homeassistant/components/bthome/strings.json +++ b/homeassistant/components/bthome/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, diff --git a/homeassistant/components/casper_glow/strings.json b/homeassistant/components/casper_glow/strings.json index 35ce4c0afedc3b..bf57a428645e5c 100644 --- a/homeassistant/components/casper_glow/strings.json +++ b/homeassistant/components/casper_glow/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported", diff --git a/homeassistant/components/chef_iq/strings.json b/homeassistant/components/chef_iq/strings.json index a9bda88ad944d6..b7097580931f73 100644 --- a/homeassistant/components/chef_iq/strings.json +++ b/homeassistant/components/chef_iq/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/cielo_home/strings.json b/homeassistant/components/cielo_home/strings.json index a7dba147925042..937550bcfadb19 100644 --- a/homeassistant/components/cielo_home/strings.json +++ b/homeassistant/components/cielo_home/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 601f5df569dcbe..d76644c6bc2c55 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -416,8 +416,11 @@ def _main_hvac_mode(self) -> HVACMode: @property @override def hvac_modes(self) -> list[HVACMode]: - """Return the hvac modes (mirrors the main unit).""" - return [self._main_hvac_mode] + """Return the main unit HVAC mode and zone power off.""" + main_mode = self._main_hvac_mode + if main_mode == HVACMode.OFF: + return [HVACMode.OFF] + return [main_mode, HVACMode.OFF] @property @override @@ -549,7 +552,13 @@ async def async_toggle(self) -> None: @override async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Disallow changing HVAC mode via zone climate.""" + """Set zone power without changing the main unit HVAC mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + return + if hvac_mode == self._main_hvac_mode: + await self.async_turn_on() + return raise HomeAssistantError( translation_domain=DOMAIN, translation_key="zone_hvac_read_only", diff --git a/homeassistant/components/deako/strings.json b/homeassistant/components/deako/strings.json index 088e85a726d2e3..edd1785d55ea34 100644 --- a/homeassistant/components/deako/strings.json +++ b/homeassistant/components/deako/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Please confirm setting up the Deako integration" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 9789b4b464a0b4..7c18c2439d0481 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Bridge is already configured", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_bridges": "No deCONZ bridges discovered", "no_hardware_available": "No radio hardware connected to deCONZ", "updated_instance": "Updated deCONZ instance with new host address" diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index ddb38b9b5a6817..b5539321d7c7a6 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "Failed to connect, please try again, disconnecting mains power and Ethernet cables and reconnecting them may help", "not_denonavr_manufacturer": "Not a Denon AVR network receiver, discovered manufacturer did not match", "not_denonavr_missing": "Not a Denon AVR network receiver, discovery information not complete" diff --git a/homeassistant/components/dialogflow/strings.json b/homeassistant/components/dialogflow/strings.json index 74333f7690f7c6..c2fcc78bac0fc4 100644 --- a/homeassistant/components/dialogflow/strings.json +++ b/homeassistant/components/dialogflow/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to the [webhook service of Dialogflow]({dialogflow_url}) and update the webhook with following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/dlna_dms/strings.json b/homeassistant/components/dlna_dms/strings.json index 1e233c6c48dec1..e41a459c79ef96 100644 --- a/homeassistant/components/dlna_dms/strings.json +++ b/homeassistant/components/dlna_dms/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "bad_ssdp": "SSDP data is missing a required value", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_dms": "Device is not a supported Media Server" diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 0b34e6e0ba450b..da8816a052f320 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "directory_does_not_exist": "The directory could not be reached. Please check your settings." }, diff --git a/homeassistant/components/dropbox/strings.json b/homeassistant/components/dropbox/strings.json index ebe90c7e84fdd6..7026ec0ba11f08 100644 --- a/homeassistant/components/dropbox/strings.json +++ b/homeassistant/components/dropbox/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", diff --git a/homeassistant/components/dsmr_reader/strings.json b/homeassistant/components/dsmr_reader/strings.json index caeca6052c343a..ca091eefdc12b6 100644 --- a/homeassistant/components/dsmr_reader/strings.json +++ b/homeassistant/components/dsmr_reader/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." diff --git a/homeassistant/components/duco/strings.json b/homeassistant/components/duco/strings.json index 85c0130db08377..aa402ba0e1c692 100644 --- a/homeassistant/components/duco/strings.json +++ b/homeassistant/components/duco/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "The device you entered belongs to a different Duco box.", diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index b8310f9b25570f..50f8ead0ad480e 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/eheimdigital/strings.json b/homeassistant/components/eheimdigital/strings.json index f02f33763d8549..ea9b0e2f6bca8f 100644 --- a/homeassistant/components/eheimdigital/strings.json +++ b/homeassistant/components/eheimdigital/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "The identifier does not match the previous identifier" }, diff --git a/homeassistant/components/ekeybionyx/strings.json b/homeassistant/components/ekeybionyx/strings.json index 301439e2fa198c..6f027ea8505c82 100644 --- a/homeassistant/components/ekeybionyx/strings.json +++ b/homeassistant/components/ekeybionyx/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "Connection to {ekeybionyx} failed. Please check your Internet connection and try again.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_available_webhooks": "There are no available webhooks in the {ekeybionyx} system. Please delete some and try again.", diff --git a/homeassistant/components/electric_kiwi/strings.json b/homeassistant/components/electric_kiwi/strings.json index 7198c9a2ee5a56..7bb3256dd21595 100644 --- a/homeassistant/components/electric_kiwi/strings.json +++ b/homeassistant/components/electric_kiwi/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" diff --git a/homeassistant/components/escea/strings.json b/homeassistant/components/escea/strings.json index 83e561d467d992..6f6892035a0cc4 100644 --- a/homeassistant/components/escea/strings.json +++ b/homeassistant/components/escea/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Do you want to set up an Escea fireplace?" diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 6606392e62d110..6bfc1c20925ff7 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -4,7 +4,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.", "already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "mdns_missing_mac": "Missing MAC address in mDNS properties.", "mqtt_missing_api": "Missing API port in MQTT properties.", "mqtt_missing_ip": "Missing IP address in MQTT properties.", diff --git a/homeassistant/components/essent/strings.json b/homeassistant/components/essent/strings.json index 1b6f692a697ce3..ed5dac8607ac02 100644 --- a/homeassistant/components/essent/strings.json +++ b/homeassistant/components/essent/strings.json @@ -3,7 +3,6 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_data": "Received invalid data from Essent", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { diff --git a/homeassistant/components/eufylife_ble/strings.json b/homeassistant/components/eufylife_ble/strings.json index 898457eefe5ffa..8f8c652838aa22 100644 --- a/homeassistant/components/eufylife_ble/strings.json +++ b/homeassistant/components/eufylife_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 70933f3470edc2..3ab854e9992ba3 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -34,7 +34,7 @@ SCAN_INTERVAL_MINIMUM, ) from .coordinator import EvoDataUpdateCoordinator -from .services import setup_service_functions +from .services import async_setup_services from .storage import TokenManager _LOGGER = logging.getLogger(__name__) @@ -110,6 +110,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config) ) - setup_service_functions(hass, coordinator) + async_setup_services(hass, coordinator) return True diff --git a/homeassistant/components/evohome/services.py b/homeassistant/components/evohome/services.py index 1c4e1344bd181d..6286b7c51df5be 100644 --- a/homeassistant/components/evohome/services.py +++ b/homeassistant/components/evohome/services.py @@ -204,10 +204,10 @@ def _validate_set_system_mode_params(tcs: ControlSystem, data: dict[str, Any]) - @callback -def setup_service_functions( +def async_setup_services( hass: HomeAssistant, coordinator: EvoDataUpdateCoordinator ) -> None: - """Set up the service handlers for Evohome systems.""" + """Register the Evohome services.""" @verify_domain_control(DOMAIN) async def force_refresh(call: ServiceCall) -> None: diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json index 1efed4a1b6219e..72514458b8441d 100644 --- a/homeassistant/components/fastdotcom/strings.json +++ b/homeassistant/components/fastdotcom/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "user": { "description": "Do you want to start the setup? The initial setup will take about 30-40 seconds." diff --git a/homeassistant/components/fitbit/strings.json b/homeassistant/components/fitbit/strings.json index ebe44522a9de52..65d7c5b7f16edf 100644 --- a/homeassistant/components/fitbit/strings.json +++ b/homeassistant/components/fitbit/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/fjaraskupan/strings.json b/homeassistant/components/fjaraskupan/strings.json index 70343f151df146..c008f3410bac18 100644 --- a/homeassistant/components/fjaraskupan/strings.json +++ b/homeassistant/components/fjaraskupan/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Do you want to set up Fjäråskupan?" diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index d8209a6107ffdd..aecc7cacf6dcbe 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 58a460e3656063..9d182ac9eb5277 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index e3ce4bd21438b3..bcff5a34f8186f 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -from .services import setup_service_functions +from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Genius Hub services.""" - setup_service_functions(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/geniushub/services.py b/homeassistant/components/geniushub/services.py index 545b925d85ac1a..f21b0bd2276fa3 100644 --- a/homeassistant/components/geniushub/services.py +++ b/homeassistant/components/geniushub/services.py @@ -41,8 +41,8 @@ @callback -def setup_service_functions(hass: HomeAssistant) -> None: - """Set up the service functions.""" +def async_setup_services(hass: HomeAssistant) -> None: + """Register the Genius Hub services.""" @verify_domain_control(DOMAIN) async def set_zone_mode(call: ServiceCall) -> None: diff --git a/homeassistant/components/gentex_homelink/strings.json b/homeassistant/components/gentex_homelink/strings.json index e50bb3e633c045..0e7e02532a17bc 100644 --- a/homeassistant/components/gentex_homelink/strings.json +++ b/homeassistant/components/gentex_homelink/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unique_id_mismatch": "Please log in using the same account, or create a new entry." diff --git a/homeassistant/components/geocaching/strings.json b/homeassistant/components/geocaching/strings.json index 95a6eb45298460..16661f62aec3c6 100644 --- a/homeassistant/components/geocaching/strings.json +++ b/homeassistant/components/geocaching/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, diff --git a/homeassistant/components/geofency/strings.json b/homeassistant/components/geofency/strings.json index 90b1a87794282c..e14e6cec4a4eef 100644 --- a/homeassistant/components/geofency/strings.json +++ b/homeassistant/components/geofency/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to the webhook feature in Geofency and update the webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/goodwe/strings.json b/homeassistant/components/goodwe/strings.json index fc8c3bccb2341d..2c4ba6aa5be769 100644 --- a/homeassistant/components/goodwe/strings.json +++ b/homeassistant/components/goodwe/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "connection_error": "[%key:common::config_flow::error::cannot_connect%]" diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 1dea97c6552ff0..f5aa84af2e7a06 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "calendar_api_disabled": "You must [enable the Google Calendar API]({calendar_api_url}) in the Google Cloud Console", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", diff --git a/homeassistant/components/google_air_quality/strings.json b/homeassistant/components/google_air_quality/strings.json index 5be5f699a7d1a3..775857af66fce1 100644 --- a/homeassistant/components/google_air_quality/strings.json +++ b/homeassistant/components/google_air_quality/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "unable_to_fetch": "[%key:component::google_air_quality::common::unable_to_fetch%]", "unknown": "[%key:common::config_flow::error::unknown%]", "wrong_account": "Wrong account: Please authenticate with the right account." diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 1a25fc252a1eac..c22a6d3525003e 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" diff --git a/homeassistant/components/google_drive/strings.json b/homeassistant/components/google_drive/strings.json index 22766f86f7c8ef..28851734744fbb 100644 --- a/homeassistant/components/google_drive/strings.json +++ b/homeassistant/components/google_drive/strings.json @@ -6,7 +6,6 @@ "abort": { "access_not_configured": "Unable to access the Google Drive API:\n\n{message}", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "create_folder_failure": "Error while creating Google Drive folder:\n\n{message}", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/google_health/strings.json b/homeassistant/components/google_health/strings.json index 9e19fd7b8c8eab..a6525b83519698 100644 --- a/homeassistant/components/google_health/strings.json +++ b/homeassistant/components/google_health/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "api_not_enabled": "The Google Health API is not enabled for your Google Cloud project. Enable it in the [Google Cloud Console]({url}), wait a few minutes for the change to propagate, then try again.", "cannot_connect": "Failed to connect.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index 548a3920b3ed08..1e13f25714e2f7 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "wrong_account": "Wrong account: Please authenticate with {email}." diff --git a/homeassistant/components/google_photos/strings.json b/homeassistant/components/google_photos/strings.json index 838434c0055523..ba532dc99e8d61 100644 --- a/homeassistant/components/google_photos/strings.json +++ b/homeassistant/components/google_photos/strings.json @@ -6,7 +6,6 @@ "abort": { "access_not_configured": "Unable to access the Google API:\n\n{message}", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index 16f5072f675067..20d53b9c2159aa 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details", diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index 5b69344a6e60c4..1115d6e94e26ec 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -6,7 +6,6 @@ "abort": { "access_not_configured": "Unable to access the Google API:\n\n{message}", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json index 3b27991efbc340..90b9988bcef1bd 100644 --- a/homeassistant/components/govee_ble/strings.json +++ b/homeassistant/components/govee_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/govee_light_local/strings.json b/homeassistant/components/govee_light_local/strings.json index 18873d294a51e5..a852fdc0171efc 100644 --- a/homeassistant/components/govee_light_local/strings.json +++ b/homeassistant/components/govee_light_local/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/gpslogger/strings.json b/homeassistant/components/gpslogger/strings.json index 972fd4952b6aba..28952b9525ef3a 100644 --- a/homeassistant/components/gpslogger/strings.json +++ b/homeassistant/components/gpslogger/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to the webhook feature in GPSLogger and update the webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index 1a68177c6bb9ac..1369236947486b 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/green_planet_energy/services.py b/homeassistant/components/green_planet_energy/services.py index 25f43f1cbb7339..0e8260fa99ee1d 100644 --- a/homeassistant/components/green_planet_energy/services.py +++ b/homeassistant/components/green_planet_energy/services.py @@ -11,6 +11,7 @@ ServiceCall, ServiceResponse, SupportsResponse, + callback, ) from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.selector import ConfigEntrySelector @@ -177,6 +178,7 @@ async def get_prices(call: ServiceCall) -> ServiceResponse: } +@callback def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Green Planet Energy.""" diff --git a/homeassistant/components/greencell/strings.json b/homeassistant/components/greencell/strings.json index aeae54678e47d4..a16042de347ed9 100644 --- a/homeassistant/components/greencell/strings.json +++ b/homeassistant/components/greencell/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_discovery_data": "The received discovery data is invalid.", "mqtt_not_configured": "MQTT is not configured. Please configure MQTT first.", "mqtt_not_connected": "MQTT is not connected. Ensure the MQTT broker is running and configured.", diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index cae34ef7f7145f..4a7a3a87fbf4fe 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": { diff --git a/homeassistant/components/harbor/strings.json b/homeassistant/components/harbor/strings.json index 755b2dc759563d..81246a47b57293 100644 --- a/homeassistant/components/harbor/strings.json +++ b/homeassistant/components/harbor/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_discovery_info": "The discovered device does not report a valid Harbor serial number" }, "error": { diff --git a/homeassistant/components/hegel/strings.json b/homeassistant/components/hegel/strings.json index bb6e7071b87e6d..ebeeb319468451 100644 --- a/homeassistant/components/hegel/strings.json +++ b/homeassistant/components/hegel/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_host_found": "[%key:common::config_flow::abort::no_devices_found%]" }, diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 16cbb5af8aed02..b9c3070090fba9 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", diff --git a/homeassistant/components/hisense_aehw4a1/strings.json b/homeassistant/components/hisense_aehw4a1/strings.json index c29a0fb6ce68d2..2172bf8061cb7a 100644 --- a/homeassistant/components/hisense_aehw4a1/strings.json +++ b/homeassistant/components/hisense_aehw4a1/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Do you want to set up Hisense AEH-W4A1?" diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index f806cd63c5e3d4..5e66be54232e3b 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -343,6 +343,12 @@ async def async_handle_reload_config_entry(call: ServiceCall) -> None: reload_entries: set[str] = set() if ATTR_ENTRY_ID in call.data: reload_entries.add(call.data[ATTR_ENTRY_ID]) + if TargetSelection(call.data).has_any_target: + _LOGGER.warning( + "Reloading a config entry by target is deprecated and will stop " + "working in Home Assistant 2027.4, please specify the config entry " + "to reload in the 'entry_id' parameter instead" + ) reload_entries.update(await async_extract_config_entry_ids(call)) if not reload_entries: raise ValueError("There were no matching config entries to reload") diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index edcc87b2f35f86..d3b3cdbe40b53a 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -49,10 +49,11 @@ update_entity: reload_custom_templates: reload_config_entry: - target: + # Target is intentionally hidden from the UI to steer users towards selecting + # the config entry to reload. It is still accepted by the service schema. fields: entry_id: - required: false + required: true selector: config_entry: diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 39470f45edd8f4..dd07fc622bed8b 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]", @@ -262,10 +263,10 @@ "name": "Reload all Home Assistant configuration" }, "reload_config_entry": { - "description": "Reloads any explicitly provided config entry ID and any config entries referenced by entities or devices in the target. If both are provided, the union of those config entries is reloaded.", + "description": "Reloads the config entry of the provided config entry ID.", "fields": { "entry_id": { - "description": "Optional configuration entry ID to reload.", + "description": "The configuration entry to reload.", "name": "Config entry ID" } }, diff --git a/homeassistant/components/homevolt/strings.json b/homeassistant/components/homevolt/strings.json index b4f784f44d9310..6f4915beb0cd66 100644 --- a/homeassistant/components/homevolt/strings.json +++ b/homeassistant/components/homevolt/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json index bbc6521a5f3492..475bff8640fc16 100644 --- a/homeassistant/components/homewizard/strings.json +++ b/homeassistant/components/homewizard/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "device_not_supported": "This device is not supported", "invalid_discovery_parameters": "Invalid discovery parameters", "reauth_enable_api_successful": "Enabling API was successful", diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 7b2d8dfff48372..b2a3aea7bd7d40 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 84d2ba3a035a5e..bbc8384ed3a092 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -3,7 +3,6 @@ "abort": { "all_configured": "All Philips Hue bridges are already configured", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "discover_timeout": "Unable to discover Hue bridges", "invalid_host": "[%key:common::config_flow::error::invalid_host%]", diff --git a/homeassistant/components/husqvarna_automower/const.py b/homeassistant/components/husqvarna_automower/const.py index 309df6702c8686..3fb21547a5f150 100644 --- a/homeassistant/components/husqvarna_automower/const.py +++ b/homeassistant/components/husqvarna_automower/const.py @@ -19,6 +19,7 @@ ] ERROR_KEYS = [ + "accessory_power_problem", "alarm_mower_in_motion", "alarm_mower_lifted", "alarm_mower_stopped", @@ -26,12 +27,21 @@ "alarm_mower_tilted", "alarm_outside_geofence", "angular_sensor_problem", + "auxiliary_cutting_means_blocked", + "battery_near_end_of_life", + "battery_needs_replacement", "battery_problem", "battery_restriction_due_to_ambient_temperature", + "bluetooth_communication_with_charging_station_failed", + "bluetooth_settings_changed", + "boundary_wire_problem", "can_error", + "cellular_settings_changed", + "charging_aborted", "charging_current_too_high", "charging_station_blocked", "charging_system_problem", + "chassis_collision", "collision_sensor_defect", "collision_sensor_error", "collision_sensor_problem_front", @@ -43,6 +53,7 @@ "connection_not_changed", "connectivity_problem", "connectivity_settings_restored", + "cutting_disc_lost", "cutting_drive_motor_1_defect", "cutting_drive_motor_2_defect", "cutting_drive_motor_3_defect", @@ -56,11 +67,14 @@ "cutting_system_blocked", "cutting_system_imbalance_warning", "cutting_system_major_imbalance", + "destination_blocked", "destination_not_reachable", "difficult_finding_home", "docking_sensor_defect", "electronic_problem", "empty_battery", + "epos_accessory_missing", + "firmware_has_been_updated", "folding_cutting_deck_sensor_defect", "folding_sensor_activated", "geofence_problem", @@ -73,13 +87,19 @@ "high_charging_power_loss", "high_internal_power_loss", "high_internal_temperature", + "high_temperature_in_cutting_motor_center", + "high_temperature_in_cutting_motor_left", + "high_temperature_in_cutting_motor_right", + "imbalanced_auxiliary_cutting_disc_detected", "internal_voltage_error", "invalid_battery_combination_invalid_combination_of_different_battery_types", + "invalid_software_configuration", "invalid_sub_device_combination", "invalid_system_configuration", "left_brush_motor_overloaded", "lift_sensor_defect", "lifted", + "lifted_in_link_arm", "limited_cutting_height_range", "loop_sensor_defect", "loop_sensor_problem_front", @@ -88,19 +108,27 @@ "loop_sensor_problem_right", "low_battery", "memory_circuit_problem", + "mower_installation_position_changed", "mower_lifted", "mower_tilted", + "multiple_reference_stations_detected", "no_accurate_position_from_satellites", + "no_charging_station_signal", "no_confirmed_position", + "no_correction_data_available", "no_drive", "no_loop_signal", "no_power_in_charging_station", "no_response_from_charger", "outside_working_area", + "pin_code_changed", "poor_signal_quality", + "poor_vision_performance", + "radar_problem", "reference_station_communication_problem", "right_brush_motor_overloaded", "safety_function_faulty", + "self_diagnostic_error", "settings_restored", "sim_card_locked", "sim_card_not_found", @@ -124,7 +152,11 @@ "unexpected_cutting_height_adj", "unexpected_error", "upside_down", + "vision_system_error", + "vision_system_malfunction", + "water_hose_found_in_working_area", "weak_gps_signal", + "wheel_brush_motor_problem", "wheel_drive_problem_left", "wheel_drive_problem_rear_left", "wheel_drive_problem_rear_right", @@ -137,7 +169,9 @@ "wheel_motor_overloaded_rear_left", "wheel_motor_overloaded_rear_right", "wheel_motor_overloaded_right", + "wi_fi_settings_changed", "work_area_not_valid", + "work_area_tampered", "wrong_loop_signal", "wrong_pin_code", "zone_generator_problem", diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 86514dd7151d36..523efafa317719 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_amc_scope": "The `Authentication API` and the `Automower Connect API` are not connected to your application in the Husqvarna Developer Portal.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_mower_connected": "No mowers connected to this account.", @@ -56,6 +55,7 @@ "state_attributes": { "event_type": { "state": { + "accessory_power_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::accessory_power_problem%]", "alarm_mower_in_motion": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_in_motion%]", "alarm_mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_lifted%]", "alarm_mower_stopped": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_stopped%]", @@ -63,12 +63,21 @@ "alarm_mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_tilted%]", "alarm_outside_geofence": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_outside_geofence%]", "angular_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::angular_sensor_problem%]", + "auxiliary_cutting_means_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::auxiliary_cutting_means_blocked%]", + "battery_near_end_of_life": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_near_end_of_life%]", + "battery_needs_replacement": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_needs_replacement%]", "battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_problem%]", "battery_restriction_due_to_ambient_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_restriction_due_to_ambient_temperature%]", + "bluetooth_communication_with_charging_station_failed": "[%key:component::husqvarna_automower::entity::sensor::error::state::bluetooth_communication_with_charging_station_failed%]", + "bluetooth_settings_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::bluetooth_settings_changed%]", + "boundary_wire_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::boundary_wire_problem%]", "can_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::can_error%]", + "cellular_settings_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::cellular_settings_changed%]", + "charging_aborted": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_aborted%]", "charging_current_too_high": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_current_too_high%]", "charging_station_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_station_blocked%]", "charging_system_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_system_problem%]", + "chassis_collision": "[%key:component::husqvarna_automower::entity::sensor::error::state::chassis_collision%]", "collision_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_defect%]", "collision_sensor_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_error%]", "collision_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_front%]", @@ -80,6 +89,7 @@ "connection_not_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_not_changed%]", "connectivity_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_problem%]", "connectivity_settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_settings_restored%]", + "cutting_disc_lost": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_disc_lost%]", "cutting_drive_motor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_1_defect%]", "cutting_drive_motor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_2_defect%]", "cutting_drive_motor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_3_defect%]", @@ -93,14 +103,17 @@ "cutting_system_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_blocked%]", "cutting_system_imbalance_warning": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_imbalance_warning%]", "cutting_system_major_imbalance": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_major_imbalance%]", + "destination_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_blocked%]", "destination_not_reachable": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_not_reachable%]", "difficult_finding_home": "[%key:component::husqvarna_automower::entity::sensor::error::state::difficult_finding_home%]", "docking_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::docking_sensor_defect%]", "electronic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::electronic_problem%]", "empty_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::empty_battery%]", + "epos_accessory_missing": "[%key:component::husqvarna_automower::entity::sensor::error::state::epos_accessory_missing%]", "error": "[%key:common::state::error%]", "error_at_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::error_at_power_up%]", "fatal_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::fatal_error%]", + "firmware_has_been_updated": "[%key:component::husqvarna_automower::entity::sensor::error::state::firmware_has_been_updated%]", "folding_cutting_deck_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_cutting_deck_sensor_defect%]", "folding_sensor_activated": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_sensor_activated%]", "geofence_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::geofence_problem%]", @@ -113,13 +126,19 @@ "high_charging_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_charging_power_loss%]", "high_internal_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_power_loss%]", "high_internal_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_temperature%]", + "high_temperature_in_cutting_motor_center": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_temperature_in_cutting_motor_center%]", + "high_temperature_in_cutting_motor_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_temperature_in_cutting_motor_left%]", + "high_temperature_in_cutting_motor_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_temperature_in_cutting_motor_right%]", + "imbalanced_auxiliary_cutting_disc_detected": "[%key:component::husqvarna_automower::entity::sensor::error::state::imbalanced_auxiliary_cutting_disc_detected%]", "internal_voltage_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::internal_voltage_error%]", "invalid_battery_combination_invalid_combination_of_different_battery_types": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_battery_combination_invalid_combination_of_different_battery_types%]", + "invalid_software_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_software_configuration%]", "invalid_sub_device_combination": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_sub_device_combination%]", "invalid_system_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_system_configuration%]", "left_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::left_brush_motor_overloaded%]", "lift_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::lift_sensor_defect%]", "lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted%]", + "lifted_in_link_arm": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted_in_link_arm%]", "limited_cutting_height_range": "[%key:component::husqvarna_automower::entity::sensor::error::state::limited_cutting_height_range%]", "loop_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_defect%]", "loop_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_front%]", @@ -128,10 +147,14 @@ "loop_sensor_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_right%]", "low_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::low_battery%]", "memory_circuit_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::memory_circuit_problem%]", + "mower_installation_position_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_installation_position_changed%]", "mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_lifted%]", "mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_tilted%]", + "multiple_reference_stations_detected": "[%key:component::husqvarna_automower::entity::sensor::error::state::multiple_reference_stations_detected%]", "no_accurate_position_from_satellites": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_accurate_position_from_satellites%]", + "no_charging_station_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_charging_station_signal%]", "no_confirmed_position": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_confirmed_position%]", + "no_correction_data_available": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_correction_data_available%]", "no_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_drive%]", "no_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_error%]", "no_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_loop_signal%]", @@ -139,10 +162,14 @@ "no_response_from_charger": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_response_from_charger%]", "off": "[%key:common::state::off%]", "outside_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::outside_working_area%]", + "pin_code_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::pin_code_changed%]", "poor_signal_quality": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_signal_quality%]", + "poor_vision_performance": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_vision_performance%]", + "radar_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::radar_problem%]", "reference_station_communication_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::reference_station_communication_problem%]", "right_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::right_brush_motor_overloaded%]", "safety_function_faulty": "[%key:component::husqvarna_automower::entity::sensor::error::state::safety_function_faulty%]", + "self_diagnostic_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::self_diagnostic_error%]", "settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::settings_restored%]", "sim_card_locked": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_locked%]", "sim_card_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_not_found%]", @@ -167,9 +194,13 @@ "unexpected_cutting_height_adj": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_cutting_height_adj%]", "unexpected_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_error%]", "upside_down": "[%key:component::husqvarna_automower::entity::sensor::error::state::upside_down%]", + "vision_system_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::vision_system_error%]", + "vision_system_malfunction": "[%key:component::husqvarna_automower::entity::sensor::error::state::vision_system_malfunction%]", "wait_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_power_up%]", "wait_updating": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_updating%]", + "water_hose_found_in_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::water_hose_found_in_working_area%]", "weak_gps_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::weak_gps_signal%]", + "wheel_brush_motor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_brush_motor_problem%]", "wheel_drive_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_left%]", "wheel_drive_problem_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_left%]", "wheel_drive_problem_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_right%]", @@ -182,7 +213,9 @@ "wheel_motor_overloaded_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_left%]", "wheel_motor_overloaded_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_right%]", "wheel_motor_overloaded_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_right%]", + "wi_fi_settings_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::wi_fi_settings_changed%]", "work_area_not_valid": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_not_valid%]", + "work_area_tampered": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_tampered%]", "wrong_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_loop_signal%]", "wrong_pin_code": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_pin_code%]", "zone_generator_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::zone_generator_problem%]" @@ -234,6 +267,7 @@ "error": { "name": "Error", "state": { + "accessory_power_problem": "Accessory power problem", "alarm_mower_in_motion": "Alarm! Mower in motion", "alarm_mower_lifted": "Alarm! Mower lifted", "alarm_mower_stopped": "Alarm! Mower stopped", @@ -241,12 +275,21 @@ "alarm_mower_tilted": "Alarm! Mower tilted", "alarm_outside_geofence": "Alarm! Outside geofence", "angular_sensor_problem": "Angular sensor problem", + "auxiliary_cutting_means_blocked": "Auxiliary cutting means blocked", + "battery_near_end_of_life": "Battery near end of life", + "battery_needs_replacement": "Battery needs replacement", "battery_problem": "Battery problem", "battery_restriction_due_to_ambient_temperature": "Battery restriction due to ambient temperature", + "bluetooth_communication_with_charging_station_failed": "Bluetooth communication with charging station failed", + "bluetooth_settings_changed": "Bluetooth settings changed", + "boundary_wire_problem": "Boundary wire problem", "can_error": "CAN error", + "cellular_settings_changed": "Cellular settings changed", + "charging_aborted": "Charging aborted", "charging_current_too_high": "Charging current too high", "charging_station_blocked": "Charging station blocked", "charging_system_problem": "Charging system problem", + "chassis_collision": "Chassis collision", "collision_sensor_defect": "Collision sensor defect", "collision_sensor_error": "Collision sensor error", "collision_sensor_problem_front": "Front collision sensor problem", @@ -258,6 +301,7 @@ "connection_not_changed": "Connection NOT changed", "connectivity_problem": "Connectivity problem", "connectivity_settings_restored": "Connectivity settings restored", + "cutting_disc_lost": "Cutting disc lost", "cutting_drive_motor_1_defect": "Cutting drive motor 1 defect", "cutting_drive_motor_2_defect": "Cutting drive motor 2 defect", "cutting_drive_motor_3_defect": "Cutting drive motor 3 defect", @@ -271,14 +315,17 @@ "cutting_system_blocked": "Cutting system blocked", "cutting_system_imbalance_warning": "Cutting system imbalance", "cutting_system_major_imbalance": "Cutting system major imbalance", + "destination_blocked": "Destination blocked", "destination_not_reachable": "Destination not reachable", "difficult_finding_home": "Difficult finding home", "docking_sensor_defect": "Docking sensor defect", "electronic_problem": "Electronic problem", "empty_battery": "Empty battery", + "epos_accessory_missing": "EPOS accessory missing", "error": "[%key:common::state::error%]", "error_at_power_up": "Error at power up", "fatal_error": "Fatal error", + "firmware_has_been_updated": "Firmware has been updated", "folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect", "folding_sensor_activated": "Folding sensor activated", "geofence_problem": "Geofence problem", @@ -291,13 +338,19 @@ "high_charging_power_loss": "High charging power loss", "high_internal_power_loss": "High internal power loss", "high_internal_temperature": "High internal temperature", + "high_temperature_in_cutting_motor_center": "High temperature in cutting motor, center", + "high_temperature_in_cutting_motor_left": "High temperature in cutting motor, left", + "high_temperature_in_cutting_motor_right": "High temperature in cutting motor, right", + "imbalanced_auxiliary_cutting_disc_detected": "Imbalanced auxiliary cutting disc detected", "internal_voltage_error": "Internal voltage error", "invalid_battery_combination_invalid_combination_of_different_battery_types": "Invalid battery combination - Invalid combination of different battery types.", + "invalid_software_configuration": "Invalid software configuration", "invalid_sub_device_combination": "Invalid sub-device combination", "invalid_system_configuration": "Invalid system configuration", "left_brush_motor_overloaded": "Left brush motor overloaded", "lift_sensor_defect": "Lift Sensor defect", "lifted": "Lifted", + "lifted_in_link_arm": "Lifted in link arm", "limited_cutting_height_range": "Limited cutting height range", "loop_sensor_defect": "Loop sensor defect", "loop_sensor_problem_front": "Front loop sensor problem", @@ -306,10 +359,14 @@ "loop_sensor_problem_right": "Right loop sensor problem", "low_battery": "Low battery", "memory_circuit_problem": "Memory circuit problem", + "mower_installation_position_changed": "Mower installation position changed", "mower_lifted": "Mower lifted", "mower_tilted": "Mower tilted", + "multiple_reference_stations_detected": "Multiple reference stations detected", "no_accurate_position_from_satellites": "No accurate position from satellites", + "no_charging_station_signal": "No charging station signal", "no_confirmed_position": "No confirmed position", + "no_correction_data_available": "No correction data available", "no_drive": "No drive", "no_error": "No error", "no_loop_signal": "No loop signal", @@ -317,10 +374,14 @@ "no_response_from_charger": "No response from charger", "off": "[%key:common::state::off%]", "outside_working_area": "Outside working area", + "pin_code_changed": "PIN code changed", "poor_signal_quality": "Poor signal quality", + "poor_vision_performance": "Poor vision performance", + "radar_problem": "Radar problem", "reference_station_communication_problem": "Reference station communication problem", "right_brush_motor_overloaded": "Right brush motor overloaded", "safety_function_faulty": "Safety function faulty", + "self_diagnostic_error": "Self diagnostic error", "settings_restored": "Settings restored", "sim_card_locked": "SIM card locked", "sim_card_not_found": "SIM card not found", @@ -345,9 +406,13 @@ "unexpected_cutting_height_adj": "Unexpected cutting height adjustment", "unexpected_error": "Unexpected error", "upside_down": "Upside down", + "vision_system_error": "Vision system error", + "vision_system_malfunction": "Vision system malfunction", "wait_power_up": "Wait power up", "wait_updating": "Wait updating", + "water_hose_found_in_working_area": "Water hose found in working area", "weak_gps_signal": "Weak GPS signal", + "wheel_brush_motor_problem": "Wheel brush motor problem", "wheel_drive_problem_left": "Left wheel drive problem", "wheel_drive_problem_rear_left": "Rear left wheel drive problem", "wheel_drive_problem_rear_right": "Rear right wheel drive problem", @@ -360,7 +425,9 @@ "wheel_motor_overloaded_rear_left": "Rear left wheel motor overloaded", "wheel_motor_overloaded_rear_right": "Rear right wheel motor overloaded", "wheel_motor_overloaded_right": "Right wheel motor overloaded", + "wi_fi_settings_changed": "Wi-Fi settings changed", "work_area_not_valid": "Work area not valid", + "work_area_tampered": "Work area tampered", "wrong_loop_signal": "Wrong loop signal", "wrong_pin_code": "Wrong PIN code", "zone_generator_problem": "Zone generator problem" diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index c0ef2a704646c3..77fc7cf603d670 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI", "auth_new_token_not_work_error": "Failed to authenticate using newly created token", "auth_required_error": "Failed to determine if authorization is required", diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json index 23857c34817c14..a9f1dac6db6a78 100644 --- a/homeassistant/components/iaqualink/strings.json +++ b/homeassistant/components/iaqualink/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, diff --git a/homeassistant/components/idasen_desk/strings.json b/homeassistant/components/idasen_desk/strings.json index ec88476d0c3db3..7c9d35e4934251 100644 --- a/homeassistant/components/idasen_desk/strings.json +++ b/homeassistant/components/idasen_desk/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "No unconfigured devices found. Make sure that the desk is in Bluetooth pairing mode. Enter pairing mode by pressing the small button with the Bluetooth logo on the controller for about 3 seconds, until it starts blinking.", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json index fc1ff7b05342bf..4030480c923597 100644 --- a/homeassistant/components/ifttt/strings.json +++ b/homeassistant/components/ifttt/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to the \"Make a web request\" action from the [IFTTT webhook applet]({applet_url}) and update the webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 0fc62301984b5e..0fded758890eae 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -19,7 +19,7 @@ IHC_CONTROLLER_INDEX, ) from .manual_setup import IHC_SCHEMA, get_manual_configuration -from .service_functions import setup_service_functions +from .services import setup_services _LOGGER = logging.getLogger(__name__) @@ -31,6 +31,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the IHC integration.""" + hass.data.setdefault(DOMAIN, {}) + setup_services(hass) conf = config[DOMAIN] for index, controller_conf in enumerate(conf): if not ihc_setup(hass, config, controller_conf, index): @@ -55,7 +57,6 @@ def ihc_setup( return False controller_id: str = ihc_controller.client.get_system_info()["serial_number"] # Store controller configuration - hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][controller_id] = { IHC_CONTROLLER: ihc_controller, CONF_INFO: controller_conf[CONF_INFO], @@ -66,7 +67,4 @@ def ihc_setup( ): return False get_manual_configuration(hass, config, controller_conf, controller_id) - # We only want to register the service functions once for the first controller - if controller_index == 0: - setup_service_functions(hass) return True diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/services.py similarity index 96% rename from homeassistant/components/ihc/service_functions.py rename to homeassistant/components/ihc/services.py index d5507328e73d7a..f6b1766e297e1c 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/services.py @@ -1,4 +1,4 @@ -"""Support for IHC devices.""" +"""Support for IHC services.""" import voluptuous as vol @@ -51,8 +51,8 @@ ) -def setup_service_functions(hass: HomeAssistant) -> None: - """Set up the IHC service functions.""" +def setup_services(hass: HomeAssistant) -> None: + """Register the IHC services.""" def _get_controller(call): controller_index = call.data[ATTR_CONTROLLER_ID] diff --git a/homeassistant/components/immich/__init__.py b/homeassistant/components/immich/__init__.py index d7b9e7924a40ad..e02b94f772c9b4 100644 --- a/homeassistant/components/immich/__init__.py +++ b/homeassistant/components/immich/__init__.py @@ -16,7 +16,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up immich integration.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/immich/services.py b/homeassistant/components/immich/services.py index 9ca476a2727142..d38b605dc24c04 100644 --- a/homeassistant/components/immich/services.py +++ b/homeassistant/components/immich/services.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.media_source import async_resolve_media -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import service from homeassistant.helpers.selector import MediaSelector @@ -75,7 +75,8 @@ async def _async_upload_file(service_call: ServiceCall) -> None: ) from ex -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for immich integration.""" hass.services.async_register( diff --git a/homeassistant/components/imou/diagnostics.py b/homeassistant/components/imou/diagnostics.py new file mode 100644 index 00000000000000..2df7e1f8d9a46d --- /dev/null +++ b/homeassistant/components/imou/diagnostics.py @@ -0,0 +1,44 @@ +"""Diagnostics support for Imou.""" + +from typing import Any + +from pyimouapi.ha_device import ImouHaDevice + +from homeassistant.core import HomeAssistant + +from .const import CONF_API_URL, CONF_APP_ID, imou_device_identifier +from .coordinator import ImouConfigEntry + + +def _serialize_device(device: ImouHaDevice) -> dict[str, Any]: + """Return a JSON-safe view of a discovered Imou device.""" + return { + "identifier": imou_device_identifier(device), + "device_id": device.device_id, + "channel_id": device.channel_id, + "model": device.model, + "manufacturer": device.manufacturer, + "sw_version": device.swversion, + "product_id": device.product_id, + "is_ipc": device.is_ipc, + } + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ImouConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = entry.runtime_data + return { + "entry": { + CONF_API_URL: entry.data[CONF_API_URL], + CONF_APP_ID: entry.data[CONF_APP_ID], + }, + "coordinator": { + "last_update_success": coordinator.last_update_success, + }, + "devices": [ + _serialize_device(device) + for device in sorted(coordinator.devices, key=imou_device_identifier) + ], + } diff --git a/homeassistant/components/imou/quality_scale.yaml b/homeassistant/components/imou/quality_scale.yaml index a9c86bee01c3ee..ed9c0718fcc1d0 100644 --- a/homeassistant/components/imou/quality_scale.yaml +++ b/homeassistant/components/imou/quality_scale.yaml @@ -45,7 +45,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: Cloud hub; DHCP only surfaces the integration. Device IP is not stored or updated. diff --git a/homeassistant/components/imou/strings.json b/homeassistant/components/imou/strings.json index e5872d8ed7d44d..051f0d3a8d2bc1 100644 --- a/homeassistant/components/imou/strings.json +++ b/homeassistant/components/imou/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unique_id_mismatch": "The App ID does not match the previously configured account." }, diff --git a/homeassistant/components/indevolt/__init__.py b/homeassistant/components/indevolt/__init__.py index 907819e123fe87..63268fdec07e8b 100644 --- a/homeassistant/components/indevolt/__init__.py +++ b/homeassistant/components/indevolt/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IndevoltConfigEntry) -> async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up indevolt services (actions).""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/indevolt/services.py b/homeassistant/components/indevolt/services.py index 105534ac16ac2a..f94c6da499c568 100644 --- a/homeassistant/components/indevolt/services.py +++ b/homeassistant/components/indevolt/services.py @@ -10,7 +10,7 @@ ) import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_config_entry_ids @@ -36,7 +36,8 @@ ) -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Indevolt integration.""" async def charge(call: ServiceCall) -> None: diff --git a/homeassistant/components/infrared/manifest.json b/homeassistant/components/infrared/manifest.json index 126a74bd3cea70..f4bda801d83ff3 100644 --- a/homeassistant/components/infrared/manifest.json +++ b/homeassistant/components/infrared/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/infrared", "integration_type": "entity", "quality_scale": "internal", - "requirements": ["infrared-protocols==9.1.0"] + "requirements": ["infrared-protocols==10.0.0"] } diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json index e2f1afb0be8750..fb07b49fb8a349 100644 --- a/homeassistant/components/inkbird/strings.json +++ b/homeassistant/components/inkbird/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/iometer/strings.json b/homeassistant/components/iometer/strings.json index 2b631abd032f7a..ba85257190b9ad 100644 --- a/homeassistant/components/iometer/strings.json +++ b/homeassistant/components/iometer/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/ios/strings.json b/homeassistant/components/ios/strings.json index 1fb7ac74aae63d..5e86ffe943fa95 100644 --- a/homeassistant/components/ios/strings.json +++ b/homeassistant/components/ios/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/iotty/strings.json b/homeassistant/components/iotty/strings.json index 5ae8b1aca12b72..cafe42dcae8359 100644 --- a/homeassistant/components/iotty/strings.json +++ b/homeassistant/components/iotty/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" }, "create_entry": { diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 80425b32862d58..7fac11a85dc14d 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -18,7 +18,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Jellyfin component.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/jellyfin/services.py b/homeassistant/components/jellyfin/services.py index eeb9f303cbbd19..c45c7e485677af 100644 --- a/homeassistant/components/jellyfin/services.py +++ b/homeassistant/components/jellyfin/services.py @@ -10,7 +10,7 @@ DOMAIN as MP_DOMAIN, MediaPlayerEntityFeature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, service from .const import DOMAIN @@ -38,7 +38,8 @@ def _promote_media_fields(data: dict[str, Any]) -> dict[str, Any]: return data -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for the Jellyfin component.""" service.async_register_platform_entity_service( diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index d06b530d4d8284..c47e97bc8a8f69 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json index e7d3adcbf5c960..a045b786bc67bc 100644 --- a/homeassistant/components/kaleidescape/strings.json +++ b/homeassistant/components/kaleidescape/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "unknown": "[%key:common::config_flow::error::unknown%]", "unsupported": "Unsupported device" }, diff --git a/homeassistant/components/kegtron/strings.json b/homeassistant/components/kegtron/strings.json index f85ef2e69208ae..3257ad384c3d26 100644 --- a/homeassistant/components/kegtron/strings.json +++ b/homeassistant/components/kegtron/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/kiosker/strings.json b/homeassistant/components/kiosker/strings.json index ea117b290b8abd..415060ecaa3809 100644 --- a/homeassistant/components/kiosker/strings.json +++ b/homeassistant/components/kiosker/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "wrong_device": "The device does not match the configured device." }, diff --git a/homeassistant/components/laundrify/strings.json b/homeassistant/components/laundrify/strings.json index 88c9752dc59ecf..d1b273b151fc54 100644 --- a/homeassistant/components/laundrify/strings.json +++ b/homeassistant/components/laundrify/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/ld2410_ble/strings.json b/homeassistant/components/ld2410_ble/strings.json index ad6b07b7581c7c..ae48cbe608b3bb 100644 --- a/homeassistant/components/ld2410_ble/strings.json +++ b/homeassistant/components/ld2410_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/leaone/strings.json b/homeassistant/components/leaone/strings.json index 06e672e243bc03..532ec3df1fcd97 100644 --- a/homeassistant/components/leaone/strings.json +++ b/homeassistant/components/leaone/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "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." }, "flow_title": "{name}", diff --git a/homeassistant/components/led_ble/strings.json b/homeassistant/components/led_ble/strings.json index 0cc44653ce2f9a..08336ef24c08cb 100644 --- a/homeassistant/components/led_ble/strings.json +++ b/homeassistant/components/led_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index dd739a8ca21ee3..cd6af704d440bb 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "exceeded_api_calls": "The number of API calls has been exceeded.", diff --git a/homeassistant/components/linkplay/strings.json b/homeassistant/components/linkplay/strings.json index 888d1100545165..da47939f18c9a9 100644 --- a/homeassistant/components/linkplay/strings.json +++ b/homeassistant/components/linkplay/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "not_linkplay_device": "This device should be set up with the WiiM integration." }, "error": { diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index be10babc099747..f2d386001c4d49 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "The Whisker account does not match the previously configured account. Please re-authenticate using the same account, or remove this integration and set it up again if you want to use a different account." diff --git a/homeassistant/components/locative/strings.json b/homeassistant/components/locative/strings.json index e4dcac05d18798..a6d9caa78c074d 100644 --- a/homeassistant/components/locative/strings.json +++ b/homeassistant/components/locative/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to webhooks in the Locative app and update webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/london_underground/strings.json b/homeassistant/components/london_underground/strings.json index 27f5213fab0203..593f02dad0b011 100644 --- a/homeassistant/components/london_underground/strings.json +++ b/homeassistant/components/london_underground/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", diff --git a/homeassistant/components/lookin/strings.json b/homeassistant/components/lookin/strings.json index 82cdc6bd606832..81aa76269dafcd 100644 --- a/homeassistant/components/lookin/strings.json +++ b/homeassistant/components/lookin/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, diff --git a/homeassistant/components/lunatone/strings.json b/homeassistant/components/lunatone/strings.json index 70d5ddde2ef308..5ccc46d2e93d74 100644 --- a/homeassistant/components/lunatone/strings.json +++ b/homeassistant/components/lunatone/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "Please ensure you reconfigure against the same device." }, diff --git a/homeassistant/components/lyngdorf/strings.json b/homeassistant/components/lyngdorf/strings.json index 9904a37d8e7bcc..0750ad7ae39a2a 100644 --- a/homeassistant/components/lyngdorf/strings.json +++ b/homeassistant/components/lyngdorf/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_determine_id": "[%key:component::lyngdorf::config::error::cannot_determine_id%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", diff --git a/homeassistant/components/mailgun/strings.json b/homeassistant/components/mailgun/strings.json index d2763f78f68b18..eabde9948a3ad1 100644 --- a/homeassistant/components/mailgun/strings.json +++ b/homeassistant/components/mailgun/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to [webhooks in Mailgun]({mailgun_url}) and update the webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index fefe7b872dd293..ca34a55df285d2 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "wrong_account": "You have to use the same account that was used to configure the integration." diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 8d0a254cc118a5..943f7eb131b2af 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -6,7 +6,6 @@ "addon_install_failed": "Failed to install the Matter Server app.", "addon_start_failed": "Failed to start the Matter Server app.", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "not_matter_addon": "Discovered app is not the official Matter Server app.", "reconfiguration_successful": "Successfully reconfigured the Matter integration." diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 6c5e79403e79b9..28671b7a5b609b 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -7,7 +7,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "wrong_account": "You have to use the same account that was used to configure the integration." diff --git a/homeassistant/components/medcom_ble/strings.json b/homeassistant/components/medcom_ble/strings.json index 76e5116588677f..2eecbd6edf7606 100644 --- a/homeassistant/components/medcom_ble/strings.json +++ b/homeassistant/components/medcom_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/microbees/strings.json b/homeassistant/components/microbees/strings.json index bbe0922f8512ae..37040cd540a736 100644 --- a/homeassistant/components/microbees/strings.json +++ b/homeassistant/components/microbees/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/midea/__init__.py b/homeassistant/components/midea/__init__.py index f47cde0bf451bb..be4db2e9a387a8 100644 --- a/homeassistant/components/midea/__init__.py +++ b/homeassistant/components/midea/__init__.py @@ -25,7 +25,7 @@ from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from .const import CONF_KEY, CONF_SN, CONF_SUBTYPE, LOGGER +from .const import CONF_KEY, CONF_SN, CONF_SUBTYPE, DOMAIN, LOGGER from .entity import MideaConfigEntry _PLATFORMS: list[Platform] = [ @@ -96,7 +96,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> boo device = await hass.async_add_executor_job(_create_device, data, ip_address) if device is None: - raise ConfigEntryError("Unable to initialize device") + raise ConfigEntryError( + translation_domain=DOMAIN, translation_key="unable_initialize_device" + ) connected = await hass.async_add_executor_job(_connect, device) if not connected: @@ -119,7 +121,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> boo device = new_device connected = await hass.async_add_executor_job(_connect, device) if not connected: - raise ConfigEntryNotReady(f"Unable to connect to device {device_id}") + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="unable_connect_device", + translation_placeholders={"device_id": str(device_id)}, + ) # The library's reconnect loop keeps retrying with a growing backoff # (up to 600s) without checking for a stop request while sleeping, so diff --git a/homeassistant/components/midea/config_flow.py b/homeassistant/components/midea/config_flow.py index c2e05d2707f8ea..61c53c2d1bff4e 100644 --- a/homeassistant/components/midea/config_flow.py +++ b/homeassistant/components/midea/config_flow.py @@ -718,6 +718,42 @@ async def async_step_manually( return await self._async_create_midea_entry(user_input) return self._show_manually_form(user_input, error) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Allow reconfiguration of a Midea config entry.""" + entry = self._get_reconfigure_entry() + error = None + if user_input is not None: + devices = await self.hass.async_add_executor_job( + lambda: discover( + list(self.supports.keys()), ip_address=user_input[CONF_IP_ADDRESS] + ), + ) + entry_device_id = entry.data[CONF_DEVICE_ID] + device = devices.get(entry_device_id) + if len(devices) == 0: + error = "invalid_device_ip" + elif device is None: + error = "invalid_device_id_for_ip" + else: + return self.async_update_reload_and_abort( + entry, + data_updates={CONF_IP_ADDRESS: device.get(CONF_IP_ADDRESS)}, + ) + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Required( + CONF_IP_ADDRESS, + default=(user_input or entry.data)[CONF_IP_ADDRESS], + ): str + } + ), + errors={"base": error} if error else None, + ) + def _show_manually_form( self, user_input: dict[str, Any] | None, diff --git a/homeassistant/components/midea/icons.json b/homeassistant/components/midea/icons.json index a1bf5b1924371e..e23b7129ba94ea 100644 --- a/homeassistant/components/midea/icons.json +++ b/homeassistant/components/midea/icons.json @@ -79,6 +79,9 @@ "aux_heating": { "default": "mdi:radiator" }, + "child_lock": { + "default": "mdi:lock" + }, "disinfect": { "default": "mdi:bacteria" }, diff --git a/homeassistant/components/midea/quality_scale.yaml b/homeassistant/components/midea/quality_scale.yaml index f0efa6b447fab6..0829394cae6c27 100644 --- a/homeassistant/components/midea/quality_scale.yaml +++ b/homeassistant/components/midea/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-translations: todo exception-translations: todo icon-translations: todo - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: todo stale-devices: todo diff --git a/homeassistant/components/midea/strings.json b/homeassistant/components/midea/strings.json index df3b9f74d637d3..5d2b52ca4eb7dc 100644 --- a/homeassistant/components/midea/strings.json +++ b/homeassistant/components/midea/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "device_auth_failed": "Could not connect with the provided configuration", @@ -78,6 +79,15 @@ }, "title": "Configure manually" }, + "reconfigure": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "data_description": { + "ip_address": "[%key:component::midea::config::step::manually::data_description::ip_address%]" + }, + "title": "Set your device's new IP address" + }, "search": { "data": { "ip_address": "[%key:common::config_flow::data::ip%]" @@ -661,6 +671,9 @@ "aux_heating": { "name": "Auxiliary heating" }, + "child_lock": { + "name": "Child lock" + }, "disinfect": { "name": "Disinfect" }, @@ -708,6 +721,12 @@ "e1_device_off": { "message": "Turn on the dishwasher before starting a wash cycle." }, + "unable_connect_device": { + "message": "Unable to connect to device {device_id}" + }, + "unable_initialize_device": { + "message": "Unable to initialize device" + }, "unsupported_hvac_mode": { "message": "HVAC mode {hvac_mode} is not supported by this device." } diff --git a/homeassistant/components/midea/switch.py b/homeassistant/components/midea/switch.py index b459aebd7004ee..f9ae21c5d85a6f 100644 --- a/homeassistant/components/midea/switch.py +++ b/homeassistant/components/midea/switch.py @@ -27,6 +27,21 @@ class MideaSwitchEntityDescription(SwitchEntityDescription): translation_key="aux_heating", models=[DeviceType.AC, DeviceType.CC, DeviceType.CF], ), + MideaSwitchEntityDescription( + key="child_lock", + translation_key="child_lock", + models=[ + DeviceType.X34, + DeviceType.A1, + DeviceType.C2, + DeviceType.CE, + DeviceType.E1, + DeviceType.ED, + DeviceType.FA, + DeviceType.FB, + DeviceType.FC, + ], + ), MideaSwitchEntityDescription( key="prompt_tone", translation_key="prompt_tone", diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index d4c4cff0028a5d..7df2ddc06ceb07 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up service actions.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index 47d1f035563a04..40a8a188d4a76b 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -13,6 +13,7 @@ ServiceCall, ServiceResponse, SupportsResponse, + callback, ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -213,7 +214,8 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: } -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" hass.services.async_register( diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 81697d5cc7e045..5070eb3c46534b 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -6,12 +6,10 @@ "abort": { "account_mismatch": "The used account does not match the original account", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json index 5e39fe3eab72aa..8ed563c8cba524 100644 --- a/homeassistant/components/moat/strings.json +++ b/homeassistant/components/moat/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/monzo/strings.json b/homeassistant/components/monzo/strings.json index 8fb9ed2109dcf0..3cdccf0348bca1 100644 --- a/homeassistant/components/monzo/strings.json +++ b/homeassistant/components/monzo/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "wrong_account": "Wrong account: The credentials provided do not match this Monzo account." diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json index 77ce8adcc902bf..2e8a289221ec8e 100644 --- a/homeassistant/components/mopeka/strings.json +++ b/homeassistant/components/mopeka/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 8d5b4ef454cac2..601e4b6579bb2e 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "not_motionblinds": "Discovered device is not a Motionblinds gateway" }, diff --git a/homeassistant/components/motionmount/strings.json b/homeassistant/components/motionmount/strings.json index ffc19d73063219..59a6dc4dc3533b 100644 --- a/homeassistant/components/motionmount/strings.json +++ b/homeassistant/components/motionmount/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_response": "Failed to connect due to an invalid response from the MotionMount.", "not_connected": "Failed to connect.", diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index fa64f81a093694..214054e8f0dd5d 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -7,8 +7,7 @@ "addon_start_failed": "Failed to start the {addon} app.", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "bad_birth": "Invalid birth topic", diff --git a/homeassistant/components/music_assistant/strings.json b/homeassistant/components/music_assistant/strings.json index f403a2b37cc781..047b96f0b0060c 100644 --- a/homeassistant/components/music_assistant/strings.json +++ b/homeassistant/components/music_assistant/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "auth_error": "Authentication error, please try again", "auth_failed": "Authentication failed, please try again", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json index 7c370f0e56d078..4a1243757f2be5 100644 --- a/homeassistant/components/myuplink/strings.json +++ b/homeassistant/components/myuplink/strings.json @@ -6,7 +6,6 @@ "abort": { "account_mismatch": "The used account does not match the original account", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0af9068b9d68fb..d3c4b3f260eaaa 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "pubsub_api_error": "[%key:component::nest::config::error::pubsub_api_error%]", diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 2cf34d7e1ee0d0..94d64f46caa405 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -2,8 +2,7 @@ "config": { "abort": { "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/nobo_hub/strings.json b/homeassistant/components/nobo_hub/strings.json index 08a8b162e03e06..88ee15ac5e3603 100644 --- a/homeassistant/components/nobo_hub/strings.json +++ b/homeassistant/components/nobo_hub/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_discover": "Could not detect a Nobø Ecohub at the discovered IP address.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, diff --git a/homeassistant/components/nrgkick/strings.json b/homeassistant/components/nrgkick/strings.json index 3da169ec74f40f..edf14d06f8e16c 100644 --- a/homeassistant/components/nrgkick/strings.json +++ b/homeassistant/components/nrgkick/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "json_api_disabled": "JSON API is disabled on the device. Enable it in the NRGkick mobile app under Extended \u2192 Local API \u2192 API Variants.", "no_serial_number": "Device does not provide a serial number", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/omie/strings.json b/homeassistant/components/omie/strings.json index 1646d7a856b8e1..10cef445348f92 100644 --- a/homeassistant/components/omie/strings.json +++ b/homeassistant/components/omie/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, diff --git a/homeassistant/components/onedrive/strings.json b/homeassistant/components/onedrive/strings.json index 34b098741455a9..1baad944a15846 100644 --- a/homeassistant/components/onedrive/strings.json +++ b/homeassistant/components/onedrive/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "Failed to connect to OneDrive.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/onedrive_for_business/strings.json b/homeassistant/components/onedrive_for_business/strings.json index dc63e62a9830c5..bc8e9e3ae6f243 100644 --- a/homeassistant/components/onedrive_for_business/strings.json +++ b/homeassistant/components/onedrive_for_business/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:component::onedrive::config::abort::connection_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 65a8c01f0fc526..8934fd83234839 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "no_h264": "There were no H.264 streams available. Check the profile configuration on your device.", "no_mac": "Could not configure unique ID for ONVIF device.", diff --git a/homeassistant/components/opendisplay/strings.json b/homeassistant/components/opendisplay/strings.json index 25e7ff98235c06..7ea82e96061e31 100644 --- a/homeassistant/components/opendisplay/strings.json +++ b/homeassistant/components/opendisplay/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/opnsense/strings.json b/homeassistant/components/opnsense/strings.json index fe5919409614ee..575782954b7e89 100644 --- a/homeassistant/components/opnsense/strings.json +++ b/homeassistant/components/opnsense/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "import_failed_missing_interfaces": "The following OPNsense tracker interfaces were not found: {missing}. Found interfaces were: {found}. Please manually set up a config entry and remove the YAML config", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/oralb/strings.json b/homeassistant/components/oralb/strings.json index 724bd26e813f4e..fca98a1576f6a1 100644 --- a/homeassistant/components/oralb/strings.json +++ b/homeassistant/components/oralb/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "No devices found", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/orvibo/strings.json b/homeassistant/components/orvibo/strings.json index 93ab02b755e0b2..0a7e5f7a050e10 100644 --- a/homeassistant/components/orvibo/strings.json +++ b/homeassistant/components/orvibo/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "Unable to connect to the S20 switch", "cannot_discover": "Unable to discover MAC address of S20 switch. Please enter the MAC address.", "invalid_mac": "Invalid MAC address format" diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json index 60ce2e6ec912f1..c03d42a6c33405 100644 --- a/homeassistant/components/otbr/strings.json +++ b/homeassistant/components/otbr/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "The Thread border router is already configured", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/homeassistant/components/persang_infrared/__init__.py b/homeassistant/components/persang_infrared/__init__.py index 5f9f3eeb3b2053..a2d2d16dfd2a74 100644 --- a/homeassistant/components/persang_infrared/__init__.py +++ b/homeassistant/components/persang_infrared/__init__.py @@ -4,7 +4,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/persang_infrared/button.py b/homeassistant/components/persang_infrared/button.py new file mode 100644 index 00000000000000..ebe7a744f1c336 --- /dev/null +++ b/homeassistant/components/persang_infrared/button.py @@ -0,0 +1,93 @@ +"""Button platform for the Persang Infrared integration. + +Only commands the media player has no slot for live here: the mode and EQ +cycles, scan and repeat, and the numeric keypad. +""" + +from dataclasses import dataclass +from typing import override + +from infrared_protocols.codes.persang.speaker import PersangSpeakerCode + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import CONF_INFRARED_EMITTER_ENTITY_ID +from .entity import PersangIrEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class PersangIrButtonEntityDescription(ButtonEntityDescription): + """Describes a Persang IR button entity.""" + + command_code: PersangSpeakerCode + + +BUTTON_DESCRIPTIONS: tuple[PersangIrButtonEntityDescription, ...] = ( + PersangIrButtonEntityDescription( + key="mode", + translation_key="mode", + command_code=PersangSpeakerCode.MODE, + ), + PersangIrButtonEntityDescription( + key="eq", + translation_key="eq", + command_code=PersangSpeakerCode.EQ, + ), + PersangIrButtonEntityDescription( + key="scan", + translation_key="scan", + command_code=PersangSpeakerCode.SCAN, + ), + PersangIrButtonEntityDescription( + key="repeat", + translation_key="repeat", + command_code=PersangSpeakerCode.REPEAT, + ), + *( + PersangIrButtonEntityDescription( + key=f"num_{digit}", + translation_key=f"num_{digit}", + command_code=PersangSpeakerCode[f"NUM_{digit}"], + ) + for digit in range(10) + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Persang IR buttons from a config entry.""" + infrared_entity_id = entry.data[CONF_INFRARED_EMITTER_ENTITY_ID] + async_add_entities( + PersangIrButton(entry, infrared_entity_id, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class PersangIrButton(PersangIrEntity, ButtonEntity): + """Persang IR button entity.""" + + entity_description: PersangIrButtonEntityDescription + + def __init__( + self, + entry: ConfigEntry, + infrared_entity_id: str, + description: PersangIrButtonEntityDescription, + ) -> None: + """Initialize Persang IR button.""" + super().__init__(entry, infrared_entity_id, unique_id_suffix=description.key) + self.entity_description = description + + @override + async def async_press(self) -> None: + """Press the button.""" + await self._send_command(self.entity_description.command_code.to_command()) diff --git a/homeassistant/components/persang_infrared/entity.py b/homeassistant/components/persang_infrared/entity.py index f90f3f88d46e94..22777214e77647 100644 --- a/homeassistant/components/persang_infrared/entity.py +++ b/homeassistant/components/persang_infrared/entity.py @@ -12,10 +12,19 @@ class PersangIrEntity(InfraredEmitterConsumerEntity): _attr_has_entity_name = True - def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None: + def __init__( + self, + entry: ConfigEntry, + infrared_entity_id: str, + unique_id_suffix: str | None = None, + ) -> None: """Initialize Persang IR entity.""" self._infrared_emitter_entity_id = infrared_entity_id - self._attr_unique_id = entry.entry_id + self._attr_unique_id = ( + entry.entry_id + if unique_id_suffix is None + else f"{entry.entry_id}_{unique_id_suffix}" + ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.entry_id)}, name="Persang speaker", diff --git a/homeassistant/components/persang_infrared/icons.json b/homeassistant/components/persang_infrared/icons.json new file mode 100644 index 00000000000000..46fa6f90d5b375 --- /dev/null +++ b/homeassistant/components/persang_infrared/icons.json @@ -0,0 +1,48 @@ +{ + "entity": { + "button": { + "eq": { + "default": "mdi:equalizer" + }, + "mode": { + "default": "mdi:playlist-music" + }, + "num_0": { + "default": "mdi:numeric-0" + }, + "num_1": { + "default": "mdi:numeric-1" + }, + "num_2": { + "default": "mdi:numeric-2" + }, + "num_3": { + "default": "mdi:numeric-3" + }, + "num_4": { + "default": "mdi:numeric-4" + }, + "num_5": { + "default": "mdi:numeric-5" + }, + "num_6": { + "default": "mdi:numeric-6" + }, + "num_7": { + "default": "mdi:numeric-7" + }, + "num_8": { + "default": "mdi:numeric-8" + }, + "num_9": { + "default": "mdi:numeric-9" + }, + "repeat": { + "default": "mdi:repeat" + }, + "scan": { + "default": "mdi:motion-play" + } + } + } +} diff --git a/homeassistant/components/persang_infrared/quality_scale.yaml b/homeassistant/components/persang_infrared/quality_scale.yaml index f4e31311defae0..2de29a842a6cf4 100644 --- a/homeassistant/components/persang_infrared/quality_scale.yaml +++ b/homeassistant/components/persang_infrared/quality_scale.yaml @@ -100,10 +100,7 @@ rules: status: exempt comment: | This integration does not raise exceptions. - icon-translations: - status: exempt - comment: | - This integration does not use custom icons. + icon-translations: done reconfiguration-flow: todo repair-issues: status: exempt diff --git a/homeassistant/components/persang_infrared/strings.json b/homeassistant/components/persang_infrared/strings.json index a74f5916bfb4c1..3409e3fc2fcdcd 100644 --- a/homeassistant/components/persang_infrared/strings.json +++ b/homeassistant/components/persang_infrared/strings.json @@ -16,5 +16,51 @@ "title": "Set up Persang IR speaker" } } + }, + "entity": { + "button": { + "eq": { + "name": "Equalizer" + }, + "mode": { + "name": "Mode" + }, + "num_0": { + "name": "[%key:common::entity::button::num_0::name%]" + }, + "num_1": { + "name": "[%key:common::entity::button::num_1::name%]" + }, + "num_2": { + "name": "[%key:common::entity::button::num_2::name%]" + }, + "num_3": { + "name": "[%key:common::entity::button::num_3::name%]" + }, + "num_4": { + "name": "[%key:common::entity::button::num_4::name%]" + }, + "num_5": { + "name": "[%key:common::entity::button::num_5::name%]" + }, + "num_6": { + "name": "[%key:common::entity::button::num_6::name%]" + }, + "num_7": { + "name": "[%key:common::entity::button::num_7::name%]" + }, + "num_8": { + "name": "[%key:common::entity::button::num_8::name%]" + }, + "num_9": { + "name": "[%key:common::entity::button::num_9::name%]" + }, + "repeat": { + "name": "Repeat" + }, + "scan": { + "name": "Scan" + } + } } } diff --git a/homeassistant/components/pglab/strings.json b/homeassistant/components/pglab/strings.json index 0ff5c7633e15d6..63c63d76304d37 100644 --- a/homeassistant/components/pglab/strings.json +++ b/homeassistant/components/pglab/strings.json @@ -2,8 +2,7 @@ "config": { "abort": { "mqtt_not_configured": "Home Assistant MQTT integration not configured.", - "mqtt_not_connected": "Home Assistant MQTT integration not connected to MQTT broker.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "mqtt_not_connected": "Home Assistant MQTT integration not connected to MQTT broker." }, "step": { "confirm_from_mqtt": { diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 7a696286c5bb09..e3167d8880412e 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 8e378b9519fb48..02d70e27da36dd 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -3,7 +3,6 @@ "abort": { "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "token_request_timeout": "Timed out obtaining token", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json index 9635c32d7c209f..45cac27236ca85 100644 --- a/homeassistant/components/point/strings.json +++ b/homeassistant/components/point/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/portainer/__init__.py b/homeassistant/components/portainer/__init__.py index 1322134c9fc172..5b0850935a0458 100644 --- a/homeassistant/components/portainer/__init__.py +++ b/homeassistant/components/portainer/__init__.py @@ -145,7 +145,7 @@ def _stop_event_listeners(_event: Event) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Portainer integration.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/portainer/services.py b/homeassistant/components/portainer/services.py index 39efb4d5074647..c2031645828244 100644 --- a/homeassistant/components/portainer/services.py +++ b/homeassistant/components/portainer/services.py @@ -171,7 +171,8 @@ async def recreate_container(call: ServiceCall) -> None: await coordinator.async_request_refresh() -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" hass.services.async_register( diff --git a/homeassistant/components/qingping/strings.json b/homeassistant/components/qingping/strings.json index f85ef2e69208ae..3257ad384c3d26 100644 --- a/homeassistant/components/qingping/strings.json +++ b/homeassistant/components/qingping/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/rapt_ble/strings.json b/homeassistant/components/rapt_ble/strings.json index 5e39fe3eab72aa..8ed563c8cba524 100644 --- a/homeassistant/components/rapt_ble/strings.json +++ b/homeassistant/components/rapt_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/refoss/strings.json b/homeassistant/components/refoss/strings.json index 3eaf68565423ce..e7e08f71352494 100644 --- a/homeassistant/components/refoss/strings.json +++ b/homeassistant/components/refoss/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index beaf5951159a16..f0277fddefc874 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -207,6 +207,6 @@ async def _update_listener(hass: HomeAssistant, entry: RiscoConfigEntry) -> None async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Risco integration services.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/risco/services.py b/homeassistant/components/risco/services.py index dc45511c7b73e9..dc369d61e65c20 100644 --- a/homeassistant/components/risco/services.py +++ b/homeassistant/components/risco/services.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_TIME -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, service from homeassistant.util import dt as dt_util @@ -12,7 +12,8 @@ from .models import RiscoConfigEntry -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Create the Risco Services/Actions.""" async def _set_time(service_call: ServiceCall) -> None: diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index ff9a7104f98ea5..8a3a06aead62be 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", "wrong_device": "This Roku device does not match the existing device ID. Please make sure you entered the correct host information." diff --git a/homeassistant/components/ruuvitag_ble/strings.json b/homeassistant/components/ruuvitag_ble/strings.json index 825dc363888bef..156c512f0bd134 100644 --- a/homeassistant/components/ruuvitag_ble/strings.json +++ b/homeassistant/components/ruuvitag_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/sensirion_ble/strings.json b/homeassistant/components/sensirion_ble/strings.json index f85ef2e69208ae..3257ad384c3d26 100644 --- a/homeassistant/components/sensirion_ble/strings.json +++ b/homeassistant/components/sensirion_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/sensorpro/strings.json b/homeassistant/components/sensorpro/strings.json index f85ef2e69208ae..3257ad384c3d26 100644 --- a/homeassistant/components/sensorpro/strings.json +++ b/homeassistant/components/sensorpro/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json index 5e39fe3eab72aa..8ed563c8cba524 100644 --- a/homeassistant/components/sensorpush/strings.json +++ b/homeassistant/components/sensorpush/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index b131edd2e5e3eb..e284866c066a5e 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -12,7 +12,7 @@ ConfigFlowResult, OptionsFlow, ) -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback from .const import ( CONF_DSN, @@ -56,7 +56,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle a user config flow.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) errors = {} if user_input is not None: diff --git a/homeassistant/components/sentry/strings.json b/homeassistant/components/sentry/strings.json index d15d053524d774..006fef82dfdd90 100644 --- a/homeassistant/components/sentry/strings.json +++ b/homeassistant/components/sentry/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "bad_dsn": "Invalid DSN", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index 47600a4ebe3709..3d8c181ef7a2bc 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -3,7 +3,6 @@ "abort": { "account_mismatch": "The used account does not match the original account", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" diff --git a/homeassistant/components/slimproto/config_flow.py b/homeassistant/components/slimproto/config_flow.py index 99291b8a5c73ea..f1710cb05096f0 100644 --- a/homeassistant/components/slimproto/config_flow.py +++ b/homeassistant/components/slimproto/config_flow.py @@ -3,6 +3,7 @@ from typing import Any, override from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from .const import DEFAULT_NAME, DOMAIN @@ -18,7 +19,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) # we have nothing to configure so simply create the entry return self.async_create_entry(title=DEFAULT_NAME, data={}) diff --git a/homeassistant/components/slimproto/strings.json b/homeassistant/components/slimproto/strings.json index 151035f1efe42b..2e07d4bc64e97b 100644 --- a/homeassistant/components/slimproto/strings.json +++ b/homeassistant/components/slimproto/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "user": {} } diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index c7dc59e0e636a4..4bc77d8dbf0426 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "You selected a different SMA device than the one this config entry was configured with, this is not allowed." diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json index 215ac5382580ae..f6247efa8e921d 100644 --- a/homeassistant/components/snooz/strings.json +++ b/homeassistant/components/snooz/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/sofar/__init__.py b/homeassistant/components/sofar/__init__.py index c232ba686ae4b2..dd455c2a3456d5 100644 --- a/homeassistant/components/sofar/__init__.py +++ b/homeassistant/components/sofar/__init__.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/sofar/binary_sensor.py b/homeassistant/components/sofar/binary_sensor.py new file mode 100644 index 00000000000000..fc1e215ceff4eb --- /dev/null +++ b/homeassistant/components/sofar/binary_sensor.py @@ -0,0 +1,82 @@ +"""Support for Sofar binary sensors.""" + +from dataclasses import dataclass +from typing import override + +from sofar_modbus.modern.faults import FaultCategory + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import SofarConfigEntry +from .entity import SofarEntity, SofarEntityDescription + +PARALLEL_UPDATES = 0 + +_DISABLED_BY_DEFAULT = frozenset( + { + FaultCategory.ARC_FAULT, + FaultCategory.COMBINER_BOX, + FaultCategory.INPUT_FUSE, + FaultCategory.STRING_FUSE, + } +) + + +@dataclass(frozen=True, kw_only=True) +class SofarFaultBinarySensorDescription( + SofarEntityDescription, BinarySensorEntityDescription +): + """Describe a Sofar fault-category binary sensor.""" + + category: FaultCategory + + +FAULT_SENSOR_DESCRIPTIONS: tuple[SofarFaultBinarySensorDescription, ...] = tuple( + SofarFaultBinarySensorDescription( + key=f"fault_{category.value}", + component="state", + translation_key=f"fault_{category.value}", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=category not in _DISABLED_BY_DEFAULT, + category=category, + ) + for category in FaultCategory +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SofarConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Sofar Inverter Modbus binary sensor platform.""" + runtime_data = entry.runtime_data + served = runtime_data.served_components + async_add_entities( + SofarFaultBinarySensor(runtime_data, description) + for description in FAULT_SENSOR_DESCRIPTIONS + if description.component in served + ) + + +class SofarFaultBinarySensor(SofarEntity, BinarySensorEntity): + """Reports whether any fault in one subsystem is currently active.""" + + entity_description: SofarFaultBinarySensorDescription + + @property + @override + def is_on(self) -> bool: + component = getattr(self.coordinator.device, self.entity_description.component) + return any( + fault.category is self.entity_description.category + for fault in component.active_faults + ) diff --git a/homeassistant/components/sofar/diagnostics.py b/homeassistant/components/sofar/diagnostics.py index 8a4b2044aacf72..3ecea362805e4c 100644 --- a/homeassistant/components/sofar/diagnostics.py +++ b/homeassistant/components/sofar/diagnostics.py @@ -29,6 +29,7 @@ async def async_get_config_entry_diagnostics( "serial_number": device.serial_number, "readings_components": device.readings_components, "settings_components": device.settings_components, + "active_faults": sorted(fault.key for fault in device.state.active_faults), "raw": raw, }, TO_REDACT, diff --git a/homeassistant/components/sofar/manifest.json b/homeassistant/components/sofar/manifest.json index ed8862c8f13636..51e28d8743911a 100644 --- a/homeassistant/components/sofar/manifest.json +++ b/homeassistant/components/sofar/manifest.json @@ -8,5 +8,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["sofar-modbus==0.8.0"] + "requirements": ["sofar-modbus==0.9.0"] } diff --git a/homeassistant/components/sofar/strings.json b/homeassistant/components/sofar/strings.json index c9be7334977bc4..5e9c01bda5216b 100644 --- a/homeassistant/components/sofar/strings.json +++ b/homeassistant/components/sofar/strings.json @@ -46,6 +46,59 @@ } }, "entity": { + "binary_sensor": { + "fault_ac_output": { + "name": "AC output fault" + }, + "fault_arc_fault": { + "name": "Arc fault (AFCI)" + }, + "fault_battery": { + "name": "Battery fault" + }, + "fault_battery_pack": { + "name": "Battery pack communication fault" + }, + "fault_combiner_box": { + "name": "Combiner box fault" + }, + "fault_communication": { + "name": "Communication fault" + }, + "fault_dc_bus": { + "name": "DC bus fault" + }, + "fault_derating": { + "name": "Derating" + }, + "fault_fan": { + "name": "Fan fault" + }, + "fault_grid": { + "name": "Grid fault" + }, + "fault_input_fuse": { + "name": "Input fuse open" + }, + "fault_insulation": { + "name": "Insulation fault" + }, + "fault_internal": { + "name": "Internal fault" + }, + "fault_pv": { + "name": "PV fault" + }, + "fault_shutdown": { + "name": "Shutdown" + }, + "fault_string_fuse": { + "name": "String fuse open" + }, + "fault_thermal": { + "name": "Thermal fault" + } + }, "button": { "iv_curve_scan": { "name": "IV curve scan" diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index b17818ddc3cbba..b9270998ae8f08 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -2,8 +2,7 @@ "config": { "abort": { "not_ipv4_address": "No IPv4 address in SSDP discovery information", - "not_sonos_device": "Discovered device is not a Sonos device", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "not_sonos_device": "Discovered device is not a Sonos device" }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 220642bd50a49f..0b32db20828a8c 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -9,7 +9,7 @@ ConfigFlowResult, OptionsFlowWithReload, ) -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback from .const import ( CONF_SERVER_ID, @@ -41,7 +41,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) if user_input is None: return self.async_show_form(step_id="user") diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json index 244549ad1796ac..f288dd3d6c73f5 100644 --- a/homeassistant/components/speedtestdotnet/strings.json +++ b/homeassistant/components/speedtestdotnet/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/splunk/strings.json b/homeassistant/components/splunk/strings.json index cd5068317f4407..b4e82c9ab01859 100644 --- a/homeassistant/components/splunk/strings.json +++ b/homeassistant/components/splunk/strings.json @@ -7,7 +7,6 @@ "invalid_config": "The YAML configuration is invalid and cannot be imported. Please check your configuration.yaml file.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index acca0cbdbda586..427b2dbdb7b693 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_server_found": "No LMS found." }, "error": { diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 53cc147d7879a2..d35b83338e6792 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -28,7 +28,7 @@ from .const import CONF_CLOUDHOOK_URL, DEVICE_SUPPORT_MAP, DOMAIN, ENTRY_TITLE from .coordinator import SwitchBotCoordinator -from .service import async_register_services +from .services import async_setup_services CONFIG_SCHEMA = cv.config_entry_only_config_schema("switchbot_cloud") @@ -280,7 +280,7 @@ async def make_new_device_data( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Switchbot Cloud.""" - async_register_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/switchbot_cloud/service.py b/homeassistant/components/switchbot_cloud/services.py similarity index 87% rename from homeassistant/components/switchbot_cloud/service.py rename to homeassistant/components/switchbot_cloud/services.py index 70250c8e402380..2924ca7e092d73 100644 --- a/homeassistant/components/switchbot_cloud/service.py +++ b/homeassistant/components/switchbot_cloud/services.py @@ -1,11 +1,11 @@ -"""SwitchBot Cloud Custom Service.""" +"""Support for SwitchBot Cloud services.""" from logging import getLogger from switchbot_api import ArtFrameCommands import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, service @@ -49,8 +49,9 @@ async def handle_upload_image(call: ServiceCall) -> None: ) -def async_register_services(hass: HomeAssistant) -> None: - """Async register services.""" +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register the SwitchBot Cloud services.""" hass.services.async_register( DOMAIN, AI_ART_FRAME_UPLOAD_IMAGE_SERVICE, diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json index 32a00fc442a27a..d632db07e8e18d 100644 --- a/homeassistant/components/switcher_kis/strings.json +++ b/homeassistant/components/switcher_kis/strings.json @@ -2,8 +2,7 @@ "config": { "abort": { "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index 97f8f16af84a4c..bcca38d7a4f9b9 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::already_configured_service%]" - }, "step": { "user": { "description": "Select **Submit** for initial setup. On the created config entry, select configure to add sensors for selected processes" diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index 7ccf7d88c1d2c3..68f2d6570ba3d9 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,6 +6,7 @@ from homeassistant.components.mqtt import valid_subscribe_topic from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN @@ -26,7 +27,10 @@ async def async_step_mqtt( ) -> ConfigFlowResult: """Handle a flow initialized by MQTT discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(DOMAIN) @@ -50,7 +54,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) return await self.async_step_config() diff --git a/homeassistant/components/tasmota/strings.json b/homeassistant/components/tasmota/strings.json index 033a161a1b57ea..b96b1bddffebc6 100644 --- a/homeassistant/components/tasmota/strings.json +++ b/homeassistant/components/tasmota/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "invalid_discovery_topic": "Invalid discovery topic prefix." }, diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json index b79f9f5129ec2f..795124bb707712 100644 --- a/homeassistant/components/tesla_fleet/strings.json +++ b/homeassistant/components/tesla_fleet/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Configuration updated for profile.", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_account_mismatch": "The reauthentication account does not match the original account", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" diff --git a/homeassistant/components/thermobeacon/strings.json b/homeassistant/components/thermobeacon/strings.json index f85ef2e69208ae..3257ad384c3d26 100644 --- a/homeassistant/components/thermobeacon/strings.json +++ b/homeassistant/components/thermobeacon/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported" }, diff --git a/homeassistant/components/thermopro/strings.json b/homeassistant/components/thermopro/strings.json index 84397872d55586..01628a9e3b8837 100644 --- a/homeassistant/components/thermopro/strings.json +++ b/homeassistant/components/thermopro/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/tilt_ble/strings.json b/homeassistant/components/tilt_ble/strings.json index 5e39fe3eab72aa..8ed563c8cba524 100644 --- a/homeassistant/components/tilt_ble/strings.json +++ b/homeassistant/components/tilt_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "flow_title": "{name}", diff --git a/homeassistant/components/todoist/config_flow.py b/homeassistant/components/todoist/config_flow.py index cb0150c24c2dbd..c8432a64d37dbc 100644 --- a/homeassistant/components/todoist/config_flow.py +++ b/homeassistant/components/todoist/config_flow.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TOKEN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from .const import DOMAIN @@ -35,7 +36,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index fb46f24efe7aa7..52dc1413041dbf 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" }, diff --git a/homeassistant/components/togrill/strings.json b/homeassistant/components/togrill/strings.json index b7909537710591..878508f562b591 100644 --- a/homeassistant/components/togrill/strings.json +++ b/homeassistant/components/togrill/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "failed_to_read_config": "Failed to read config from device", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json index fcf1e729aa8d57..b15361ae396e58 100644 --- a/homeassistant/components/traccar/strings.json +++ b/homeassistant/components/traccar/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to webhooks in the Traccar Client and update the webhook with the following URL: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 740ae3f924b605..e256059c24c300 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_authenticate": "Cannot authenticate, is your gateway paired with another server like e.g. HomeKit?", diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 1dc7709a84a124..c58b255cc31c0a 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -29,7 +29,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tuya Services.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/tuya/services.py b/homeassistant/components/tuya/services.py index bef24571c2e5b3..0a6bfa61968fc6 100644 --- a/homeassistant/components/tuya/services.py +++ b/homeassistant/components/tuya/services.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr @@ -129,7 +129,8 @@ async def async_set_feeder_meal_plan(call: ServiceCall) -> None: ) -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up Tuya services.""" hass.services.async_register( diff --git a/homeassistant/components/twilio/strings.json b/homeassistant/components/twilio/strings.json index bb3a726fb5e670..7986b4d7a9e0ac 100644 --- a/homeassistant/components/twilio/strings.json +++ b/homeassistant/components/twilio/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "reconfigure_successful": "**Reconfiguration was successful**\n\nGo to [webhooks in Twilio]({twilio_url}) and update the webhook with the following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { diff --git a/homeassistant/components/unifi_access/__init__.py b/homeassistant/components/unifi_access/__init__.py index 5c7956eb85b9a6..571d06ee762c6e 100644 --- a/homeassistant/components/unifi_access/__init__.py +++ b/homeassistant/components/unifi_access/__init__.py @@ -29,7 +29,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the UniFi Access integration.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/unifi_access/services.py b/homeassistant/components/unifi_access/services.py index 190eddf0f742b2..47643a53f8fff2 100644 --- a/homeassistant/components/unifi_access/services.py +++ b/homeassistant/components/unifi_access/services.py @@ -64,7 +64,8 @@ def _async_get_target( ) -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for the UniFi Access integration.""" async def _handle_set_lock_rule(call: ServiceCall) -> None: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 528057952030f7..5acec229951889 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["uiprotect"], "quality_scale": "platinum", - "requirements": ["uiprotect==16.7.0"] + "requirements": ["uiprotect==16.8.0"] } diff --git a/homeassistant/components/vegehub/strings.json b/homeassistant/components/vegehub/strings.json index c48ffdd66df133..297ae90270be0c 100644 --- a/homeassistant/components/vegehub/strings.json +++ b/homeassistant/components/vegehub/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "Device already detected. Check discovered devices.", "cannot_connect": "Failed to connect to the device. Please try again.", "timeout_connect": "Timed out connecting. Ensure VegeHub is awake, and try again.", "unknown_error": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 5203c2e82c3ef3..b4868b3c161502 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -5,6 +5,7 @@ from typing import Any, override from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo @@ -35,7 +36,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self.source != SOURCE_REAUTH and self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) return await super().async_step_user(user_input) @@ -81,6 +85,9 @@ async def async_step_dhcp( self._abort_if_unique_id_configured() if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) return await self.async_step_user() diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index a528e2ca33b645..ad723aed14f2a7 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -7,7 +7,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "create_entry": { diff --git a/homeassistant/components/victron_ble/strings.json b/homeassistant/components/victron_ble/strings.json index 6c1747c3301e0d..be63312d11e059 100644 --- a/homeassistant/components/victron_ble/strings.json +++ b/homeassistant/components/victron_ble/strings.json @@ -8,7 +8,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_access_token": "Invalid encryption key for instant readout", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Device not supported", diff --git a/homeassistant/components/vistapool/strings.json b/homeassistant/components/vistapool/strings.json index 8d829e4abafd21..a5ad2d081724fa 100644 --- a/homeassistant/components/vistapool/strings.json +++ b/homeassistant/components/vistapool/strings.json @@ -3,7 +3,6 @@ "abort": { "account_mismatch": "The credentials entered are for a different Vistapool account than the one being reconfigured.", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, diff --git a/homeassistant/components/voip/config_flow.py b/homeassistant/components/voip/config_flow.py index daa916d3d4c1d4..915fe26147b7e3 100644 --- a/homeassistant/components/voip/config_flow.py +++ b/homeassistant/components/voip/config_flow.py @@ -11,7 +11,7 @@ ConfigFlowResult, OptionsFlow, ) -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback from homeassistant.helpers import config_validation as cv from .const import CONF_SIP_PORT, CONF_SIP_USER, DOMAIN @@ -28,7 +28,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/voip/strings.json b/homeassistant/components/voip/strings.json index d0502190b4be61..9f5f15d7813733 100644 --- a/homeassistant/components/voip/strings.json +++ b/homeassistant/components/voip/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "user": { "description": "Receive Voice over IP calls to interact with Assist." diff --git a/homeassistant/components/volvo/__init__.py b/homeassistant/components/volvo/__init__.py index 5808641ab7676d..902936151fae94 100644 --- a/homeassistant/components/volvo/__init__.py +++ b/homeassistant/components/volvo/__init__.py @@ -39,7 +39,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Volvo integration.""" - await async_setup_services(hass) + async_setup_services(hass) return True diff --git a/homeassistant/components/volvo/services.py b/homeassistant/components/volvo/services.py index a3c2b2838bd98c..0eaabae1104473 100644 --- a/homeassistant/components/volvo/services.py +++ b/homeassistant/components/volvo/services.py @@ -8,7 +8,7 @@ from httpx import AsyncClient, HTTPError, HTTPStatusError import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, service from homeassistant.helpers.httpx_client import get_async_client @@ -53,7 +53,8 @@ } -async def async_setup_services(hass: HomeAssistant) -> None: +@callback +def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" hass.services.async_register( diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index f9280c242a831b..6b8ab4aed00dae 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -5,7 +5,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" diff --git a/homeassistant/components/watts/strings.json b/homeassistant/components/watts/strings.json index 1ecac929bc7b03..4c20f220fb6b5c 100644 --- a/homeassistant/components/watts/strings.json +++ b/homeassistant/components/watts/strings.json @@ -3,7 +3,6 @@ "abort": { "account_mismatch": "The authenticated account does not match the configured account", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_token": "The provided access token is invalid.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/weatherflow/config_flow.py b/homeassistant/components/weatherflow/config_flow.py index 1c11ae4b933d9d..e7c878fc397e76 100644 --- a/homeassistant/components/weatherflow/config_flow.py +++ b/homeassistant/components/weatherflow/config_flow.py @@ -9,7 +9,7 @@ from pyweatherflowudp.errors import AddressInUseError, EndpointError, ListenerError from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback from .const import ( DOMAIN, @@ -55,7 +55,10 @@ async def async_step_user( # will pick up all devices on the network and we don't want to # create multiple entries. if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) found = False errors = {} try: diff --git a/homeassistant/components/weatherflow/strings.json b/homeassistant/components/weatherflow/strings.json index eb562131709446..2f0f18eca3b0df 100644 --- a/homeassistant/components/weatherflow/strings.json +++ b/homeassistant/components/weatherflow/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "address_in_use": "Unable to open local UDP port 50222.", "cannot_connect": "UDP discovery error.", diff --git a/homeassistant/components/weheat/manifest.json b/homeassistant/components/weheat/manifest.json index 98a147b72dd68d..8b79cd0ca6656d 100644 --- a/homeassistant/components/weheat/manifest.json +++ b/homeassistant/components/weheat/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/weheat", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["weheat==2026.4.8"] + "requirements": ["weheat==2026.9.3"] } diff --git a/homeassistant/components/weheat/strings.json b/homeassistant/components/weheat/strings.json index b2a50174e14782..b84c19ea58bb86 100644 --- a/homeassistant/components/weheat/strings.json +++ b/homeassistant/components/weheat/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_devices_found": "Could not find any heat pumps on this account", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", @@ -114,8 +113,10 @@ "heating": "Heating", "legionella_prevention": "Legionella prevention", "manual_control": "Manual control", + "offline": "Offline", "self_test": "Self-test", "standby": "[%key:common::state::standby%]", + "updating": "Updating", "water_check": "Checking water temperature" } }, diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json index 874f4633c214b7..eaefaa709a3272 100644 --- a/homeassistant/components/wemo/strings.json +++ b/homeassistant/components/wemo/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "Do you want to set up Wemo?" diff --git a/homeassistant/components/wiim/strings.json b/homeassistant/components/wiim/strings.json index c9af2837c25416..0b755f1ef9fe14 100644 --- a/homeassistant/components/wiim/strings.json +++ b/homeassistant/components/wiim/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unique_id_mismatch": "The host belongs to a different WiiM device." diff --git a/homeassistant/components/wmspro/strings.json b/homeassistant/components/wmspro/strings.json index 988d20e1efca68..6d98e33c1a033b 100644 --- a/homeassistant/components/wmspro/strings.json +++ b/homeassistant/components/wmspro/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index b0a074073badac..60396c0cfa8713 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways" }, "error": { diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 30e5792e647f66..6b18d34203c94c 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "api_error": "Error while communicating with Mi API: {error_detail}", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index dd7781d97a4519..5efc44b57621b0 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "incomplete_info": "Incomplete information to set up device, no host or token supplied.", "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Home integration.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/yale/strings.json b/homeassistant/components/yale/strings.json index 1f961dd1b98503..76a5c0ea118f73 100644 --- a/homeassistant/components/yale/strings.json +++ b/homeassistant/components/yale/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_invalid_user": "Reauthenticate must use the same account.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 479908c15e0e11..35c28b3bd5ccf3 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, diff --git a/homeassistant/components/zerproc/strings.json b/homeassistant/components/zerproc/strings.json index 7b2ae512458d0a..0a9cf0004bc939 100644 --- a/homeassistant/components/zerproc/strings.json +++ b/homeassistant/components/zerproc/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index ec7e147bf51f81..a434c502a3cca1 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -31,7 +31,7 @@ OptionsFlow, ) from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( @@ -828,7 +828,10 @@ async def async_step_confirm( # Abort discovery if the device path is already configured if path is not None and path in current_device_paths: - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) # Without confirmation, discovery can automatically progress into parts of the # config flow logic that interacts with hardware. @@ -1032,7 +1035,10 @@ async def _async_create_radio_entry(self) -> ConfigFlowResult: return self.async_create_entry(title="", data=data) # This should never be reached - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) class ZhaOptionsFlowHandler(BaseZhaFlow, OptionsFlow): diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 50b5cc71802fee..175e8a2b9cf93d 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -8,7 +8,6 @@ "invalid_zeroconf_data": "The coordinator has invalid Zeroconf service info and cannot be identified by ZHA", "not_zha_device": "This device is not a ZHA device", "reconfigure_successful": "ZHA has successfully migrated from your old adapter to the new one. Give your Zigbee network a few minutes to stabilize.\n\nIf you no longer need the old adapter, you can now unplug it.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "usb_probe_failed": "Failed to probe the USB device", "wrong_firmware_installed": "Your device is running the wrong firmware and cannot be used with ZHA until the correct firmware is installed. [A repair has been created]({repair_url}) with more information and instructions for how to fix this." }, diff --git a/homeassistant/components/zodiac/config_flow.py b/homeassistant/components/zodiac/config_flow.py index b226b9d29664d7..81e839eeb40327 100644 --- a/homeassistant/components/zodiac/config_flow.py +++ b/homeassistant/components/zodiac/config_flow.py @@ -3,6 +3,7 @@ from typing import Any, override from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from .const import DEFAULT_NAME, DOMAIN @@ -18,7 +19,10 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) diff --git a/homeassistant/components/zodiac/strings.json b/homeassistant/components/zodiac/strings.json index 2c534cef989031..af971f3fe8b27b 100644 --- a/homeassistant/components/zodiac/strings.json +++ b/homeassistant/components/zodiac/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "step": { "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae9ae2da1cf2e3..f3fa67f9320108 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3210,7 +3210,9 @@ async def async_set_unique_id( include_uninitialized=True, match_context={"unique_id": unique_id} ) ): - raise data_entry_flow.AbortFlow("already_in_progress") + raise data_entry_flow.AbortFlow( + "already_in_progress", translation_domain=HOMEASSISTANT_DOMAIN + ) self.context["unique_id"] = unique_id @@ -3308,8 +3310,7 @@ async def _async_handle_discovery_without_unique_id(self) -> None: It ensures that the discovery can be ignored by the user. - Requires `already_configured` and `already_in_progress` in strings.json - in user visible flows. + Requires `already_configured` in strings.json in user visible flows. """ if self.unique_id is not None: return @@ -3324,7 +3325,9 @@ async def _async_handle_discovery_without_unique_id(self) -> None: # Abort if any other flow for this handler is already in progress if self._async_in_progress(include_uninitialized=True): - raise data_entry_flow.AbortFlow("already_in_progress") + raise data_entry_flow.AbortFlow( + "already_in_progress", translation_domain=HOMEASSISTANT_DOMAIN + ) async def _async_step_discovery_without_unique_id( self, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index c46188a329503e..e6d195f68d4038 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -47,7 +47,10 @@ async def async_step_user( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain, raise_on_progress=False) @@ -83,7 +86,10 @@ async def async_step_confirm( self.hass.config_entries.flow.async_abort(flow["flow_id"]) if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) return self.async_create_entry(title=self._title, data={}) @@ -93,7 +99,10 @@ async def async_step_discovery( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -105,7 +114,10 @@ async def async_step_bluetooth( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -117,7 +129,10 @@ async def async_step_dhcp( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -129,7 +144,10 @@ async def async_step_homekit( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by Homekit discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -141,7 +159,10 @@ async def async_step_mqtt( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by mqtt discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -153,7 +174,10 @@ async def async_step_zeroconf( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by Zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -165,7 +189,10 @@ async def async_step_ssdp( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by Ssdp discovery.""" if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) await self.async_set_unique_id(self._domain) @@ -176,7 +203,10 @@ async def async_step_import( ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by import.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) # Cancel other flows. in_progress = self._async_in_progress() @@ -230,7 +260,10 @@ async def async_step_user( and self._async_current_entries() and self.source != config_entries.SOURCE_RECONFIGURE ): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort( + reason="single_instance_allowed", + translation_domain=HOMEASSISTANT_DOMAIN, + ) if user_input is None: return self.async_show_form( diff --git a/requirements.txt b/requirements.txt index 674ee602aa81db..87ed11507892c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ home-assistant-bluetooth==2.0.0 home-assistant-intents==2026.8.28 httpx==0.28.1 ifaddr==0.2.0 -infrared-protocols==9.1.0 +infrared-protocols==10.0.0 Jinja2==3.1.6 lru-dict==1.4.1 mutagen==1.48.1 diff --git a/requirements_all.txt b/requirements_all.txt index 5fc9c2cc5e16b5..55119bdec4cb45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ influxdb-client==1.50.0 influxdb==5.3.2 # homeassistant.components.infrared -infrared-protocols==9.1.0 +infrared-protocols==10.0.0 # homeassistant.components.inkbird inkbird-ble==1.4.4 @@ -3114,7 +3114,7 @@ snapcast==2.3.8 soco==0.31.2 # homeassistant.components.sofar -sofar-modbus==0.8.0 +sofar-modbus==0.9.0 # homeassistant.components.solaredge_local solaredge-local==0.2.3 @@ -3326,7 +3326,7 @@ uasiren==0.0.1 uhooapi==1.2.8 # homeassistant.components.unifiprotect -uiprotect==16.7.0 +uiprotect==16.8.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.6.1 @@ -3440,7 +3440,7 @@ webio-api==0.1.12 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2026.4.8 +weheat==2026.9.3 # homeassistant.components.whirlpool whirlpool-sixth-sense==1.3.1 diff --git a/tests/components/amberelectric/test_helpers.py b/tests/components/amberelectric/test_helpers.py index 958c60fd1b3307..7b5167492fdb5d 100644 --- a/tests/components/amberelectric/test_helpers.py +++ b/tests/components/amberelectric/test_helpers.py @@ -1,8 +1,12 @@ """Test formatters.""" from amberelectric.models.price_descriptor import PriceDescriptor +import pytest -from homeassistant.components.amberelectric.helpers import normalize_descriptor +from homeassistant.components.amberelectric.helpers import ( + format_cents_to_dollars, + normalize_descriptor, +) def test_normalize_descriptor() -> None: @@ -15,3 +19,18 @@ def test_normalize_descriptor() -> None: assert normalize_descriptor(PriceDescriptor.NEUTRAL) == "neutral" assert normalize_descriptor(PriceDescriptor.HIGH) == "high" assert normalize_descriptor(PriceDescriptor.SPIKE) == "spike" + + +@pytest.mark.parametrize( + ("cents", "expected"), + [ + pytest.param(8.8, 8.8 / 100, id="general"), + pytest.param(4.4, 4.4 / 100, id="controlled_load"), + pytest.param(1.1, 1.1 / 100, id="feed_in"), + pytest.param(0.09619, 0.0009619, id="near_zero"), + pytest.param(-0.09619, -0.0009619, id="near_zero_negative"), + ], +) +def test_format_cents_to_dollars(cents: float, expected: float) -> None: + """Test cents-to-dollars conversion preserves source precision.""" + assert format_cents_to_dollars(cents) == expected diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 0d979a2021c48b..36e489bb77b887 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -16,11 +16,11 @@ async def test_general_price_sensor( assert len(hass.states.async_all()) == 6 price = hass.states.get("sensor.mock_title_general_price") assert price - assert price.state == "0.09" + assert price.state == "0.088" attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == 0.09 + assert attributes["per_kwh"] == 8.8 / 100 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" @@ -44,8 +44,8 @@ async def test_general_price_sensor_with_range( price = hass.states.get("sensor.mock_title_general_price") assert price attributes = price.attributes - assert attributes.get("range_min") == 0.07 - assert attributes.get("range_max") == 0.09 + assert attributes.get("range_min") == 6.7 / 100 + assert attributes.get("range_max") == 9.1 / 100 @pytest.mark.usefixtures("mock_amber_client_general_and_controlled_load") @@ -58,11 +58,11 @@ async def test_general_and_controlled_load_price_sensor( assert len(hass.states.async_all()) == 9 price = hass.states.get("sensor.mock_title_controlled_load_price") assert price - assert price.state == "0.04" + assert price.state == "0.044" attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == 0.04 + assert attributes["per_kwh"] == 4.4 / 100 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" @@ -83,11 +83,11 @@ async def test_general_and_feed_in_price_sensor( assert len(hass.states.async_all()) == 9 price = hass.states.get("sensor.mock_title_feed_in_price") assert price - assert price.state == "-0.01" + assert price.state == "-0.011" attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == -0.01 + assert attributes["per_kwh"] == -1.1 / 100 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" @@ -108,7 +108,7 @@ async def test_general_forecast_sensor( assert len(hass.states.async_all()) == 6 price = hass.states.get("sensor.mock_title_general_forecast") assert price - assert price.state == "0.09" + assert price.state == "0.088" attributes = price.attributes assert attributes["channel_type"] == "general" assert attributes["attribution"] == "Data provided by Amber Electric" @@ -116,9 +116,9 @@ async def test_general_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == 0.09 + assert first_forecast["per_kwh"] == 8.8 / 100 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 0.01 + assert first_forecast["spot_per_kwh"] == 1.1 / 100 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 @@ -140,8 +140,8 @@ async def test_general_forecast_sensor_with_range( assert price attributes = price.attributes first_forecast = attributes["forecasts"][0] - assert first_forecast.get("range_min") == 0.07 - assert first_forecast.get("range_max") == 0.09 + assert first_forecast.get("range_min") == 6.7 / 100 + assert first_forecast.get("range_max") == 9.1 / 100 @pytest.mark.usefixtures("mock_amber_client_general_and_controlled_load") @@ -154,7 +154,7 @@ async def test_controlled_load_forecast_sensor( assert len(hass.states.async_all()) == 9 price = hass.states.get("sensor.mock_title_controlled_load_forecast") assert price - assert price.state == "0.04" + assert price.state == "0.044" attributes = price.attributes assert attributes["channel_type"] == "controlledLoad" assert attributes["attribution"] == "Data provided by Amber Electric" @@ -162,9 +162,9 @@ async def test_controlled_load_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == 0.04 + assert first_forecast["per_kwh"] == 4.4 / 100 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 0.01 + assert first_forecast["spot_per_kwh"] == 1.1 / 100 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 @@ -181,7 +181,7 @@ async def test_feed_in_forecast_sensor( assert len(hass.states.async_all()) == 9 price = hass.states.get("sensor.mock_title_feed_in_forecast") assert price - assert price.state == "-0.01" + assert price.state == "-0.011" attributes = price.attributes assert attributes["channel_type"] == "feedIn" assert attributes["attribution"] == "Data provided by Amber Electric" @@ -189,9 +189,9 @@ async def test_feed_in_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == -0.01 + assert first_forecast["per_kwh"] == -1.1 / 100 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 0.01 + assert first_forecast["spot_per_kwh"] == 1.1 / 100 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 diff --git a/tests/components/amberelectric/test_services.py b/tests/components/amberelectric/test_services.py index 2bc4546949ed60..26a5503a39bb1f 100644 --- a/tests/components/amberelectric/test_services.py +++ b/tests/components/amberelectric/test_services.py @@ -41,8 +41,8 @@ async def test_get_general_forecasts( assert first["duration"] == 30 assert first["date"] == "2021-09-21" assert first["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first["per_kwh"] == 0.09 - assert first["spot_per_kwh"] == 0.01 + assert first["per_kwh"] == 8.8 / 100 + assert first["spot_per_kwh"] == 1.1 / 100 assert first["start_time"] == "2021-09-21T08:30:00+10:00" assert first["end_time"] == "2021-09-21T09:00:00+10:00" assert first["renewables"] == 50 @@ -73,8 +73,8 @@ async def test_get_controlled_load_forecasts( assert first["duration"] == 30 assert first["date"] == "2021-09-21" assert first["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first["per_kwh"] == 0.04 - assert first["spot_per_kwh"] == 0.01 + assert first["per_kwh"] == 4.4 / 100 + assert first["spot_per_kwh"] == 1.1 / 100 assert first["start_time"] == "2021-09-21T08:30:00+10:00" assert first["end_time"] == "2021-09-21T09:00:00+10:00" assert first["renewables"] == 50 @@ -105,8 +105,8 @@ async def test_get_feed_in_forecasts( assert first["duration"] == 30 assert first["date"] == "2021-09-21" assert first["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first["per_kwh"] == -0.01 - assert first["spot_per_kwh"] == 0.01 + assert first["per_kwh"] == -1.1 / 100 + assert first["spot_per_kwh"] == 1.1 / 100 assert first["start_time"] == "2021-09-21T08:30:00+10:00" assert first["end_time"] == "2021-09-21T09:00:00+10:00" assert first["renewables"] == 50 diff --git a/tests/components/daikin/test_zone_climate.py b/tests/components/daikin/test_zone_climate.py index 2ca160f8a76894..78515736f263fc 100644 --- a/tests/components/daikin/test_zone_climate.py +++ b/tests/components/daikin/test_zone_climate.py @@ -279,29 +279,31 @@ async def test_zone_climate_parameters_unavailable( assert state.state == STATE_UNAVAILABLE -async def test_zone_climate_hvac_modes_read_only( +async def test_zone_climate_rejects_main_mode_change( hass: HomeAssistant, entity_registry: er.EntityRegistry, zone_device: ZoneDevice, ) -> None: - """Changing HVAC mode through a zone climate is blocked.""" + """Changing the main HVAC mode through a zone climate is blocked.""" configure_zone_device(zone_device, zones=[["Living", "1", 22]]) await _async_setup_daikin(hass, zone_device) entity_id = _zone_entity_id(entity_registry, zone_device, 0) assert entity_id is not None - with pytest.raises(HomeAssistantError) as err: + with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: entity_id, - ATTR_HVAC_MODE: HVACMode.HEAT, + ATTR_HVAC_MODE: HVACMode.COOL, }, blocking=True, ) - assert err.value.translation_key == "zone_hvac_read_only" + assert err.value.translation_key == "not_valid_hvac_mode" + zone_device.set_zone.assert_not_awaited() + zone_device.set.assert_not_awaited() async def test_zone_climate_set_temperature_requires_heat_or_cool( @@ -360,7 +362,7 @@ async def test_zone_climate_properties( assert state.attributes[ATTR_TEMPERATURE] == 18.0 assert state.attributes[ATTR_MIN_TEMP] == 22.0 assert state.attributes[ATTR_MAX_TEMP] == 26.0 - assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.COOL] + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.COOL, HVACMode.OFF] assert state.attributes[ATTR_SUPPORTED_FEATURES] == ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF @@ -377,6 +379,7 @@ async def test_zone_climate_properties( "expected_climate_state", "expected_switch_state", "main_mode", + "service_data", ), [ pytest.param( @@ -386,6 +389,7 @@ async def test_zone_climate_properties( HVACMode.COOL, STATE_ON, "cool", + {}, id="climate-turn-on", ), pytest.param( @@ -395,6 +399,7 @@ async def test_zone_climate_properties( HVACMode.OFF, STATE_OFF, "cool", + {}, id="climate-turn-off", ), pytest.param( @@ -404,6 +409,7 @@ async def test_zone_climate_properties( HVACMode.OFF, STATE_OFF, "cool", + {}, id="climate-toggle-off", ), pytest.param( @@ -413,8 +419,29 @@ async def test_zone_climate_properties( HVACMode.OFF, STATE_OFF, "off", + {}, id="climate-toggle-zone-on-main-off", ), + pytest.param( + SERVICE_SET_HVAC_MODE, + "0", + "1", + HVACMode.HEAT, + STATE_ON, + "hot", + {ATTR_HVAC_MODE: HVACMode.HEAT}, + id="hvac-mode-turn-on", + ), + pytest.param( + SERVICE_SET_HVAC_MODE, + "1", + "0", + HVACMode.OFF, + STATE_OFF, + "hot", + {ATTR_HVAC_MODE: HVACMode.OFF}, + id="hvac-mode-turn-off", + ), ], ) async def test_zone_climate_power_controls( @@ -427,6 +454,7 @@ async def test_zone_climate_power_controls( expected_climate_state: HVACMode, expected_switch_state: str, main_mode: str, + service_data: dict[str, HVACMode], ) -> None: """Zone climate and switch power controls stay synchronized.""" configure_zone_device( @@ -449,7 +477,7 @@ async def set_zone(zone_id: int, key: str, value: str) -> None: await hass.services.async_call( CLIMATE_DOMAIN, service, - {ATTR_ENTITY_ID: climate_entity_id}, + {ATTR_ENTITY_ID: climate_entity_id, **service_data}, blocking=True, ) @@ -504,10 +532,15 @@ async def set_zone(zone_id: int, key: str, value: str) -> None: @pytest.mark.parametrize( - ("mode", "expected_state"), + ("mode", "expected_state", "expected_modes"), [ - pytest.param("auto", HVACMode.HEAT_COOL, id="auto"), - pytest.param("off", HVACMode.OFF, id="off"), + pytest.param( + "auto", + HVACMode.HEAT_COOL, + [HVACMode.HEAT_COOL, HVACMode.OFF], + id="auto", + ), + pytest.param("off", HVACMode.OFF, [HVACMode.OFF], id="off"), ], ) async def test_zone_climate_target_temperature_inactive_mode( @@ -516,6 +549,7 @@ async def test_zone_climate_target_temperature_inactive_mode( zone_device: ZoneDevice, mode: str, expected_state: HVACMode, + expected_modes: list[HVACMode], ) -> None: """In non-heating/cooling modes, zone target temperature is None.""" configure_zone_device( @@ -532,6 +566,7 @@ async def test_zone_climate_target_temperature_inactive_mode( state = hass.states.get(entity_id) assert state is not None assert state.state == expected_state + assert state.attributes[ATTR_HVAC_MODES] == expected_modes assert state.attributes[ATTR_TEMPERATURE] is None diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index b23939cb4030a6..c2e612dfd78477 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -368,7 +368,9 @@ async def test_not_allowing_recursion( async def test_reload_config_entry_by_entity_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test being able to reload a config entry by entity_id.""" await async_setup_component(hass, DOMAIN, {}) @@ -398,6 +400,11 @@ async def test_reload_config_entry_by_entity_id( entry1.entry_id, entry2.entry_id, } + assert ( + "Reloading a config entry by target is deprecated and will stop working in " + "Home Assistant 2027.4, please specify the config entry to reload in the " + "'entry_id' parameter instead" + ) in caplog.text with pytest.raises(ValueError): await hass.services.async_call( @@ -408,7 +415,9 @@ async def test_reload_config_entry_by_entity_id( ) -async def test_reload_config_entry_by_entry_id(hass: HomeAssistant) -> None: +async def test_reload_config_entry_by_entry_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test being able to reload a config entry by config entry id.""" await async_setup_component(hass, DOMAIN, {}) @@ -425,6 +434,7 @@ async def test_reload_config_entry_by_entry_id(hass: HomeAssistant) -> None: assert len(mock_reload.mock_calls) == 1 assert mock_reload.mock_calls[0][1][0] == "8955375327824e14ba89e4b29cc3ec9a" + assert "Reloading a config entry by target is deprecated" not in caplog.text @pytest.mark.parametrize( diff --git a/tests/components/husqvarna_automower/snapshots/test_event.ambr b/tests/components/husqvarna_automower/snapshots/test_event.ambr index c25234a9553458..3590edeb2bc304 100644 --- a/tests/components/husqvarna_automower/snapshots/test_event.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_event.ambr @@ -7,6 +7,7 @@ 'area_id': None, 'capabilities': dict({ : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -14,12 +15,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -31,6 +41,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -44,11 +55,14 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -61,13 +75,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -76,19 +96,27 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -112,7 +140,11 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -125,7 +157,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', @@ -167,6 +201,7 @@ 'date_time': HAFakeDatetime(2025, 7, 13, 15, 30, tzinfo=datetime.timezone.utc), : 'wheel_motor_overloaded_rear_left', : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -174,12 +209,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -191,6 +235,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -204,11 +249,14 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -221,13 +269,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -236,19 +290,27 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_loop_signal', 'no_power_in_charging_station', 'no_response_from_charger', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -272,7 +334,11 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -285,7 +351,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 3284c189b49511..0f82b9db2982e3 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -184,6 +184,7 @@ 'area_id': None, 'capabilities': dict({ : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -191,12 +192,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -208,6 +218,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -221,14 +232,17 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', 'error', 'error_at_power_up', 'fatal_error', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -241,13 +255,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -256,10 +276,14 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_error', 'no_loop_signal', @@ -267,10 +291,14 @@ 'no_response_from_charger', 'off', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -295,9 +323,13 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', 'wait_power_up', 'wait_updating', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -310,7 +342,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', @@ -352,6 +386,7 @@ : 'enum', : 'Test Mower 1 Error', : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -359,12 +394,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -376,6 +420,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -389,14 +434,17 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', 'error', 'error_at_power_up', 'fatal_error', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -409,13 +457,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -424,10 +478,14 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_error', 'no_loop_signal', @@ -435,10 +493,14 @@ 'no_response_from_charger', 'off', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -463,9 +525,13 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', 'wait_power_up', 'wait_updating', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -478,7 +544,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', @@ -1645,6 +1713,7 @@ 'area_id': None, 'capabilities': dict({ : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -1652,12 +1721,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -1669,6 +1747,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -1682,14 +1761,17 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', 'error', 'error_at_power_up', 'fatal_error', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1702,13 +1784,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -1717,10 +1805,14 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_error', 'no_loop_signal', @@ -1728,10 +1820,14 @@ 'no_response_from_charger', 'off', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -1756,9 +1852,13 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', 'wait_power_up', 'wait_updating', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -1771,7 +1871,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', @@ -1813,6 +1915,7 @@ : 'enum', : 'Test Mower 2 Error', : list([ + 'accessory_power_problem', 'alarm_mower_in_motion', 'alarm_mower_lifted', 'alarm_mower_stopped', @@ -1820,12 +1923,21 @@ 'alarm_mower_tilted', 'alarm_outside_geofence', 'angular_sensor_problem', + 'auxiliary_cutting_means_blocked', + 'battery_near_end_of_life', + 'battery_needs_replacement', 'battery_problem', 'battery_restriction_due_to_ambient_temperature', + 'bluetooth_communication_with_charging_station_failed', + 'bluetooth_settings_changed', + 'boundary_wire_problem', 'can_error', + 'cellular_settings_changed', + 'charging_aborted', 'charging_current_too_high', 'charging_station_blocked', 'charging_system_problem', + 'chassis_collision', 'collision_sensor_defect', 'collision_sensor_error', 'collision_sensor_problem_front', @@ -1837,6 +1949,7 @@ 'connection_not_changed', 'connectivity_problem', 'connectivity_settings_restored', + 'cutting_disc_lost', 'cutting_drive_motor_1_defect', 'cutting_drive_motor_2_defect', 'cutting_drive_motor_3_defect', @@ -1850,14 +1963,17 @@ 'cutting_system_blocked', 'cutting_system_imbalance_warning', 'cutting_system_major_imbalance', + 'destination_blocked', 'destination_not_reachable', 'difficult_finding_home', 'docking_sensor_defect', 'electronic_problem', 'empty_battery', + 'epos_accessory_missing', 'error', 'error_at_power_up', 'fatal_error', + 'firmware_has_been_updated', 'folding_cutting_deck_sensor_defect', 'folding_sensor_activated', 'geofence_problem', @@ -1870,13 +1986,19 @@ 'high_charging_power_loss', 'high_internal_power_loss', 'high_internal_temperature', + 'high_temperature_in_cutting_motor_center', + 'high_temperature_in_cutting_motor_left', + 'high_temperature_in_cutting_motor_right', + 'imbalanced_auxiliary_cutting_disc_detected', 'internal_voltage_error', 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_software_configuration', 'invalid_sub_device_combination', 'invalid_system_configuration', 'left_brush_motor_overloaded', 'lift_sensor_defect', 'lifted', + 'lifted_in_link_arm', 'limited_cutting_height_range', 'loop_sensor_defect', 'loop_sensor_problem_front', @@ -1885,10 +2007,14 @@ 'loop_sensor_problem_right', 'low_battery', 'memory_circuit_problem', + 'mower_installation_position_changed', 'mower_lifted', 'mower_tilted', + 'multiple_reference_stations_detected', 'no_accurate_position_from_satellites', + 'no_charging_station_signal', 'no_confirmed_position', + 'no_correction_data_available', 'no_drive', 'no_error', 'no_loop_signal', @@ -1896,10 +2022,14 @@ 'no_response_from_charger', 'off', 'outside_working_area', + 'pin_code_changed', 'poor_signal_quality', + 'poor_vision_performance', + 'radar_problem', 'reference_station_communication_problem', 'right_brush_motor_overloaded', 'safety_function_faulty', + 'self_diagnostic_error', 'settings_restored', 'sim_card_locked', 'sim_card_not_found', @@ -1924,9 +2054,13 @@ 'unexpected_cutting_height_adj', 'unexpected_error', 'upside_down', + 'vision_system_error', + 'vision_system_malfunction', 'wait_power_up', 'wait_updating', + 'water_hose_found_in_working_area', 'weak_gps_signal', + 'wheel_brush_motor_problem', 'wheel_drive_problem_left', 'wheel_drive_problem_rear_left', 'wheel_drive_problem_rear_right', @@ -1939,7 +2073,9 @@ 'wheel_motor_overloaded_rear_left', 'wheel_motor_overloaded_rear_right', 'wheel_motor_overloaded_right', + 'wi_fi_settings_changed', 'work_area_not_valid', + 'work_area_tampered', 'wrong_loop_signal', 'wrong_pin_code', 'zone_generator_problem', diff --git a/tests/components/husqvarna_automower/test_event.py b/tests/components/husqvarna_automower/test_event.py index e29b108ac87029..6fa36b1d0c6e6f 100644 --- a/tests/components/husqvarna_automower/test_event.py +++ b/tests/components/husqvarna_automower/test_event.py @@ -164,6 +164,56 @@ def fake_register_websocket_response( assert entry is None +@pytest.mark.freeze_time(datetime(2023, 6, 5, 12)) +async def test_event_destination_blocked( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + automower_ws_ready: list[Callable[[], None]], +) -> None: + """Newer error codes such as destination_blocked must trigger the message event.""" + callbacks: list[Callable[[SingleMessageData], None]] = [] + + @callback + def fake_register_websocket_response( + cb: Callable[[SingleMessageData], None], + ) -> None: + callbacks.append(cb) + + mock_automower_client.register_single_message_callback.side_effect = ( + fake_register_websocket_response + ) + + await setup_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + for cb in automower_ws_ready: + cb() + await hass.async_block_till_done() + + message = SingleMessageData( + type="messages", + id=TEST_MOWER_ID, + attributes=SingleMessageAttributes( + message=Message( + time=datetime(2025, 7, 13, 15, 30, tzinfo=UTC), + code="destination_blocked", + severity=Severity.ERROR, + latitude=49.0, + longitude=10.0, + ) + ), + ) + + for cb in callbacks: + cb(message) + await hass.async_block_till_done() + + state = hass.states.get("event.garden_test_mower_1_message") + assert state is not None + assert state.attributes[ATTR_EVENT_TYPE] == "destination_blocked" + + @pytest.mark.freeze_time(datetime(2023, 6, 5, 12)) async def test_event_snapshot( hass: HomeAssistant, diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 2a75597ee4e28c..80c5f75b298c62 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -286,6 +286,7 @@ async def test_error_sensor( for state, error_key, expected_state in ( (MowerStates.IN_OPERATION, None, "no_error"), (MowerStates.ERROR, "can_error", "can_error"), + (MowerStates.ERROR, "destination_blocked", "destination_blocked"), (MowerStates.ERROR, None, MowerStates.ERROR.lower()), (MowerStates.ERROR_AT_POWER_UP, None, MowerStates.ERROR_AT_POWER_UP.lower()), (MowerStates.FATAL_ERROR, None, MowerStates.FATAL_ERROR.lower()), diff --git a/tests/components/imou/snapshots/test_diagnostics.ambr b/tests/components/imou/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..324bda74759fb6 --- /dev/null +++ b/tests/components/imou/snapshots/test_diagnostics.ambr @@ -0,0 +1,24 @@ +# serializer version: 1 +# name: test_config_entry_diagnostics + dict({ + 'coordinator': dict({ + 'last_update_success': True, + }), + 'devices': list([ + dict({ + 'channel_id': None, + 'device_id': 'd1', + 'identifier': 'd1', + 'is_ipc': False, + 'manufacturer': 'Imou', + 'model': 'm1', + 'product_id': None, + 'sw_version': '1.0', + }), + ]), + 'entry': dict({ + 'api_url': 'sg', + 'app_id': 'test_app_id', + }), + }) +# --- diff --git a/tests/components/imou/test_diagnostics.py b/tests/components/imou/test_diagnostics.py new file mode 100644 index 00000000000000..2d8ffccbdc7d2a --- /dev/null +++ b/tests/components/imou/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Tests for Imou diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Diagnostics include the region, App ID, and current devices.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) diff --git a/tests/components/midea/conftest.py b/tests/components/midea/conftest.py index 6a0ed19f8cbe9e..8baae012f62a54 100644 --- a/tests/components/midea/conftest.py +++ b/tests/components/midea/conftest.py @@ -167,6 +167,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_entry +@pytest.fixture +def config_entry( + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> MockConfigEntry: + """Return a mock config entry fixture.""" + return mock_config_entry(default_ac_device()) + + @pytest.fixture def mock_config_entry() -> Callable[[DummyDevice], MockConfigEntry]: """Return a function that creates a mock config entry for a given device.""" diff --git a/tests/components/midea/const.py b/tests/components/midea/const.py index 4bc4d1e5cb24ab..e65db41d278759 100644 --- a/tests/components/midea/const.py +++ b/tests/components/midea/const.py @@ -1,6 +1,6 @@ """Constants for Midea tests.""" -from midealocal.const import ProtocolVersion +from midealocal.const import DeviceType, ProtocolVersion from homeassistant.components.midea.const import CONF_KEY, CONF_SN, CONF_SUBTYPE from homeassistant.components.midea.device_catalog import MIDEA_DEVICE_NAMES @@ -9,6 +9,7 @@ CONF_IP_ADDRESS, CONF_MAC, CONF_MODEL, + CONF_NAME, CONF_PORT, CONF_PROTOCOL, CONF_TOKEN, @@ -52,3 +53,16 @@ CONF_TOKEN: TEST_TOKEN, CONF_KEY: TEST_KEY, } + +ENTRY_DATA = { + CONF_DEVICE_ID: TEST_DEVICE_ID, + CONF_NAME: "m", + CONF_TYPE: DeviceType.AC, + CONF_IP_ADDRESS: "1.1.1.1", + CONF_PORT: 6444, + CONF_MODEL: "m", + CONF_PROTOCOL: ProtocolVersion.V2, + CONF_TOKEN: "", + CONF_KEY: "", + CONF_SUBTYPE: 0, +} diff --git a/tests/components/midea/snapshots/test_switch.ambr b/tests/components/midea/snapshots/test_switch.ambr index 7fe32a5b9e583c..dbf7c45a891e55 100644 --- a/tests/components/midea/snapshots/test_switch.ambr +++ b/tests/components/midea/snapshots/test_switch.ambr @@ -399,6 +399,56 @@ 'state': 'on', }) # --- +# name: test_switch_state_snapshot[c2][switch.toilet_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.toilet_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Child lock', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': '12345678_child_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_state_snapshot[c2][switch.toilet_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Toilet Child lock', + }), + 'context': , + 'entity_id': 'switch.toilet_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch_state_snapshot[c3][switch.heat_pump_wi_fi_controller_disinfect-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/midea/test_config_flow.py b/tests/components/midea/test_config_flow.py index a86af3682b094c..a9998885126548 100644 --- a/tests/components/midea/test_config_flow.py +++ b/tests/components/midea/test_config_flow.py @@ -23,7 +23,7 @@ DOMAIN, ) from homeassistant.components.midea.device_catalog import MIDEA_DEVICE_NAMES -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigFlowResult from homeassistant.const import ( CONF_DEVICE, CONF_DEVICE_ID, @@ -2038,6 +2038,109 @@ async def test_auth_method_preset_login_failed(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "preset_login_failed"} +async def _assert_reconfigure_success( + hass: HomeAssistant, config_entry: MockConfigEntry, result: ConfigFlowResult +) -> None: + """Assert reconfigure success.""" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + discovery_result = DISCOVERY_RESULT + discovery_result[TEST_DEVICE_ID][CONF_IP_ADDRESS] = "8.8.8.8" + with ( + patch( + "homeassistant.components.midea.config_flow.discover", + return_value=discovery_result, + ) as mock_discovery, + patch( + "homeassistant.components.midea.config_flow.device_selector", + ) as mock_device_selector, + ): + mock_device = MagicMock() + mock_device.connect.return_value = True + mock_device_selector.return_value = mock_device + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "8.8.8.8"}, + ) + + mock_discovery.assert_called_once() + assert mock_discovery.call_args.kwargs["ip_address"] == "8.8.8.8" + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_IP_ADDRESS] == "8.8.8.8" + + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reconfigure_flow_no_discovery( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow with no discovery in the new IP.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with patch( + "homeassistant.components.midea.config_flow.discover", + return_value={}, + ) as mock_discovery: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: TEST_IP_ADDRESS}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"].get("base") == "invalid_device_ip" + mock_discovery.assert_called_once() + assert mock_discovery.call_args.kwargs["ip_address"] == TEST_IP_ADDRESS + + await _assert_reconfigure_success( + hass=hass, config_entry=config_entry, result=result + ) + + +async def test_reconfigure_flow_wrong_device_id_discovery( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow when the new IP belongs to a different device.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with patch( + "homeassistant.components.midea.config_flow.discover", + return_value={ + 123: { + **BASE_DATA, + CONF_TYPE: TEST_TYPE, + CONF_MAC: TEST_MAC_ADDRESS, + CONF_SN: TEST_SERIAL_NUMBER, + } + }, + ) as mock_discovery: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: TEST_IP_ADDRESS}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"].get("base") == "invalid_device_id_for_ip" + mock_discovery.assert_called_once() + assert mock_discovery.call_args.kwargs["ip_address"] == TEST_IP_ADDRESS + + await _assert_reconfigure_success( + hass=hass, config_entry=config_entry, result=result + ) + + async def test_dhcp_discovery_updates_host( hass: HomeAssistant, mock_config_entry: Callable[[DummyDevice], MockConfigEntry], diff --git a/tests/components/midea/test_init.py b/tests/components/midea/test_init.py index 2c1276b44449f7..b5186838ae4dea 100644 --- a/tests/components/midea/test_init.py +++ b/tests/components/midea/test_init.py @@ -4,7 +4,7 @@ from midealocal.const import DeviceType, ProtocolVersion -from homeassistant.components.midea.const import CONF_KEY, CONF_SN, CONF_SUBTYPE, DOMAIN +from homeassistant.components.midea.const import CONF_SN, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_DEVICE_ID, @@ -22,60 +22,55 @@ from homeassistant.setup import async_setup_component from .conftest import DummyDevice -from .const import TEST_DEVICE_ID, TEST_IP_ADDRESS, TEST_MAC_ADDRESS, TEST_SERIAL_NUMBER +from .const import ( + CONF_KEY, + CONF_SUBTYPE, + ENTRY_DATA, + TEST_DEVICE_ID, + TEST_IP_ADDRESS, + TEST_MAC_ADDRESS, + TEST_SERIAL_NUMBER, +) from tests.common import MockConfigEntry -_ENTRY_DATA = { - CONF_DEVICE_ID: TEST_DEVICE_ID, - CONF_NAME: "m", - CONF_TYPE: DeviceType.AC, - CONF_IP_ADDRESS: "1.1.1.1", - CONF_PORT: 6444, - CONF_MODEL: "m", - CONF_PROTOCOL: ProtocolVersion.V2, - CONF_TOKEN: "", - CONF_KEY: "", - CONF_SUBTYPE: 0, -} - async def test_async_setup(hass: HomeAssistant) -> None: """Test the midea domain can be set up without any config entries.""" assert await async_setup_component(hass, DOMAIN, {}) -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test async_unload_entry unloads platforms and closes the device.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) device = DummyDevice(DeviceType.AC) with patch( "homeassistant.components.midea.device_selector", return_value=device, ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.LOADED + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED assert device.daemon is True - assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state is ConfigEntryState.NOT_LOADED + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED assert ("close",) in device.calls -async def test_async_setup_entry_paths(hass: HomeAssistant) -> None: +async def test_async_setup_entry_paths( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test async_setup_entry for success and no-device return.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) with patch( "homeassistant.components.midea.device_selector", return_value=DummyDevice(DeviceType.AC), ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.LOADED + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED entry2 = MockConfigEntry( domain=DOMAIN, - data={**_ENTRY_DATA, CONF_DEVICE_ID: TEST_DEVICE_ID + 1}, + data={**ENTRY_DATA, CONF_DEVICE_ID: TEST_DEVICE_ID + 1}, minor_version=2, ) entry2.add_to_hass(hass) @@ -89,6 +84,7 @@ async def test_async_setup_entry_paths(hass: HomeAssistant) -> None: async def test_setup_entry_not_ready_on_connect_failure( hass: HomeAssistant, + config_entry: MockConfigEntry, ) -> None: """Test async_setup_entry raises ConfigEntryNotReady when connect returns False. @@ -97,8 +93,7 @@ async def test_setup_entry_not_ready_on_connect_failure( It can also leave the socket open in that case (e.g. when authentication fails), so the socket must be closed explicitly to avoid a ResourceWarning. """ - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) device = DummyDevice(DeviceType.AC) with ( patch( @@ -108,14 +103,15 @@ async def test_setup_entry_not_ready_on_connect_failure( patch.object(device, "connect", return_value=False), patch("homeassistant.components.midea.discover", return_value={}), ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert ("close_socket",) in device.calls - assert entry.data[CONF_IP_ADDRESS] == _ENTRY_DATA[CONF_IP_ADDRESS] + assert config_entry.data[CONF_IP_ADDRESS] == ENTRY_DATA[CONF_IP_ADDRESS] async def test_setup_entry_recovers_ip_on_connect_failure( hass: HomeAssistant, + config_entry: MockConfigEntry, ) -> None: """Test async_setup_entry loads successfully after discovering a moved device. @@ -123,8 +119,7 @@ async def test_setup_entry_recovers_ip_on_connect_failure( found at a different address by the discovery broadcast should update the config entry and connect there, finishing setup normally. """ - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) stale_device = DummyDevice(DeviceType.AC) recovered_device = DummyDevice(DeviceType.AC) @@ -139,19 +134,18 @@ async def test_setup_entry_recovers_ip_on_connect_failure( return_value={TEST_DEVICE_ID: {CONF_IP_ADDRESS: "2.2.2.2"}}, ), ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.LOADED - assert entry.data[CONF_IP_ADDRESS] == "2.2.2.2" + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.data[CONF_IP_ADDRESS] == "2.2.2.2" assert recovered_device.daemon is True assert ("open",) in recovered_device.calls async def test_setup_entry_no_recovery_when_device_not_discovered( - hass: HomeAssistant, + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test the stored IP is left untouched when discovery finds no match.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) device = DummyDevice(DeviceType.AC) with ( patch( @@ -164,16 +158,16 @@ async def test_setup_entry_no_recovery_when_device_not_discovered( return_value={TEST_DEVICE_ID + 1: {CONF_IP_ADDRESS: "2.2.2.2"}}, ), ): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY - assert entry.data[CONF_IP_ADDRESS] == _ENTRY_DATA[CONF_IP_ADDRESS] + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert config_entry.data[CONF_IP_ADDRESS] == ENTRY_DATA[CONF_IP_ADDRESS] async def test_migrate_entry_backfills_mac_and_serial_number( hass: HomeAssistant, ) -> None: """Test migration to minor_version 2 backfills mac and serial number.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_DATA, minor_version=1) entry.add_to_hass(hass) with ( patch( @@ -204,7 +198,7 @@ async def test_migrate_entry_drops_empty_mac_connection( device_registry: dr.DeviceRegistry, ) -> None: """Test migration removes a leftover empty network-mac connection.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_DATA, minor_version=1) entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -258,7 +252,7 @@ async def test_migrate_entry_drops_empty_mac_connection( async def test_migrate_entry_without_discovery_result(hass: HomeAssistant) -> None: """Test migration still completes when discovery finds nothing.""" - entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_DATA, minor_version=1) entry.add_to_hass(hass) with ( patch("homeassistant.components.midea.discover", return_value={}), diff --git a/tests/components/midea/test_switch.py b/tests/components/midea/test_switch.py index 2d36ef24286f18..dc1c51773ba7be 100644 --- a/tests/components/midea/test_switch.py +++ b/tests/components/midea/test_switch.py @@ -121,6 +121,10 @@ async def _assert_service_call( DummyDevice(DeviceType.CF, attributes={CFAttributes.aux_heating: True}), id="cf", ), + pytest.param( + DummyDevice(DeviceType.C2, attributes={"child_lock": True}), + id="c2", + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -179,6 +183,51 @@ async def test_ac_switch_services( ) +CHILD_LOCK_DEVICE_TYPES = [ + DeviceType.X34, + DeviceType.A1, + DeviceType.C2, + DeviceType.CE, + DeviceType.E1, + DeviceType.ED, + DeviceType.FA, + DeviceType.FB, + DeviceType.FC, +] + + +@pytest.mark.parametrize("device_type", CHILD_LOCK_DEVICE_TYPES) +async def test_child_lock_switch_created_and_services( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], + device_type: DeviceType, +) -> None: + """Test the child lock switch is created and its services reach the device.""" + device = DummyDevice(device_type, attributes={"child_lock": True}) + config_entry = mock_config_entry(device) + with patch("homeassistant.components.midea._PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, config_entry, device) + + entity_entry = entity_entries(hass, config_entry)[f"{TEST_DEVICE_ID}_child_lock"] + assert (state := hass.states.get(entity_entry.entity_id)) is not None + assert state.state == "on" + + await _assert_service_call( + hass, + entity_entry.entity_id, + SERVICE_TURN_OFF, + [("set_attribute", "child_lock", False)], + device, + ) + await _assert_service_call( + hass, + entity_entry.entity_id, + SERVICE_TURN_ON, + [("set_attribute", "child_lock", True)], + device, + ) + + async def test_switch_unknown_when_attribute_becomes_non_bool( hass: HomeAssistant, mock_config_entry: Callable[[DummyDevice], MockConfigEntry], diff --git a/tests/components/persang_infrared/snapshots/test_button.ambr b/tests/components/persang_infrared/snapshots/test_button.ambr new file mode 100644 index 00000000000000..db1b9ba07e941e --- /dev/null +++ b/tests/components/persang_infrared/snapshots/test_button.ambr @@ -0,0 +1,701 @@ +# serializer version: 1 +# name: test_entities[button.persang_speaker_equalizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_equalizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Equalizer', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Equalizer', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'eq', + 'unique_id': '01JTEST0000000000000000000_eq', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_equalizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Equalizer', + }), + 'context': , + 'entity_id': 'button.persang_speaker_equalizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mode', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': '01JTEST0000000000000000000_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Mode', + }), + 'context': , + 'entity_id': 'button.persang_speaker_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 0', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 0', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_0', + 'unique_id': '01JTEST0000000000000000000_num_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 0', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 1', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_1', + 'unique_id': '01JTEST0000000000000000000_num_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 1', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 2', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_2', + 'unique_id': '01JTEST0000000000000000000_num_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 2', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 3', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 3', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_3', + 'unique_id': '01JTEST0000000000000000000_num_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 3', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 4', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 4', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_4', + 'unique_id': '01JTEST0000000000000000000_num_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 4', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 5', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 5', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_5', + 'unique_id': '01JTEST0000000000000000000_num_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 5', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 6', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 6', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_6', + 'unique_id': '01JTEST0000000000000000000_num_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 6', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 7', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 7', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_7', + 'unique_id': '01JTEST0000000000000000000_num_7', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 7', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 8', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 8', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_8', + 'unique_id': '01JTEST0000000000000000000_num_8', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 8', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_9-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_9', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 9', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 9', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_9', + 'unique_id': '01JTEST0000000000000000000_num_9', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_9-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 9', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_repeat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_repeat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Repeat', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Repeat', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'repeat', + 'unique_id': '01JTEST0000000000000000000_repeat', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_repeat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Repeat', + }), + 'context': , + 'entity_id': 'button.persang_speaker_repeat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_scan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_scan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Scan', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scan', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'scan', + 'unique_id': '01JTEST0000000000000000000_scan', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_scan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Scan', + }), + 'context': , + 'entity_id': 'button.persang_speaker_scan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/persang_infrared/test_button.py b/tests/components/persang_infrared/test_button.py new file mode 100644 index 00000000000000..8e8710ac2f1c32 --- /dev/null +++ b/tests/components/persang_infrared/test_button.py @@ -0,0 +1,92 @@ +"""Tests for the Persang Infrared button platform.""" + +from infrared_protocols.codes.persang.speaker import PersangSpeakerCode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .conftest import MOCK_INFRARED_EMITTER_ENTITY_ID + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.common import assert_availability_follows_source_entity +from tests.components.infrared.common import MockInfraredEmitterEntity + +BUTTON_ENTITY_ID_MODE = "button.persang_speaker_mode" + + +@pytest.fixture +def platforms() -> list[Platform]: + """Return platforms to set up.""" + return [Platform.BUTTON] + + +@pytest.mark.usefixtures("init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test all button entities are created with the correct attributes.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + device_entry = device_registry.async_get_device_by_identifier( + ("persang_infrared", mock_config_entry.entry_id), mock_config_entry.entry_id + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("entity_id", "expected_code"), + [ + ("button.persang_speaker_mode", PersangSpeakerCode.MODE), + ("button.persang_speaker_equalizer", PersangSpeakerCode.EQ), + ("button.persang_speaker_scan", PersangSpeakerCode.SCAN), + ("button.persang_speaker_repeat", PersangSpeakerCode.REPEAT), + ("button.persang_speaker_number_0", PersangSpeakerCode.NUM_0), + ("button.persang_speaker_number_1", PersangSpeakerCode.NUM_1), + ("button.persang_speaker_number_2", PersangSpeakerCode.NUM_2), + ("button.persang_speaker_number_3", PersangSpeakerCode.NUM_3), + ("button.persang_speaker_number_4", PersangSpeakerCode.NUM_4), + ("button.persang_speaker_number_5", PersangSpeakerCode.NUM_5), + ("button.persang_speaker_number_6", PersangSpeakerCode.NUM_6), + ("button.persang_speaker_number_7", PersangSpeakerCode.NUM_7), + ("button.persang_speaker_number_8", PersangSpeakerCode.NUM_8), + ("button.persang_speaker_number_9", PersangSpeakerCode.NUM_9), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_button_press_sends_correct_code( + hass: HomeAssistant, + mock_infrared_emitter_entity: MockInfraredEmitterEntity, + entity_id: str, + expected_code: PersangSpeakerCode, +) -> None: + """Test pressing a button sends the correct IR code.""" + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(mock_infrared_emitter_entity.send_command_calls) == 1 + assert mock_infrared_emitter_entity.send_command_calls[0] == expected_code + + +@pytest.mark.usefixtures("init_integration") +async def test_button_availability_follows_ir_entity(hass: HomeAssistant) -> None: + """Test a button becomes unavailable when the IR entity is unavailable.""" + await assert_availability_follows_source_entity( + hass, BUTTON_ENTITY_ID_MODE, MOCK_INFRARED_EMITTER_ENTITY_ID + ) diff --git a/tests/components/picnic/__init__.py b/tests/components/picnic/__init__.py index fe6e65cbd2b3fd..2330da78648aaf 100644 --- a/tests/components/picnic/__init__.py +++ b/tests/components/picnic/__init__.py @@ -1 +1,13 @@ """Tests for the Picnic 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/picnic/snapshots/test_sensor.ambr b/tests/components/picnic/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..0034b024cc8a5e --- /dev/null +++ b/tests/components/picnic/snapshots/test_sensor.ambr @@ -0,0 +1,883 @@ +# serializer version: 1 +# name: test_all_entities[sensor.mock_title_cart_items_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_cart_items_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cart items count', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cart items count', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cart_items_count', + 'unique_id': '295-6y3-1nf4.cart_items_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_cart_items_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'Mock Title Cart items count', + }), + 'context': , + 'entity_id': 'sensor.mock_title_cart_items_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_all_entities[sensor.mock_title_cart_total_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_cart_total_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cart total price', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cart total price', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cart_total_price', + 'unique_id': '295-6y3-1nf4.cart_total_price', + 'unit_of_measurement': '€', + }) +# --- +# name: test_all_entities[sensor.mock_title_cart_total_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'Mock Title Cart total price', + : '€', + }), + 'context': , + 'entity_id': 'sensor.mock_title_cart_total_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.35', + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_last_order_s_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_end_of_last_order_s_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': "End of last order's slot", + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "End of last order's slot", + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_slot_end', + 'unique_id': '295-6y3-1nf4.last_order_slot_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_last_order_s_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : "Mock Title End of last order's slot", + }), + 'context': , + 'entity_id': 'sensor.mock_title_end_of_last_order_s_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-02-26T20:15:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_next_delivery_s_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_end_of_next_delivery_s_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': "End of next delivery's slot", + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "End of next delivery's slot", + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'next_delivery_slot_end', + 'unique_id': '295-6y3-1nf4.next_delivery_slot_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_next_delivery_s_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : "Mock Title End of next delivery's slot", + }), + 'context': , + 'entity_id': 'sensor.mock_title_end_of_next_delivery_s_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_selected_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_end_of_selected_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'End of selected slot', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'End of selected slot', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'selected_slot_end', + 'unique_id': '295-6y3-1nf4.selected_slot_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_end_of_selected_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title End of selected slot', + }), + 'context': , + 'entity_id': 'sensor.mock_title_end_of_selected_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-03-03T14:45:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_estimated_arrival_of_next_delivery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_estimated_arrival_of_next_delivery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Estimated arrival of next delivery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Estimated arrival of next delivery', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'next_delivery_estimated_arrival', + 'unique_id': '295-6y3-1nf4.next_delivery_estimated_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_estimated_arrival_of_next_delivery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Estimated arrival of next delivery', + }), + 'context': , + 'entity_id': 'sensor.mock_title_estimated_arrival_of_next_delivery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.mock_title_expected_end_of_next_delivery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_expected_end_of_next_delivery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Expected end of next delivery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Expected end of next delivery', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'next_delivery_eta_end', + 'unique_id': '295-6y3-1nf4.next_delivery_eta_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_expected_end_of_next_delivery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Expected end of next delivery', + }), + 'context': , + 'entity_id': 'sensor.mock_title_expected_end_of_next_delivery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.mock_title_expected_start_of_next_delivery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_expected_start_of_next_delivery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Expected start of next delivery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Expected start of next delivery', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'next_delivery_eta_start', + 'unique_id': '295-6y3-1nf4.next_delivery_eta_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_expected_start_of_next_delivery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Expected start of next delivery', + }), + 'context': , + 'entity_id': 'sensor.mock_title_expected_start_of_next_delivery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.mock_title_last_order_delivery_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_last_order_delivery_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Last order delivery time', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last order delivery time', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_delivery_time', + 'unique_id': '295-6y3-1nf4.last_order_delivery_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_last_order_delivery_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Last order delivery time', + }), + 'context': , + 'entity_id': 'sensor.mock_title_last_order_delivery_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-02-26T19:54:05+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_max_order_time_of_last_order-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_max_order_time_of_last_order', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Max order time of last order', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max order time of last order', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_max_order_time', + 'unique_id': '295-6y3-1nf4.last_order_max_order_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_max_order_time_of_last_order-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Max order time of last order', + }), + 'context': , + 'entity_id': 'sensor.mock_title_max_order_time_of_last_order', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-02-25T21:00:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_max_order_time_of_selected_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_max_order_time_of_selected_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Max order time of selected slot', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max order time of selected slot', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'selected_slot_max_order_time', + 'unique_id': '295-6y3-1nf4.selected_slot_max_order_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_max_order_time_of_selected_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Max order time of selected slot', + }), + 'context': , + 'entity_id': 'sensor.mock_title_max_order_time_of_selected_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-03-02T21:00:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_minimum_order_value_for_selected_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_minimum_order_value_for_selected_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Minimum order value for selected slot', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Minimum order value for selected slot', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'selected_slot_min_order_value', + 'unique_id': '295-6y3-1nf4.selected_slot_min_order_value', + 'unit_of_measurement': '€', + }) +# --- +# name: test_all_entities[sensor.mock_title_minimum_order_value_for_selected_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'Mock Title Minimum order value for selected slot', + : '€', + }), + 'context': , + 'entity_id': 'sensor.mock_title_minimum_order_value_for_selected_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_last_order_s_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_start_of_last_order_s_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': "Start of last order's slot", + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "Start of last order's slot", + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_slot_start', + 'unique_id': '295-6y3-1nf4.last_order_slot_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_last_order_s_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : "Mock Title Start of last order's slot", + }), + 'context': , + 'entity_id': 'sensor.mock_title_start_of_last_order_s_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-02-26T19:15:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_next_delivery_s_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_start_of_next_delivery_s_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': "Start of next delivery's slot", + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "Start of next delivery's slot", + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'next_delivery_slot_start', + 'unique_id': '295-6y3-1nf4.next_delivery_slot_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_next_delivery_s_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : "Mock Title Start of next delivery's slot", + }), + 'context': , + 'entity_id': 'sensor.mock_title_start_of_next_delivery_s_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_selected_slot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_start_of_selected_slot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start of selected slot', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Start of selected slot', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'selected_slot_start', + 'unique_id': '295-6y3-1nf4.selected_slot_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_start_of_selected_slot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'timestamp', + : 'Mock Title Start of selected slot', + }), + 'context': , + 'entity_id': 'sensor.mock_title_start_of_selected_slot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-03-03T13:45:00+00:00', + }) +# --- +# name: test_all_entities[sensor.mock_title_status_of_last_order-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_status_of_last_order', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Status of last order', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Status of last order', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_status', + 'unique_id': '295-6y3-1nf4.last_order_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mock_title_status_of_last_order-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'Mock Title Status of last order', + }), + 'context': , + 'entity_id': 'sensor.mock_title_status_of_last_order', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'COMPLETED', + }) +# --- +# name: test_all_entities[sensor.mock_title_total_price_of_last_order-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.mock_title_total_price_of_last_order', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total price of last order', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total price of last order', + 'platform': 'picnic', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_order_total_price', + 'unique_id': '295-6y3-1nf4.last_order_total_price', + 'unit_of_measurement': '€', + }) +# --- +# name: test_all_entities[sensor.mock_title_total_price_of_last_order-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by Picnic', + : 'Mock Title Total price of last order', + : '€', + }), + 'context': , + 'entity_id': 'sensor.mock_title_total_price_of_last_order', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41.33', + }) +# --- diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 0dbbc2a6a40116..88630eb17d1183 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -1,628 +1,373 @@ """The tests for the Picnic sensor platform.""" -import copy -from datetime import timedelta -import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest -from python_picnic_api2.models import Cart, DeliverySummary, User +from python_picnic_api2.models import DeliverySummary, Eta import requests +from syrupy.assertion import SnapshotAssertion -from homeassistant import config_entries -from homeassistant.components.picnic import const -from homeassistant.components.picnic.const import DOMAIN -from homeassistant.components.picnic.sensor import SENSOR_TYPES -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.picnic.const import DEFAULT_UPDATE_INTERVAL, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ACCESS_TOKEN, - CONF_COUNTRY_CODE, - CURRENCY_EURO, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import dt as dt_util -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_test_home_assistant, -) - -DEFAULT_USER_RESPONSE = { - "user_id": "295-6y3-1nf4", - "firstname": "User", - "lastname": "Name", - "address": { - "house_number": 123, - "house_number_ext": "a", - "postcode": "4321 AB", - "street": "Commonstreet", - "city": "Somewhere", - }, - "total_deliveries": 123, - "completed_deliveries": 112, -} -DEFAULT_CART_RESPONSE = { - "items": [], - "delivery_slots": [ - { - "slot_id": "611a3b074872b23576bef456a", - "window_start": "2021-03-03T14:45:00.000+01:00", - "window_end": "2021-03-03T15:45:00.000+01:00", - "cut_off_time": "2021-03-02T22:00:00.000+01:00", - "minimum_order_value": 3500, - }, - ], - "selected_slot": {"slot_id": "611a3b074872b23576bef456a", "state": "EXPLICIT"}, - "total_count": 10, - "total_price": 2535, -} -DEFAULT_DELIVERY_RESPONSE = { - "delivery_id": "z28fjso23e", - "creation_time": "2021-02-24T21:48:46.395+01:00", - "slot": { - "slot_id": "602473859a40dc24c6b65879", - "hub_id": "AMS", - "window_start": "2021-02-26T20:15:00.000+01:00", - "window_end": "2021-02-26T21:15:00.000+01:00", - "cut_off_time": "2021-02-25T22:00:00.000+01:00", - "minimum_order_value": 3500, - }, - "eta2": { - "start": "2021-02-26T20:54:00.000+01:00", - "end": "2021-02-26T21:14:00.000+01:00", - }, - "status": "COMPLETED", - "delivery_time": { - "start": "2021-02-26T20:54:05.221+01:00", - "end": "2021-02-26T20:58:31.802+01:00", - }, - "orders": [ - { - "creation_time": "2021-02-24T21:48:46.418+01:00", - "total_price": 3597, +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +def enable_all_entities(entity_registry_enabled_by_default: None) -> None: + """Make sure all entities are enabled.""" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.picnic.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setup_platform_not_available( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the set-up of the sensor platform if API is not available.""" + mock_picnic_api.get_user.side_effect = requests.exceptions.ConnectionError + mock_picnic_api.get_cart.side_effect = requests.exceptions.ConnectionError + mock_picnic_api.get_deliveries.side_effect = requests.exceptions.ConnectionError + mock_picnic_api.get_delivery_position.side_effect = ( + requests.exceptions.ConnectionError + ) + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert hass.states.get("sensor.mock_title_max_order_time_of_selected_slot") is None + assert hass.states.get("sensor.mock_title_status_of_last_order") is None + assert hass.states.get("sensor.mock_title_total_price_of_last_order") is None + + +async def test_sensors_no_selected_time_slot( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor states with no explicit selected time slot.""" + cart = mock_picnic_api.get_cart.return_value + cart.selected_slot.state = "IMPLICIT" + + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("sensor.mock_title_start_of_selected_slot").state + == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.mock_title_end_of_selected_slot").state == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.mock_title_max_order_time_of_selected_slot").state + == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.mock_title_minimum_order_value_for_selected_slot").state + == STATE_UNKNOWN + ) + + +async def test_next_delivery_sensors( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor states when last order is not yet delivered.""" + delivery = mock_picnic_api.get_deliveries.return_value[0] + delivery.raw["delivery_time"] = None + delivery.status = "CURRENT" + + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("sensor.mock_title_last_order_delivery_time").state + == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.mock_title_expected_start_of_next_delivery").state + == "2021-02-26T19:54:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_expected_end_of_next_delivery").state + == "2021-02-26T20:14:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_start_of_next_delivery_s_slot").state + == "2021-02-26T19:15:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_end_of_next_delivery_s_slot").state + == "2021-02-26T20:15:00+00:00" + ) + + +async def test_sensors_eta_date_malformed( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor states when last order eta dates are malformed.""" + await setup_integration(hass, mock_config_entry) + + delivery = mock_picnic_api.get_deliveries.return_value[0] + delivery.eta2 = Eta(start="wrong-time", end="other-malformed-datetime") + delivery.status = "CURRENT" + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + hass.states.get("sensor.mock_title_expected_start_of_next_delivery").state + == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.mock_title_expected_end_of_next_delivery").state + == STATE_UNKNOWN + ) + + +async def test_sensors_use_detailed_eta_if_available( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor states use the more precise delivery position ETA.""" + await setup_integration(hass, mock_config_entry) + + delivery = mock_picnic_api.get_deliveries.return_value[0] + delivery.raw["delivery_time"] = None + delivery.status = "CURRENT" + mock_picnic_api.get_delivery_position.return_value = { + "eta_window": { + "start": "2021-03-05T10:19:20.452+00:00", + "end": "2021-03-05T10:39:20.452+00:00", }, - { - "creation_time": "2021-02-25T17:10:26.816+01:00", - "total_price": 536, - }, - ], -} - -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - - -@pytest.mark.usefixtures("hass_storage") -class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): - """Test the Picnic sensor.""" - - async def asyncSetUp(self): - """Set up things to be run when tests are started.""" - self._manager = async_test_home_assistant() - self.hass = await self._manager.__aenter__() - self.entity_registry = er.async_get(self.hass) - - # Patch the api client - self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") - self.picnic_mock = self.picnic_patcher.start() - self.picnic_mock().session.auth_token = "3q29fpwhulzes" - - # Add a config entry and setup the integration - config_data = { - CONF_ACCESS_TOKEN: "x-original-picnic-auth-token", - CONF_COUNTRY_CODE: "NL", - } - self.config_entry = MockConfigEntry( - domain=const.DOMAIN, - data=config_data, - unique_id="295-6y3-1nf4", - ) - self.config_entry.add_to_hass(self.hass) - - async def asyncTearDown(self): - """Tear down the test setup, stop hass/patchers.""" - await self.hass.async_stop(force=True) - await self._manager.__aexit__(None, None, None) - self.picnic_patcher.stop() - - @property - def _coordinator(self): - return self.config_entry.runtime_data - - def _assert_sensor(self, name, state=None, cls=None, unit=None, disabled=False): - sensor = self.hass.states.get(name) - if disabled: - assert sensor is None - return - - assert sensor.state == state - if cls: - assert sensor.attributes["device_class"] == cls - if unit: - assert sensor.attributes["unit_of_measurement"] == unit - - assert sensor.attributes["attribution"] == "Data provided by Picnic" - - async def _setup_platform( - self, use_default_responses=False, enable_all_sensors=True + "eta": 1614941090000, + } + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + mock_picnic_api.get_delivery_position.assert_called_with(delivery.delivery_id) + assert ( + hass.states.get("sensor.mock_title_expected_start_of_next_delivery").state + == "2021-03-05T10:19:20+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_expected_end_of_next_delivery").state + == "2021-03-05T10:39:20+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_estimated_arrival_of_next_delivery").state + == "2021-03-05T10:44:50+00:00" + ) + + +async def test_sensors_no_data( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor states when the api only returns empty objects.""" + await setup_integration(hass, mock_config_entry) + + mock_picnic_api.get_user.return_value = {} + mock_picnic_api.get_cart.return_value = None + mock_picnic_api.get_deliveries.return_value = None + mock_picnic_api.get_delivery_position.side_effect = ValueError + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + for entity_id in ( + "sensor.mock_title_cart_total_price", + "sensor.mock_title_start_of_selected_slot", + "sensor.mock_title_end_of_selected_slot", + "sensor.mock_title_max_order_time_of_selected_slot", + "sensor.mock_title_minimum_order_value_for_selected_slot", + "sensor.mock_title_max_order_time_of_last_order", + "sensor.mock_title_last_order_delivery_time", + "sensor.mock_title_expected_start_of_next_delivery", + "sensor.mock_title_expected_end_of_next_delivery", + ): + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_sensors_malformed_delivery_data( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor states when the delivery api returns not a list.""" + await setup_integration(hass, mock_config_entry) + + mock_picnic_api.get_deliveries.return_value = {"error": "message"} + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + for entity_id in ( + "sensor.mock_title_max_order_time_of_last_order", + "sensor.mock_title_last_order_delivery_time", + "sensor.mock_title_expected_start_of_next_delivery", + "sensor.mock_title_expected_end_of_next_delivery", ): - """Set up the Picnic sensor platform.""" - if use_default_responses: - self.picnic_mock().get_user.return_value = User.from_api( - copy.deepcopy(DEFAULT_USER_RESPONSE) - ) - self.picnic_mock().get_cart.return_value = Cart.from_api( - copy.deepcopy(DEFAULT_CART_RESPONSE) - ) - self.picnic_mock().get_deliveries.return_value = [ - DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)) - ] - self.picnic_mock().get_delivery_position.return_value = {} - - await self.hass.config_entries.async_setup(self.config_entry.entry_id) - await self.hass.async_block_till_done() - - if enable_all_sensors: - await self._enable_all_sensors() - - async def _enable_all_sensors(self): - """Enable all sensors of the Picnic integration.""" - # Enable the sensors - for sensor_type in SENSOR_KEYS: - entry = self.entity_registry.async_get_or_create( - Platform.SENSOR, DOMAIN, f"{self.config_entry.unique_id}.{sensor_type}" - ) - updated_entry = self.entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - assert updated_entry.disabled is False - await self.hass.async_block_till_done() - - # Trigger a reload of the data - async_fire_time_changed( - self.hass, - dt_util.utcnow() - + timedelta(seconds=config_entries.RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await self.hass.async_block_till_done() - - async def test_sensor_setup_platform_not_available(self): - """Test the set-up of the sensor platform if API is not available.""" - # Configure mock requests to yield exceptions - self.picnic_mock().get_user.side_effect = requests.exceptions.ConnectionError - self.picnic_mock().get_cart.side_effect = requests.exceptions.ConnectionError - self.picnic_mock().get_deliveries.side_effect = ( - requests.exceptions.ConnectionError - ) - self.picnic_mock().get_delivery_position.side_effect = ( - requests.exceptions.ConnectionError - ) - await self._setup_platform(enable_all_sensors=False) - - # Assert that sensors are not set up - assert ( - self.hass.states.get("sensor.mock_title_max_order_time_of_selected_slot") - is None - ) - assert self.hass.states.get("sensor.mock_title_status_of_last_order") is None - assert ( - self.hass.states.get("sensor.mock_title_total_price_of_last_order") is None - ) - - async def test_sensors_setup(self): - """Test the default sensor setup behaviour.""" - await self._setup_platform(use_default_responses=True) - - self._assert_sensor("sensor.mock_title_cart_items_count", "10") - self._assert_sensor( - "sensor.mock_title_cart_total_price", - "25.35", - unit=CURRENCY_EURO, - ) - self._assert_sensor( - "sensor.mock_title_start_of_selected_slot", - "2021-03-03T13:45:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_end_of_selected_slot", - "2021-03-03T14:45:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_max_order_time_of_selected_slot", - "2021-03-02T21:00:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_minimum_order_value_for_selected_slot", - "35.0", - ) - self._assert_sensor( - "sensor.mock_title_start_of_last_order_s_slot", - "2021-02-26T19:15:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_end_of_last_order_s_slot", - "2021-02-26T20:15:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor("sensor.mock_title_status_of_last_order", "COMPLETED") - self._assert_sensor( - "sensor.mock_title_max_order_time_of_last_order", - "2021-02-25T21:00:00+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_last_order_delivery_time", - "2021-02-26T19:54:05+00:00", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_total_price_of_last_order", - "41.33", - unit=CURRENCY_EURO, - ) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - "unknown", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - "unknown", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_start_of_next_delivery_s_slot", - "unknown", - cls=SensorDeviceClass.TIMESTAMP, - ) - self._assert_sensor( - "sensor.mock_title_end_of_next_delivery_s_slot", - "unknown", - cls=SensorDeviceClass.TIMESTAMP, - ) - - async def test_sensors_setup_disabled_by_default(self): - """Test that some sensors are disabled by default.""" - await self._setup_platform(use_default_responses=True, enable_all_sensors=False) - - self._assert_sensor("sensor.mock_title_cart_items_count", disabled=True) - self._assert_sensor( - "sensor.mock_title_start_of_last_order_s_slot", disabled=True - ) - self._assert_sensor("sensor.mock_title_end_of_last_order_s_slot", disabled=True) - self._assert_sensor("sensor.mock_title_status_of_last_order", disabled=True) - self._assert_sensor( - "sensor.mock_title_total_price_of_last_order", disabled=True - ) - self._assert_sensor( - "sensor.mock_title_start_of_next_delivery_s_slot", - disabled=True, - ) - self._assert_sensor( - "sensor.mock_title_end_of_next_delivery_s_slot", disabled=True - ) - - async def test_sensors_no_selected_time_slot(self): - """Test sensor states with no explicit selected time slot.""" - # Adjust cart response - cart_response = copy.deepcopy(DEFAULT_CART_RESPONSE) - cart_response["selected_slot"]["state"] = "IMPLICIT" - - # Set mock responses - self.picnic_mock().get_user.return_value = User.from_api( - copy.deepcopy(DEFAULT_USER_RESPONSE) - ) - self.picnic_mock().get_cart.return_value = Cart.from_api(cart_response) - self.picnic_mock().get_deliveries.return_value = [ - DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)) - ] - self.picnic_mock().get_delivery_position.return_value = {} - await self._setup_platform() - - # Assert sensors are unknown - self._assert_sensor("sensor.mock_title_start_of_selected_slot", STATE_UNKNOWN) - self._assert_sensor("sensor.mock_title_end_of_selected_slot", STATE_UNKNOWN) - self._assert_sensor( - "sensor.mock_title_max_order_time_of_selected_slot", - STATE_UNKNOWN, - ) - self._assert_sensor( - "sensor.mock_title_minimum_order_value_for_selected_slot", - STATE_UNKNOWN, - ) - - async def test_next_delivery_sensors(self): - """Test sensor states when last order is not yet delivered.""" - # Adjust default delivery response - delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) - del delivery_response["delivery_time"] - delivery_response["status"] = "CURRENT" - - # Set mock responses - self.picnic_mock().get_user.return_value = User.from_api( - copy.deepcopy(DEFAULT_USER_RESPONSE) - ) - self.picnic_mock().get_cart.return_value = Cart.from_api( - copy.deepcopy(DEFAULT_CART_RESPONSE) - ) - self.picnic_mock().get_deliveries.return_value = [ - DeliverySummary.from_api(delivery_response) - ] - self.picnic_mock().get_delivery_position.return_value = {} - await self._setup_platform() - - # Assert delivery time is not available, but eta is - self._assert_sensor("sensor.mock_title_last_order_delivery_time", STATE_UNKNOWN) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - "2021-02-26T19:54:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - "2021-02-26T20:14:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_start_of_next_delivery_s_slot", - "2021-02-26T19:15:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_end_of_next_delivery_s_slot", - "2021-02-26T20:15:00+00:00", - ) - - async def test_sensors_eta_date_malformed(self): - """Test sensor states when last order eta dates are malformed.""" - # Set-up platform with default mock responses - await self._setup_platform(use_default_responses=True) - - # Set non-datetime strings as eta - eta_dates: dict[str, str] = { - "start": "wrong-time", - "end": "other-malformed-datetime", - } - delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) - delivery_response["eta2"] = eta_dates - delivery_response["status"] = "CURRENT" - self.picnic_mock().get_deliveries.return_value = [ - DeliverySummary.from_api(delivery_response) - ] - await self._coordinator.async_refresh() - - # Assert eta times are not available due to malformed date strings - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - STATE_UNKNOWN, - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - STATE_UNKNOWN, - ) - - async def test_sensors_use_detailed_eta_if_available(self): - """Test sensor states when last order is not yet delivered.""" - # Set-up platform with default mock responses - await self._setup_platform(use_default_responses=True) - - # Provide a delivery position response with different ETA - # and remove delivery time from response - delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) - del delivery_response["delivery_time"] - delivery_response["status"] = "CURRENT" - self.picnic_mock().get_deliveries.return_value = [ - DeliverySummary.from_api(delivery_response) - ] - self.picnic_mock().get_delivery_position.return_value = { - "eta_window": { - "start": "2021-03-05T10:19:20.452+00:00", - "end": "2021-03-05T10:39:20.452+00:00", - }, - "eta": 1614941090000, - } - await self._coordinator.async_refresh() - - # Assert detailed ETA is used - self.picnic_mock().get_delivery_position.assert_called_with( - delivery_response["delivery_id"] - ) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - "2021-03-05T10:19:20+00:00", - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - "2021-03-05T10:39:20+00:00", - ) - self._assert_sensor( - "sensor.mock_title_estimated_arrival_of_next_delivery", - "2021-03-05T10:44:50+00:00", - ) - - # The live estimate is cleared again once position data disappears - self.picnic_mock().get_delivery_position.return_value = {} - async_fire_time_changed(self.hass, dt_util.utcnow() + timedelta(minutes=31)) - await self.hass.async_block_till_done(wait_background_tasks=True) - - self._assert_sensor( - "sensor.mock_title_estimated_arrival_of_next_delivery", - STATE_UNKNOWN, - ) - - async def test_sensors_no_data(self): - """Test sensor states when the api only returns empty objects.""" - # Setup platform with default responses - await self._setup_platform(use_default_responses=True) - - # Change mock responses to empty data and refresh the coordinator - self.picnic_mock().get_user.return_value = {} - self.picnic_mock().get_cart.return_value = None - self.picnic_mock().get_deliveries.return_value = None - self.picnic_mock().get_delivery_position.side_effect = ValueError - await self._coordinator.async_refresh() - - # Assert all default-enabled sensors have STATE_UNAVAILABLE - # because the last update failed - assert self._coordinator.last_update_success is False - self._assert_sensor("sensor.mock_title_cart_total_price", STATE_UNAVAILABLE) - self._assert_sensor( - "sensor.mock_title_start_of_selected_slot", STATE_UNAVAILABLE - ) - self._assert_sensor("sensor.mock_title_end_of_selected_slot", STATE_UNAVAILABLE) - self._assert_sensor( - "sensor.mock_title_max_order_time_of_selected_slot", - STATE_UNAVAILABLE, - ) - self._assert_sensor( - "sensor.mock_title_minimum_order_value_for_selected_slot", - STATE_UNAVAILABLE, - ) - self._assert_sensor( - "sensor.mock_title_max_order_time_of_last_order", - STATE_UNAVAILABLE, - ) - self._assert_sensor( - "sensor.mock_title_last_order_delivery_time", - STATE_UNAVAILABLE, - ) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - STATE_UNAVAILABLE, - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - STATE_UNAVAILABLE, - ) - - async def test_sensors_malformed_delivery_data(self): - """Test sensor states when the delivery api returns not a list.""" - # Setup platform with default responses - await self._setup_platform(use_default_responses=True) - - # Change mock responses to empty data and refresh the coordinator - self.picnic_mock().get_deliveries.return_value = {"error": "message"} - await self._coordinator.async_refresh() - - # Assert all last-order sensors have STATE_UNAVAILABLE - # because the delivery info fetch failed - assert self._coordinator.last_update_success is True - self._assert_sensor( - "sensor.mock_title_max_order_time_of_last_order", - STATE_UNKNOWN, - ) - self._assert_sensor("sensor.mock_title_last_order_delivery_time", STATE_UNKNOWN) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - STATE_UNKNOWN, - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - STATE_UNKNOWN, - ) - - async def test_sensors_malformed_response(self): - """Test coordinator update fails when API yields ValueError.""" - # Setup platform with default responses - await self._setup_platform(use_default_responses=True) - - # Change mock responses to empty data and refresh the coordinator - self.picnic_mock().get_user.side_effect = ValueError - self.picnic_mock().get_cart.side_effect = ValueError - await self._coordinator.async_refresh() - - # Assert coordinator update failed - assert self._coordinator.last_update_success is False - - async def test_multiple_active_orders(self): - """Test sensor values with multiple active orders.""" - # Create 2 undelivered orders - undelivered_order = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) - del undelivered_order["delivery_time"] - undelivered_order["status"] = "CURRENT" - undelivered_order["slot"]["window_start"] = "2022-03-01T09:15:00.000+01:00" - undelivered_order["slot"]["window_end"] = "2022-03-01T10:15:00.000+01:00" - undelivered_order["eta2"]["start"] = "2022-03-01T09:30:00.000+01:00" - undelivered_order["eta2"]["end"] = "2022-03-01T09:45:00.000+01:00" - - undelivered_order_2 = copy.deepcopy(undelivered_order) - undelivered_order_2["slot"]["window_start"] = "2022-03-08T13:15:00.000+01:00" - undelivered_order_2["slot"]["window_end"] = "2022-03-08T14:15:00.000+01:00" - undelivered_order_2["eta2"]["start"] = "2022-03-08T13:30:00.000+01:00" - undelivered_order_2["eta2"]["end"] = "2022-03-08T13:45:00.000+01:00" - - deliveries_response = [ - DeliverySummary.from_api(undelivered_order_2), - DeliverySummary.from_api(undelivered_order), - DeliverySummary.from_api(copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)), - ] - - # Set mock responses - self.picnic_mock().get_user.return_value = User.from_api( - copy.deepcopy(DEFAULT_USER_RESPONSE) - ) - self.picnic_mock().get_cart.return_value = Cart.from_api( - copy.deepcopy(DEFAULT_CART_RESPONSE) - ) - self.picnic_mock().get_deliveries.return_value = deliveries_response - self.picnic_mock().get_delivery_position.return_value = {} - await self._setup_platform() - - self._assert_sensor( - "sensor.mock_title_start_of_last_order_s_slot", - "2022-03-08T12:15:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_end_of_last_order_s_slot", - "2022-03-08T13:15:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_start_of_next_delivery_s_slot", - "2022-03-01T08:15:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_end_of_next_delivery_s_slot", - "2022-03-01T09:15:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_expected_start_of_next_delivery", - "2022-03-01T08:30:00+00:00", - ) - self._assert_sensor( - "sensor.mock_title_expected_end_of_next_delivery", - "2022-03-01T08:45:00+00:00", - ) - - async def test_device_registry_entry(self): - """Test if device registry entry is populated correctly.""" - # Setup platform and default mock responses - await self._setup_platform(use_default_responses=True) - - device_registry = dr.async_get(self.hass) # pylint: disable=home-assistant-tests-registry-fixtures - picnic_service = device_registry.async_get_device_by_identifier( - (const.DOMAIN, DEFAULT_USER_RESPONSE["user_id"]), - self.config_entry.entry_id, - ) - assert picnic_service.model == DEFAULT_USER_RESPONSE["user_id"] - assert picnic_service.name == "Mock Title" - assert picnic_service.entry_type is dr.DeviceEntryType.SERVICE - - async def test_auth_token_is_saved_on_update(self): - """Test auth-token changes are reflected by the config entry.""" - # Setup platform and default mock responses - await self._setup_platform(use_default_responses=True) - - # Set a different auth token in the session mock - updated_auth_token = "x-updated-picnic-auth-token" - self.picnic_mock().session.auth_token = updated_auth_token - - # Verify the updated auth token is not set and fetch data using the coordinator - assert self.config_entry.data.get(CONF_ACCESS_TOKEN) != updated_auth_token - await self._coordinator.async_refresh() - - # Verify that the updated auth token is saved in the config entry - assert self.config_entry.data.get(CONF_ACCESS_TOKEN) == updated_auth_token + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + +async def test_sensors_malformed_response( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensors become unavailable when the API yields ValueError.""" + await setup_integration(hass, mock_config_entry) + + mock_picnic_api.get_user.side_effect = ValueError + mock_picnic_api.get_cart.side_effect = ValueError + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + hass.states.get("sensor.mock_title_cart_total_price").state == STATE_UNAVAILABLE + ) + + +async def test_multiple_active_orders( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor values with multiple active orders.""" + completed = mock_picnic_api.get_deliveries.return_value[0] + + def _undelivered(window_start: str, window_end: str, eta_start: str, eta_end: str): + order = DeliverySummary.from_api(completed.raw.copy()) + order.raw["delivery_time"] = None + order.status = "CURRENT" + order.slot.window_start = window_start + order.slot.window_end = window_end + order.eta2 = Eta(start=eta_start, end=eta_end) + return order + + undelivered_order = _undelivered( + "2022-03-01T09:15:00.000+01:00", + "2022-03-01T10:15:00.000+01:00", + "2022-03-01T09:30:00.000+01:00", + "2022-03-01T09:45:00.000+01:00", + ) + undelivered_order_2 = _undelivered( + "2022-03-08T13:15:00.000+01:00", + "2022-03-08T14:15:00.000+01:00", + "2022-03-08T13:30:00.000+01:00", + "2022-03-08T13:45:00.000+01:00", + ) + + mock_picnic_api.get_deliveries.return_value = [ + undelivered_order_2, + undelivered_order, + completed, + ] + mock_picnic_api.get_delivery_position.return_value = {} + + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("sensor.mock_title_start_of_last_order_s_slot").state + == "2022-03-08T12:15:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_end_of_last_order_s_slot").state + == "2022-03-08T13:15:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_start_of_next_delivery_s_slot").state + == "2022-03-01T08:15:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_end_of_next_delivery_s_slot").state + == "2022-03-01T09:15:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_expected_start_of_next_delivery").state + == "2022-03-01T08:30:00+00:00" + ) + assert ( + hass.states.get("sensor.mock_title_expected_end_of_next_delivery").state + == "2022-03-01T08:45:00+00:00" + ) + + +async def test_device_registry_entry( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test if device registry entry is populated correctly.""" + await setup_integration(hass, mock_config_entry) + + picnic_service = device_registry.async_get_device_by_identifier( + (DOMAIN, "295-6y3-1nf4"), + mock_config_entry.entry_id, + ) + assert picnic_service.model == "295-6y3-1nf4" + assert picnic_service.name == "Mock Title" + assert picnic_service.entry_type is dr.DeviceEntryType.SERVICE + + +async def test_auth_token_is_saved_on_update( + hass: HomeAssistant, + mock_picnic_api: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test auth-token changes are reflected by the config entry.""" + await setup_integration(hass, mock_config_entry) + + updated_auth_token = "x-updated-picnic-auth-token" + mock_picnic_api.session.auth_token = updated_auth_token + + assert mock_config_entry.data.get(CONF_ACCESS_TOKEN) != updated_auth_token + + freezer.tick(DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.data.get(CONF_ACCESS_TOKEN) == updated_auth_token diff --git a/tests/components/sofar/snapshots/test_binary_sensor.ambr b/tests/components/sofar/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..2d17ac76681e40 --- /dev/null +++ b/tests/components/sofar/snapshots/test_binary_sensor.ambr @@ -0,0 +1,868 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_ac_output_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_ac_output_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC output fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC output fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_ac_output', + 'unique_id': 'SS2ES104N5S445_fault_ac_output', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_ac_output_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 AC output fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_ac_output_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_arc_fault_afci-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_arc_fault_afci', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Arc fault (AFCI)', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Arc fault (AFCI)', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_arc_fault', + 'unique_id': 'SS2ES104N5S445_fault_arc_fault', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_arc_fault_afci-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Arc fault (AFCI)', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_arc_fault_afci', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_battery_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_battery', + 'unique_id': 'SS2ES104N5S445_fault_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Battery fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_battery_pack_communication_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack communication fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack communication fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_battery_pack', + 'unique_id': 'SS2ES104N5S445_fault_battery_pack', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Battery pack communication fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_combiner_box_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_combiner_box_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Combiner box fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Combiner box fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_combiner_box', + 'unique_id': 'SS2ES104N5S445_fault_combiner_box', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_combiner_box_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Combiner box fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_combiner_box_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_communication_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_communication_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Communication fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Communication fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_communication', + 'unique_id': 'SS2ES104N5S445_fault_communication', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_communication_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Communication fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_communication_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_dc_bus_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_dc_bus_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC bus fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC bus fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_dc_bus', + 'unique_id': 'SS2ES104N5S445_fault_dc_bus', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_dc_bus_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 DC bus fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_dc_bus_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_derating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_derating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Derating', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Derating', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_derating', + 'unique_id': 'SS2ES104N5S445_fault_derating', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_derating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Derating', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_derating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_fan_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_fan_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Fan fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fan fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_fan', + 'unique_id': 'SS2ES104N5S445_fault_fan', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_fan_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Fan fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_fan_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_grid_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_grid_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Grid fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_grid', + 'unique_id': 'SS2ES104N5S445_fault_grid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_grid_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Grid fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_grid_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_input_fuse_open-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_input_fuse_open', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Input fuse open', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Input fuse open', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_input_fuse', + 'unique_id': 'SS2ES104N5S445_fault_input_fuse', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_input_fuse_open-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Input fuse open', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_input_fuse_open', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_insulation_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_insulation_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Insulation fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Insulation fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_insulation', + 'unique_id': 'SS2ES104N5S445_fault_insulation', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_insulation_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Insulation fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_insulation_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_internal_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_internal_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Internal fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Internal fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_internal', + 'unique_id': 'SS2ES104N5S445_fault_internal', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_internal_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Internal fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_internal_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_pv_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_pv_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'PV fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PV fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_pv', + 'unique_id': 'SS2ES104N5S445_fault_pv', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_pv_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 PV fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_pv_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_shutdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_shutdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Shutdown', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Shutdown', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_shutdown', + 'unique_id': 'SS2ES104N5S445_fault_shutdown', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_shutdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Shutdown', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_shutdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_string_fuse_open-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_string_fuse_open', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'String fuse open', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'String fuse open', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_string_fuse', + 'unique_id': 'SS2ES104N5S445_fault_string_fuse', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_string_fuse_open-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 String fuse open', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_string_fuse_open', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_thermal_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + '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.4_4_ktlx_g3_thermal_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Thermal fault', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Thermal fault', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fault_thermal', + 'unique_id': 'SS2ES104N5S445_fault_thermal', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.4_4_ktlx_g3_thermal_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : '4.4 KTLX-G3 Thermal fault', + }), + 'context': , + 'entity_id': 'binary_sensor.4_4_ktlx_g3_thermal_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/sofar/snapshots/test_diagnostics.ambr b/tests/components/sofar/snapshots/test_diagnostics.ambr index 4770d42925d2b7..a8e8adc7909ec9 100644 --- a/tests/components/sofar/snapshots/test_diagnostics.ambr +++ b/tests/components/sofar/snapshots/test_diagnostics.ambr @@ -1,6 +1,8 @@ # serializer version: 1 # name: test_diagnostics dict({ + 'active_faults': list([ + ]), 'inverter_type': 1537, 'model': '4.4 KTLX-G3', 'raw': dict({ diff --git a/tests/components/sofar/test_binary_sensor.py b/tests/components/sofar/test_binary_sensor.py new file mode 100644 index 00000000000000..60ae99abae99c8 --- /dev/null +++ b/tests/components/sofar/test_binary_sensor.py @@ -0,0 +1,124 @@ +"""Test the Sofar Inverter Modbus binary sensor platform.""" + +from unittest.mock import patch + +from modbus_connection.mock import MockModbusConnection +import pytest +from sofar_modbus.modern.faults import FaultCategory +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sofar.binary_sensor import FAULT_SENSOR_DESCRIPTIONS +from homeassistant.components.sofar.const import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities match their snapshot on a PV device.""" + connection = MockModbusConnection() + seed_pv_inverter(connection.for_unit(1)) + entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT, title=MOCK_MODEL + ) + entry.add_to_hass(hass) + with ( + patch("homeassistant.components.sofar.PLATFORMS", [Platform.BINARY_SENSOR]), + patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: connection.for_unit( + unit_id + ), + ), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +async def test_no_faults_reports_off( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, +) -> None: + """Test every category is off when nothing is seeded.""" + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_fault_grid" + ) + assert entity_id is not None + assert (state := hass.states.get(entity_id)) is not None + assert state.state == STATE_OFF + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_one_bit_only_lights_its_own_category( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_connection: MockModbusConnection, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a fan failure isn't masked by a shutdown bit in the same register.""" + unit = mock_connection.for_unit(1) + unit.holding[0x0405] = 0b1 # ID001_GRID_OVER_VOLTAGE + unit.holding[0x040F] = 0b100000001 # ID161_FORCED_SHUTDOWN | ID169_FAN_1_FAILURE + unit.holding[0x0410] = 0b100 # ID179_BMS_HIGH_TEMPERATURE_PROTECTION + + with patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( + unit_id + ), + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + on_categories = {"grid", "shutdown", "fan", "battery"} + for description in FAULT_SENSOR_DESCRIPTIONS: + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_{description.key}" + ) + assert entity_id is not None + assert (state := hass.states.get(entity_id)) is not None + expected = ( + STATE_ON if description.category.value in on_categories else STATE_OFF + ) + assert state.state == expected, description.category + + +async def test_enabled_by_default_excludes_commercial_hardware( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, +) -> None: + """Test combiner box, string fuse, input fuse and AFCI start disabled.""" + entries = [ + e + for e in er.async_entries_for_config_entry( + entity_registry, init_integration.entry_id + ) + if e.domain == BINARY_SENSOR_DOMAIN + ] + assert len(entries) == len(FAULT_SENSOR_DESCRIPTIONS) + + disabled_categories = { + FaultCategory(e.unique_id.removeprefix(f"{MOCK_SERIAL}_fault_")) + for e in entries + if e.disabled_by is er.RegistryEntryDisabler.INTEGRATION + } + assert disabled_categories == { + FaultCategory.ARC_FAULT, + FaultCategory.COMBINER_BOX, + FaultCategory.INPUT_FUSE, + FaultCategory.STRING_FUSE, + } diff --git a/tests/components/sofar/test_diagnostics.py b/tests/components/sofar/test_diagnostics.py index cd2a4f2e71eac7..d7aff386402c0b 100644 --- a/tests/components/sofar/test_diagnostics.py +++ b/tests/components/sofar/test_diagnostics.py @@ -1,5 +1,8 @@ """Test the Sofar Inverter Modbus diagnostics.""" +from unittest.mock import patch + +from modbus_connection.mock import MockModbusConnection from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -21,6 +24,30 @@ async def test_diagnostics( assert diag == snapshot +async def test_diagnostics_includes_active_faults( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_connection: MockModbusConnection, + mock_config_entry: MockConfigEntry, +) -> None: + """Test active faults are decoded by name in the diagnostics dump.""" + mock_connection.for_unit(1).holding[0x0405] = 0b1 # ID001_GRID_OVER_VOLTAGE + + with patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( + unit_id + ), + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + + assert diag["active_faults"] == ["grid_over_voltage"] + + async def test_diagnostics_redacts_serial_number( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/weheat/snapshots/test_sensor.ambr b/tests/components/weheat/snapshots/test_sensor.ambr index 778773ee67df17..61890870abfc6e 100644 --- a/tests/components/weheat/snapshots/test_sensor.ambr +++ b/tests/components/weheat/snapshots/test_sensor.ambr @@ -17,6 +17,7 @@ 'defrosting', 'self_test', 'manual_control', + 'updating', ]), }), 'config_entry_id': , @@ -65,6 +66,7 @@ 'defrosting', 'self_test', 'manual_control', + 'updating', ]), }), 'context': , diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index b03dcc0a487c7f..85c48b1d9cb5fe 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -77,6 +77,7 @@ async def test_single_entry_allowed( assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN async def test_user_no_devices_found( @@ -152,6 +153,7 @@ async def test_user_has_confirmation_async_discovery_flow( [ config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_HOMEKIT, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, config_entries.SOURCE_ZEROCONF, @@ -171,6 +173,23 @@ async def test_discovery_single_instance( assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN + + +async def test_discovery_confirm_single_instance( + hass: HomeAssistant, discovery_flow_conf: dict[str, bool] +) -> None: + """Test confirming a discovery aborts once an entry exists.""" + flow = config_entries.HANDLERS["test"]() + flow.hass = hass + flow.context = {"source": config_entries.SOURCE_DISCOVERY} + + MockConfigEntry(domain="test").add_to_hass(hass) + result = await flow.async_step_confirm({}) + + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN @pytest.mark.parametrize( @@ -322,6 +341,8 @@ async def test_import_single_instance( result = await flow.async_step_import(None) assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN async def test_ignored_discoveries( @@ -370,6 +391,7 @@ async def test_webhook_single_entry_allowed( assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN async def test_webhook_multiple_entries_allowed( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index fa19dab78fc1ee..98b28ddad8f03a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -4290,6 +4290,7 @@ async def async_step_user(self, user_input=None): ABORT_IN_PROGRESS = { "type": data_entry_flow.FlowResultType.ABORT, "reason": "already_in_progress", + "translation_domain": HOMEASSISTANT_DOMAIN, } @@ -5318,6 +5319,8 @@ async def async_step_mock(self, user_input=None): "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["reason"] == "already_in_progress" + assert result2["translation_domain"] == HOMEASSISTANT_DOMAIN flows = hass.config_entries.flow.async_progress() assert len(flows) == 1