diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00eda06042c132..2a17e0b2d42cc6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 10 + CACHE_VERSION: 11 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 9 HA_SHORT_VERSION: "2024.10" diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index c0472131357df1..137641426974ae 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.9.0"], + "requirements": ["airgradient==0.9.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index e0b0695655d0b7..b1d3400c9be8e2 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.6.5"] + "requirements": ["aioairzone-cloud==0.6.6"] } diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json index dd698201b0944e..ccf1d965855996 100644 --- a/homeassistant/components/alarmdecoder/strings.json +++ b/homeassistant/components/alarmdecoder/strings.json @@ -22,7 +22,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "create_entry": { "default": "Successfully connected to AlarmDecoder." @@ -37,7 +38,7 @@ "title": "Configure AlarmDecoder", "description": "What would you like to edit?", "data": { - "edit_select": "Edit" + "edit_selection": "Edit" } }, "arm_settings": { diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index ccdc2374142f0b..684a5a2a0cc467 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -10,7 +10,7 @@ }, "site": { "data": { - "site_nmi": "Site NMI", + "site_id": "Site NMI", "site_name": "Site Name" }, "description": "Select the NMI of the site you would like to add" diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 3b770f189a41be..b036815259ce6d 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -17,7 +17,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { - "no_integration_selected": "You must select at least one integration to track" + "no_integrations_selected": "You must select at least one integration to track" } }, "options": { @@ -37,7 +37,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": { - "no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]" + "no_integrations_selected": "[%key:component::analytics_insights::config::error::no_integrations_selected%]" } }, "entity": { diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 273f6c6fec269d..b6c47cf36b23c4 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -4,6 +4,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from .const import CONF_THRESHOLD, DEFAULT_THRESHOLD from .coordinator import AuroraDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -21,9 +22,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: AuroraConfigEntry) -> bo await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True +async def update_listener(hass: HomeAssistant, entry: AuroraConfigEntry) -> None: + """Handle options update.""" + entry.runtime_data.threshold = int( + entry.options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD) + ) + # refresh the state of the visibility alert binary sensor + await entry.runtime_data.async_request_refresh() + + async def async_unload_entry(hass: HomeAssistant, entry: AuroraConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aurora/coordinator.py b/homeassistant/components/aurora/coordinator.py index 422dff839220b8..9771cc536528cc 100644 --- a/homeassistant/components/aurora/coordinator.py +++ b/homeassistant/components/aurora/coordinator.py @@ -38,8 +38,8 @@ def __init__(self, hass: HomeAssistant) -> None: ) self.api = AuroraForecast(async_get_clientsession(hass)) - self.latitude = int(self.config_entry.data[CONF_LATITUDE]) - self.longitude = int(self.config_entry.data[CONF_LONGITUDE]) + self.latitude = round(self.config_entry.data[CONF_LATITUDE]) + self.longitude = round(self.config_entry.data[CONF_LONGITUDE]) self.threshold = int( self.config_entry.options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD) ) diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json index 09ec86bdf4d38a..5ba3a1273fd482 100644 --- a/homeassistant/components/aurora/strings.json +++ b/homeassistant/components/aurora/strings.json @@ -14,14 +14,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "options": { "step": { "init": { "data": { - "threshold": "Threshold (%)" + "forecast_threshold": "Threshold (%)" } } } diff --git a/homeassistant/components/azure_event_hub/strings.json b/homeassistant/components/azure_event_hub/strings.json index 3f05e4b8e35b79..3319a29a15432a 100644 --- a/homeassistant/components/azure_event_hub/strings.json +++ b/homeassistant/components/azure_event_hub/strings.json @@ -38,7 +38,7 @@ }, "options": { "step": { - "options": { + "init": { "title": "Options for the Azure Event Hub.", "data": { "send_interval": "Interval between sending batches to the hub." diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index b179f0d097ba85..18c689e093d2c6 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -15,7 +15,9 @@ "description": "Set up your BleBox to integrate with Home Assistant.", "data": { "host": "[%key:common::config_flow::data::ip%]", - "port": "[%key:common::config_flow::data::port%]" + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]" }, "title": "Set up your BleBox device" } diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 53f2d8a02405c1..4d92a5f7fc06ec 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["pyblu==1.0.2"], + "requirements": ["pyblu==1.0.3"], "zeroconf": [ { "type": "_musc._tcp.local." diff --git a/homeassistant/components/bryant_evolution/strings.json b/homeassistant/components/bryant_evolution/strings.json index d446fdc5345ae1..9e2b5509cc474c 100644 --- a/homeassistant/components/bryant_evolution/strings.json +++ b/homeassistant/components/bryant_evolution/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "reconfigure": { + "reconfigure_confirm": { "data": { "filename": "[%key:component::bryant_evolution::config::step::user::data::filename%]" } diff --git a/homeassistant/components/caldav/__init__.py b/homeassistant/components/caldav/__init__.py index 3111460e9687a6..beb03cec554743 100644 --- a/homeassistant/components/caldav/__init__.py +++ b/homeassistant/components/caldav/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ssl_verify_cert=entry.data[CONF_VERIFY_SSL], - timeout=10, + timeout=30, ) try: await hass.async_add_executor_job(client.principal) diff --git a/homeassistant/components/cambridge_audio/manifest.json b/homeassistant/components/cambridge_audio/manifest.json index f2f067a4a9da1b..232e3d8e2aa020 100644 --- a/homeassistant/components/cambridge_audio/manifest.json +++ b/homeassistant/components/cambridge_audio/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aiostreammagic"], - "requirements": ["aiostreammagic==2.3.1"], + "requirements": ["aiostreammagic==2.5.0"], "zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."] } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 1d06ae23ca2b93..65f39a7171e6eb 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.1"], + "requirements": ["PyChromecast==14.0.3"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 0e9f03c8ef8da3..153f552b6984fd 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/doorbird", "iot_class": "local_push", "loggers": ["doorbirdpy"], - "requirements": ["DoorBirdPy==3.0.2"], + "requirements": ["DoorBirdPy==3.0.4"], "zeroconf": [ { "type": "_axis-video._tcp.local.", diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 8f8740ddfdf4e9..37ed4457184acc 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "quality_scale": "silver", - "requirements": ["pyDuotecno==2024.9.0"] + "requirements": ["pyDuotecno==2024.10.0"] } diff --git a/homeassistant/components/duotecno/strings.json b/homeassistant/components/duotecno/strings.json index a5585c3dd2c890..2342eeb8288af2 100644 --- a/homeassistant/components/duotecno/strings.json +++ b/homeassistant/components/duotecno/strings.json @@ -5,18 +5,21 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" }, "data_description": { "host": "The hostname or IP address of your Duotecno device." } } }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, "error": { "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%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "entity": { diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index c96867b489b048..6586af92d1fc8d 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/econet", "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"], - "requirements": ["pyeconet==0.1.22"] + "requirements": ["pyeconet==0.1.23"] } diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index d4f59fd1c08015..2b7dbff0a20c46 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -47,6 +47,7 @@ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } }, diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9f79dcf34f6111..80119002be5001 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20241002.2"] + "requirements": ["home-assistant-frontend==20241002.3"] } diff --git a/homeassistant/components/fujitsu_fglair/const.py b/homeassistant/components/fujitsu_fglair/const.py index 8aa911a8b30174..73c811a1ed517c 100644 --- a/homeassistant/components/fujitsu_fglair/const.py +++ b/homeassistant/components/fujitsu_fglair/const.py @@ -9,5 +9,5 @@ CONF_REGION = "region" CONF_EUROPE = "is_europe" -REGION_EU = "EU" +REGION_EU = "eu" REGION_DEFAULT = "default" diff --git a/homeassistant/components/fujitsu_fglair/manifest.json b/homeassistant/components/fujitsu_fglair/manifest.json index 76cf3966fbe24a..1c7b9b0b469aff 100644 --- a/homeassistant/components/fujitsu_fglair/manifest.json +++ b/homeassistant/components/fujitsu_fglair/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair", "iot_class": "cloud_polling", - "requirements": ["ayla-iot-unofficial==1.4.1"] + "requirements": ["ayla-iot-unofficial==1.4.2"] } diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index dbd44ed34dc1db..73f6b42f53b316 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["fyta_cli==0.6.6"] + "requirements": ["fyta_cli==0.6.7"] } diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index e7bb899361a9fc..c3a8254ad90b5a 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -172,10 +172,12 @@ async def _async_get_tts_audio( _LOGGER.error("Error: %s when validating options: %s", err, options) return None, None - encoding = texttospeech.AudioEncoding(options[CONF_ENCODING]) - gender: texttospeech.SsmlVoiceGender | None = texttospeech.SsmlVoiceGender( + encoding: texttospeech.AudioEncoding = texttospeech.AudioEncoding[ + options[CONF_ENCODING] + ] # type: ignore[misc] + gender: texttospeech.SsmlVoiceGender | None = texttospeech.SsmlVoiceGender[ options[CONF_GENDER] - ) + ] # type: ignore[misc] voice = options[CONF_VOICE] if voice: gender = None diff --git a/homeassistant/components/google_photos/strings.json b/homeassistant/components/google_photos/strings.json index 21942ce71a7801..bd565a6122d361 100644 --- a/homeassistant/components/google_photos/strings.json +++ b/homeassistant/components/google_photos/strings.json @@ -21,7 +21,8 @@ "wrong_account": "Wrong account: Please authenticate with the right account.", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index 447da5e24c27ed..a26cf8c58ec6ae 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -21,7 +21,8 @@ "wrong_account": "Wrong account: Please authenticate with the right account.", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 0ac3ea2a4e2e5e..26549e29cb0578 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -14,6 +14,9 @@ def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None: """Calculate due date for dailies and yesterdailies.""" + if task["everyX"] == 0 or not task.get("nextDue"): # grey dailies never become due + return None + today = to_date(last_cron) startdate = to_date(task["startDate"]) if TYPE_CHECKING: diff --git a/homeassistant/components/hdmi_cec/strings.json b/homeassistant/components/hdmi_cec/strings.json index 22715907a99b6f..d280cfc1a2b35a 100644 --- a/homeassistant/components/hdmi_cec/strings.json +++ b/homeassistant/components/hdmi_cec/strings.json @@ -24,11 +24,11 @@ }, "cmd": { "name": "Command", - "description": "Command itself. Could be decimal number or string with hexadeximal notation: \"0x10\"." + "description": "Command itself. Could be decimal number or string with hexadecimal notation: \"0x10\"." }, "dst": { "name": "Destination", - "description": "Destination for command. Could be decimal number or string with hexadeximal notation: \"0x10\"." + "description": "Destination for command. Could be decimal number or string with hexadecimal notation: \"0x10\"." }, "raw": { "name": "Raw", @@ -36,7 +36,7 @@ }, "src": { "name": "Source", - "description": "Source of command. Could be decimal number or string with hexadeximal notation: \"0x10\"." + "description": "Source of command. Could be decimal number or string with hexadecimal notation: \"0x10\"." } } }, diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 30cfd34e0fb50a..559f18b331a39c 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.57", "babel==2.15.0"] + "requirements": ["holidays==0.58", "babel==2.15.0"] } diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 88ccf890c66a63..aaa74473dd9532 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -23,7 +23,7 @@ HAP_SUFFIX = "._hap._tcp.local." POWERVIEW_G2_SUFFIX = "._powerview._tcp.local." -POWERVIEW_G3_SUFFIX = "._powerview-g3._tcp.local." +POWERVIEW_G3_SUFFIX = "._PowerView-G3._tcp.local." async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str]: diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 4120c55a7a7984..a80708d9a3fc91 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_polling", "loggers": ["aiopvapi"], "requirements": ["aiopvapi==3.1.1"], - "zeroconf": ["_powerview._tcp.local.", "_powerview-g3._tcp.local."] + "zeroconf": ["_powerview._tcp.local.", "_PowerView-G3._tcp.local."] } diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index fd9e7578fb2157..6ce17926cfea6d 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -125,7 +125,9 @@ def __init__( self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mower_id)}, manufacturer="Husqvarna", - model=self.mower_attributes.system.model, + model=self.mower_attributes.system.model.removeprefix( + "HUSQVARNA " + ).removeprefix("Husqvarna "), name=self.mower_attributes.system.name, serial_number=self.mower_attributes.system.serial_number, suggested_area="Garden", diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 85acfaf66a2424..17d32c270d9820 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.9.3"] + "requirements": ["aioautomower==2024.10.0"] } diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py index f731ecf278ccc9..47b9bef845e4b4 100644 --- a/homeassistant/components/hydrawise/const.py +++ b/homeassistant/components/hydrawise/const.py @@ -10,7 +10,7 @@ MANUFACTURER = "Hydrawise" -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=60) SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 08946a802f1937..c01be10fc68867 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.5"] + "requirements": ["imgw_pib==1.0.6"] } diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index ec7d78edd53bce..f0e55881652dae 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -29,7 +29,8 @@ "invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index b89139cbab3e94..b517bf064e1d3a 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -24,6 +24,7 @@ "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%]" }, "error": { diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index aa0178b2c4a860..a3b9f29e01d942 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==3.2.0", - "xknxproject==3.8.0", + "xknxproject==3.8.1", "knx-frontend==2024.9.10.221729" ], "single_config_entry": true diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 8adae25b0ae5cd..dd1e08eda49b07 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -6,6 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/linkplay", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["python-linkplay==0.0.12"], + "loggers": ["linkplay"], + "requirements": ["python-linkplay==0.0.15"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json index 279841bb14777c..20dd291902634b 100644 --- a/homeassistant/components/meater/strings.json +++ b/homeassistant/components/meater/strings.json @@ -19,7 +19,8 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/microbees/strings.json b/homeassistant/components/microbees/strings.json index 49d42af83d3a77..8635753a564278 100644 --- a/homeassistant/components/microbees/strings.json +++ b/homeassistant/components/microbees/strings.json @@ -21,6 +21,7 @@ "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", "wrong_account": "You can only reauthenticate this entry with the same microBees account." }, diff --git a/homeassistant/components/motionblinds_ble/manifest.json b/homeassistant/components/motionblinds_ble/manifest.json index d9968cfde4cf3a..ce7e7a6bb8be85 100644 --- a/homeassistant/components/motionblinds_ble/manifest.json +++ b/homeassistant/components/motionblinds_ble/manifest.json @@ -14,5 +14,5 @@ "integration_type": "device", "iot_class": "assumed_state", "loggers": ["motionblindsble"], - "requirements": ["motionblindsble==0.1.1"] + "requirements": ["motionblindsble==0.1.2"] } diff --git a/homeassistant/components/nyt_games/coordinator.py b/homeassistant/components/nyt_games/coordinator.py index 3b6955747505dd..5e88a5dd92a1c7 100644 --- a/homeassistant/components/nyt_games/coordinator.py +++ b/homeassistant/components/nyt_games/coordinator.py @@ -23,7 +23,7 @@ class NYTGamesData: wordle: Wordle spelling_bee: SpellingBee | None - connections: Connections + connections: Connections | None class NYTGamesCoordinator(DataUpdateCoordinator[NYTGamesData]): diff --git a/homeassistant/components/nyt_games/manifest.json b/homeassistant/components/nyt_games/manifest.json index 1cdc5988e382be..a2cd5629ed1c96 100644 --- a/homeassistant/components/nyt_games/manifest.json +++ b/homeassistant/components/nyt_games/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nyt_games", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["nyt_games==0.4.2"] + "requirements": ["nyt_games==0.4.3"] } diff --git a/homeassistant/components/nyt_games/sensor.py b/homeassistant/components/nyt_games/sensor.py index 6e19a4c21dc6bd..57759fb354d6bb 100644 --- a/homeassistant/components/nyt_games/sensor.py +++ b/homeassistant/components/nyt_games/sensor.py @@ -161,10 +161,11 @@ async def async_setup_entry( NYTGamesSpellingBeeSensor(coordinator, description) for description in SPELLING_BEE_SENSORS ) - entities.extend( - NYTGamesConnectionsSensor(coordinator, description) - for description in CONNECTIONS_SENSORS - ) + if coordinator.data.connections is not None: + entities.extend( + NYTGamesConnectionsSensor(coordinator, description) + for description in CONNECTIONS_SENSORS + ) async_add_entities(entities) @@ -236,4 +237,5 @@ def __init__( @property def native_value(self) -> StateType | date: """Return the state of the sensor.""" + assert self.coordinator.data.connections is not None return self.entity_description.value_fn(self.coordinator.data.connections) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index cd2e28ed63850f..629dce0823c400 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -130,20 +130,32 @@ async def _insert_statistics(self) -> None: continue start = cost_reads[0].start_time _LOGGER.debug("Getting statistics at: %s", start) - stats = await get_instance(self.hass).async_add_executor_job( - statistics_during_period, - self.hass, - start, - start + timedelta(seconds=1), - {cost_statistic_id, consumption_statistic_id}, - "hour", - None, - {"sum"}, - ) + # In the common case there should be a previous statistic at start time + # so we only need to fetch one statistic. If there isn't any, fetch all. + for end in (start + timedelta(seconds=1), None): + stats = await get_instance(self.hass).async_add_executor_job( + statistics_during_period, + self.hass, + start, + end, + {cost_statistic_id, consumption_statistic_id}, + "hour", + None, + {"sum"}, + ) + if stats: + break + if end: + _LOGGER.debug( + "Not found. Trying to find the oldest statistic after %s", + start, + ) + # We are in this code path only if get_last_statistics found a stat + # so statistics_during_period should also have found at least one. + assert stats cost_sum = cast(float, stats[cost_statistic_id][0]["sum"]) consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"]) last_stats_time = stats[consumption_statistic_id][0]["start"] - assert last_stats_time == start.timestamp() cost_statistics = [] consumption_statistics = [] diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index c347e52ef0e52e..6c78dc5229c844 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.8.0"] + "requirements": ["opower==0.8.3"] } diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json index bc7812c1db777a..e1afa5b89099b6 100644 --- a/homeassistant/components/otbr/strings.json +++ b/homeassistant/components/otbr/strings.json @@ -13,7 +13,9 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "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%]" } }, "issues": { diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 7cce25d08d5dfd..0576421fa713a9 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client_session=async_get_clientsession(hass), ) - if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + if (custom_account := entry.data.get(CONF_ACCOUNT)) is not None: client.custom_account_id = custom_account try: @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" - if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + if (custom_account := entry.data.get(CONF_ACCOUNT)) is not None: client.custom_account_id = custom_account async with asyncio.timeout(10): diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 87d53e5fbf9422..e65aae91e0f4f2 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -46,7 +46,7 @@ async def async_step_user( client_session=async_get_clientsession(self.hass), ) - if custom_account := user_input.get(CONF_ACCOUNT) is not None: + if (custom_account := user_input.get(CONF_ACCOUNT)) is not None: client.custom_account_id = custom_account try: diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py index c748c63e992d35..ab32a5d7352a07 100644 --- a/homeassistant/components/radarr/config_flow.py +++ b/homeassistant/components/radarr/config_flow.py @@ -10,6 +10,7 @@ from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient import voluptuous as vol +from yarl import URL from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL @@ -54,6 +55,12 @@ async def async_step_user( user_input = dict(self.entry.data) if self.entry else None else: + # aiopyarr defaults to the service port if one isn't given + # this is counter to standard practice where http = 80 + # and https = 443. + url = URL(user_input[CONF_URL]) + user_input[CONF_URL] = f"{url.scheme}://{url.host}:{url.port}{url.path}" + try: if result := await validate_input(self.hass, user_input): user_input[CONF_API_KEY] = result[1] diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index c1042a9546d16f..b2340b3455613b 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -10,13 +10,9 @@ from ring_doorbell import Auth, Ring, RingDevices from homeassistant.config_entries import ConfigEntry -from homeassistant.const import APPLICATION_NAME, CONF_TOKEN +from homeassistant.const import APPLICATION_NAME, CONF_DEVICE_ID, CONF_TOKEN from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - instance_id, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_LISTEN_CREDENTIALS, DOMAIN, PLATFORMS @@ -38,18 +34,12 @@ class RingData: type RingConfigEntry = ConfigEntry[RingData] -async def get_auth_agent_id(hass: HomeAssistant) -> tuple[str, str]: - """Return user-agent and hardware id for Auth instantiation. +def get_auth_user_agent() -> str: + """Return user-agent for Auth instantiation. user_agent will be the display name in the ring.com authorised devices. - hardware_id will uniquely describe the authorised HA device. """ - user_agent = f"{APPLICATION_NAME}/{DOMAIN}-integration" - - # Generate a new uuid from the instance_uuid to keep the HA one private - instance_uuid = uuid.UUID(hex=await instance_id.async_get(hass)) - hardware_id = str(uuid.uuid5(instance_uuid, user_agent)) - return user_agent, hardware_id + return f"{APPLICATION_NAME}/{DOMAIN}-integration" async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool: @@ -69,13 +59,13 @@ def listen_credentials_updater(token: dict[str, Any]) -> None: data={**entry.data, CONF_LISTEN_CREDENTIALS: token}, ) - user_agent, hardware_id = await get_auth_agent_id(hass) + user_agent = get_auth_user_agent() client_session = async_get_clientsession(hass) auth = Auth( user_agent, entry.data[CONF_TOKEN], token_updater, - hardware_id=hardware_id, + hardware_id=entry.data[CONF_DEVICE_ID], http_client_session=client_session, ) ring = Ring(auth) @@ -138,3 +128,25 @@ def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None: return None await er.async_migrate_entries(hass, entry_id, _async_migrator) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old config entry.""" + entry_version = entry.version + entry_minor_version = entry.minor_version + + new_minor_version = 2 + if entry_version == 1 and entry_minor_version == 1: + _LOGGER.debug( + "Migrating from version %s.%s", entry_version, entry_minor_version + ) + hardware_id = str(uuid.uuid4()) + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_DEVICE_ID: hardware_id}, + minor_version=new_minor_version, + ) + _LOGGER.debug( + "Migration to version %s.%s complete", entry_version, new_minor_version + ) + return True diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 8b933e8580db39..abeaea07171c2c 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -3,18 +3,25 @@ from collections.abc import Mapping import logging from typing import Any +import uuid from ring_doorbell import Auth, AuthenticationError, Requires2FAError import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import get_auth_agent_id -from .const import CONF_2FA, DOMAIN +from . import get_auth_user_agent +from .const import CONF_2FA, CONF_CONFIG_ENTRY_MINOR_VERSION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,11 +30,15 @@ ) STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + -async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]: +async def validate_input( + hass: HomeAssistant, hardware_id: str, data: dict[str, str] +) -> dict[str, Any]: """Validate the user input allows us to connect.""" - user_agent, hardware_id = await get_auth_agent_id(hass) + user_agent = get_auth_user_agent() auth = Auth( user_agent, http_client_session=async_get_clientsession(hass), @@ -52,8 +63,10 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ring.""" VERSION = 1 + MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION user_pass: dict[str, Any] = {} + hardware_id: str | None = None reauth_entry: ConfigEntry | None = None async def async_step_user( @@ -64,8 +77,10 @@ async def async_step_user( if user_input is not None: await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() + if not self.hardware_id: + self.hardware_id = str(uuid.uuid4()) try: - token = await validate_input(self.hass, user_input) + token = await validate_input(self.hass, self.hardware_id, user_input) except Require2FA: self.user_pass = user_input @@ -78,7 +93,11 @@ async def async_step_user( else: return self.async_create_entry( title=user_input[CONF_USERNAME], - data={CONF_USERNAME: user_input[CONF_USERNAME], CONF_TOKEN: token}, + data={ + CONF_DEVICE_ID: self.hardware_id, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_TOKEN: token, + }, ) return self.async_show_form( @@ -120,8 +139,13 @@ async def async_step_reauth_confirm( if user_input: user_input[CONF_USERNAME] = self.reauth_entry.data[CONF_USERNAME] + # Reauth will use the same hardware id and re-authorise an existing + # authorised device. + if not self.hardware_id: + self.hardware_id = self.reauth_entry.data[CONF_DEVICE_ID] + assert self.hardware_id try: - token = await validate_input(self.hass, user_input) + token = await validate_input(self.hass, self.hardware_id, user_input) except Require2FA: self.user_pass = user_input return await self.async_step_2fa() @@ -134,6 +158,7 @@ async def async_step_reauth_confirm( data = { CONF_USERNAME: user_input[CONF_USERNAME], CONF_TOKEN: token, + CONF_DEVICE_ID: self.hardware_id, } self.hass.config_entries.async_update_entry( self.reauth_entry, data=data @@ -146,7 +171,8 @@ async def async_step_reauth_confirm( data_schema=STEP_REAUTH_DATA_SCHEMA, errors=errors, description_placeholders={ - CONF_USERNAME: self.reauth_entry.data[CONF_USERNAME] + CONF_USERNAME: self.reauth_entry.data[CONF_USERNAME], + CONF_NAME: self.reauth_entry.data[CONF_USERNAME], }, ) diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py index 24801045b17151..9595241ebb1425 100644 --- a/homeassistant/components/ring/const.py +++ b/homeassistant/components/ring/const.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Final from homeassistant.const import Platform @@ -31,3 +32,5 @@ CONF_2FA = "2fa" CONF_LISTEN_CREDENTIALS = "listen_token" + +CONF_CONFIG_ENTRY_MINOR_VERSION: Final = 2 diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index e35b13394cbbcb..86d131b4f80c4d 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -28,7 +28,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/rympro/strings.json b/homeassistant/components/rympro/strings.json index c58bf5b93babd6..2c1e2ad93c9a46 100644 --- a/homeassistant/components/rympro/strings.json +++ b/homeassistant/components/rympro/strings.json @@ -14,7 +14,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/homeassistant/components/schlage/coordinator.py b/homeassistant/components/schlage/coordinator.py index 365fabb8ac7da9..53bb43751a96b9 100644 --- a/homeassistant/components/schlage/coordinator.py +++ b/homeassistant/components/schlage/coordinator.py @@ -90,13 +90,21 @@ def _add_remove_locks(self) -> None: devices = dr.async_entries_for_config_entry( device_registry, self.config_entry.entry_id ) - previous_locks = {device.id for device in devices} + previous_locks = set() + previous_locks_by_lock_id = {} + for device in devices: + for domain, identifier in device.identifiers: + if domain == DOMAIN: + previous_locks.add(identifier) + previous_locks_by_lock_id[identifier] = device + continue current_locks = set(self.data.locks.keys()) + if removed_locks := previous_locks - current_locks: LOGGER.debug("Removed locks: %s", ", ".join(removed_locks)) - for device_id in removed_locks: + for lock_id in removed_locks: device_registry.async_update_device( - device_id=device_id, + device_id=previous_locks_by_lock_id[lock_id].id, remove_config_entry_id=self.config_entry.entry_id, ) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 59f20a9ed25035..675d24b9240a4d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -6,7 +6,6 @@ from collections.abc import Callable, Iterable from contextlib import suppress import datetime -from functools import partial import itertools import logging import math @@ -39,6 +38,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.loader import async_suggest_report_issue from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.enum import try_parse_enum from homeassistant.util.hass_dict import HassKey @@ -686,7 +686,6 @@ def list_statistic_ids( @callback def _update_issues( report_issue: Callable[[str, str, dict[str, Any]], None], - clear_issue: Callable[[str, str], None], sensor_states: list[State], metadatas: dict[str, tuple[int, StatisticMetaData]], ) -> None: @@ -707,8 +706,6 @@ def _update_issues( entity_id, {"statistic_id": entity_id}, ) - else: - clear_issue("state_class_removed", entity_id) metadata_unit = metadata[1]["unit_of_measurement"] converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit) @@ -725,8 +722,6 @@ def _update_issues( "supported_unit": metadata_unit, }, ) - else: - clear_issue("units_changed", entity_id) elif numeric and state_unit not in converter.VALID_UNITS: # The state unit can't be converted to the unit in metadata valid_units = (unit or "" for unit in converter.VALID_UNITS) @@ -741,8 +736,6 @@ def _update_issues( "supported_unit": valid_units_str, }, ) - else: - clear_issue("units_changed", entity_id) def update_statistics_issues( @@ -756,36 +749,50 @@ def update_statistics_issues( instance, session, statistic_source=RECORDER_DOMAIN ) + @callback + def get_sensor_statistics_issues(hass: HomeAssistant) -> set[str]: + """Return a list of statistics issues.""" + issues = set() + issue_registry = ir.async_get(hass) + for issue in issue_registry.issues.values(): + if ( + issue.domain != DOMAIN + or not (issue_data := issue.data) + or issue_data.get("issue_type") + not in ("state_class_removed", "units_changed") + ): + continue + issues.add(issue.issue_id) + return issues + + issues = run_callback_threadsafe( + hass.loop, get_sensor_statistics_issues, hass + ).result() + def create_issue_registry_issue( issue_type: str, statistic_id: str, data: dict[str, Any] ) -> None: """Create an issue registry issue.""" - hass.loop.call_soon_threadsafe( - partial( - ir.async_create_issue, - hass, - DOMAIN, - f"{issue_type}_{statistic_id}", - data=data | {"issue_type": issue_type}, - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key=issue_type, - translation_placeholders=data, - ) - ) - - def delete_issue_registry_issue(issue_type: str, statistic_id: str) -> None: - """Delete an issue registry issue.""" - hass.loop.call_soon_threadsafe( - ir.async_delete_issue, hass, DOMAIN, f"{issue_type}_{statistic_id}" + issue_id = f"{issue_type}_{statistic_id}" + issues.discard(issue_id) + ir.create_issue( + hass, + DOMAIN, + issue_id, + data=data | {"issue_type": issue_type}, + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key=issue_type, + translation_placeholders=data, ) _update_issues( create_issue_registry_issue, - delete_issue_registry_issue, sensor_states, metadatas, ) + for issue_id in issues: + hass.loop.call_soon_threadsafe(ir.async_delete_issue, hass, DOMAIN, issue_id) def validate_statistics( @@ -811,7 +818,6 @@ def create_statistic_validation_issue( _update_issues( create_statistic_validation_issue, - lambda issue_type, statistic_id: None, sensor_states, metadatas, ) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index febd4e34aaf370..d8a7929ae7927c 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -135,3 +135,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data[PYSMA_REMOVE_LISTENER]() return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate entry.""" + + _LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + # 1 -> 2: Unique ID from integer to string + if entry.minor_version == 1: + minor_version = 2 + hass.config_entries.async_update_entry( + entry, unique_id=str(entry.unique_id), minor_version=minor_version + ) + + _LOGGER.debug("Migration successful") + + return True diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index fe26cbee2c83a1..4b3e01a79a8bbc 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -40,6 +40,7 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SMA.""" VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """Initialize.""" @@ -76,7 +77,7 @@ async def async_step_user( errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(device_info["serial"]) + await self.async_set_unique_id(str(device_info["serial"])) self._abort_if_unique_id_configured(updates=self._data) return self.async_create_entry( title=self._data[CONF_HOST], data=self._data diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index 10984e8efb1c18..c1eca45871b206 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/smlight", "integration_type": "device", "iot_class": "local_push", - "requirements": ["pysmlight==0.1.2"], + "requirements": ["pysmlight==0.1.3"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/homeassistant/components/solarlog/entity.py b/homeassistant/components/solarlog/entity.py index 1d91fc8726b210..b0f3ddf99f9e01 100644 --- a/homeassistant/components/solarlog/entity.py +++ b/homeassistant/components/solarlog/entity.py @@ -38,7 +38,7 @@ def __init__( """Initialize the SolarLogCoordinator sensor.""" super().__init__(coordinator, description) - self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" self._attr_device_info = DeviceInfo( manufacturer="Solar-Log", model="Controller", @@ -59,8 +59,8 @@ def __init__( ) -> None: """Initialize the SolarLogInverter sensor.""" super().__init__(coordinator, description) - name = f"{coordinator.unique_id}-{slugify(coordinator.solarlog.device_name(device_id))}" - self._attr_unique_id = f"{name}-{description.key}" + name = f"{coordinator.unique_id}_{slugify(coordinator.solarlog.device_name(device_id))}" + self._attr_unique_id = f"{name}_{description.key}" self._attr_device_info = DeviceInfo( manufacturer="Solar-Log", model="Inverter", diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 99ddc2ed162e87..274c97c76b52de 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solarlog", "iot_class": "local_polling", "loggers": ["solarlog_cli"], - "requirements": ["solarlog_cli==0.3.0"] + "requirements": ["solarlog_cli==0.3.1"] } diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 69ebbbcceda7cb..89c41194859aca 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -32,7 +32,8 @@ "reconfigure_confirm": { "title": "Configure SolarLog", "data": { - "has_password": "[%key:component::solarlog::config::step::user::data::has_password%]" + "has_password": "[%key:component::solarlog::config::step::user::data::has_password%]", + "password": "[%key:common::config_flow::data::password%]" } } }, diff --git a/homeassistant/components/squeezebox/icons.json b/homeassistant/components/squeezebox/icons.json index e86016329f5bb3..29911ddad77f3c 100644 --- a/homeassistant/components/squeezebox/icons.json +++ b/homeassistant/components/squeezebox/icons.json @@ -27,12 +27,6 @@ }, "call_query": { "service": "mdi:database" - }, - "sync": { - "service": "mdi:sync" - }, - "unsync": { - "service": "mdi:sync-off" } } } diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 90f9bf2d7695f0..07885ae5dd6884 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -30,19 +30,3 @@ call_query: advanced: true selector: object: -sync: - target: - entity: - integration: squeezebox - domain: media_player - fields: - other_player: - required: true - example: "media_player.living_room" - selector: - text: -unsync: - target: - entity: - integration: squeezebox - domain: media_player diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 1a120ee0567db8..b1b71cd8c1d1a6 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -60,20 +60,6 @@ "description": "[%key:component::squeezebox::services::call_method::fields::parameters::description%]" } } - }, - "sync": { - "name": "Sync", - "description": "Adds another player to this player's sync group. If the other player is already in a sync group, it will leave it.\n.", - "fields": { - "other_player": { - "name": "Other player", - "description": "Name of the other Squeezebox player to link." - } - } - }, - "unsync": { - "name": "Unsync", - "description": "Removes this player from its sync group." } }, "entity": { diff --git a/homeassistant/components/surepetcare/strings.json b/homeassistant/components/surepetcare/strings.json index c3b7864f36ae2a..58db669732a7f9 100644 --- a/homeassistant/components/surepetcare/strings.json +++ b/homeassistant/components/surepetcare/strings.json @@ -21,7 +21,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "services": { diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index d12eddbb14ad27..32a171a11caca8 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -37,17 +37,29 @@ class SensorData: def as_dict(self) -> dict[str, Any]: """Return as dict.""" + disk_usage = None + if self.disk_usage: + disk_usage = {k: str(v) for k, v in self.disk_usage.items()} + io_counters = None + if self.io_counters: + io_counters = {k: str(v) for k, v in self.io_counters.items()} + addresses = None + if self.addresses: + addresses = {k: str(v) for k, v in self.addresses.items()} + temperatures = None + if self.temperatures: + temperatures = {k: str(v) for k, v in self.temperatures.items()} return { - "disk_usage": {k: str(v) for k, v in self.disk_usage.items()}, + "disk_usage": disk_usage, "swap": str(self.swap), "memory": str(self.memory), - "io_counters": {k: str(v) for k, v in self.io_counters.items()}, - "addresses": {k: str(v) for k, v in self.addresses.items()}, + "io_counters": io_counters, + "addresses": addresses, "load": str(self.load), "cpu_percent": str(self.cpu_percent), "boot_time": str(self.boot_time), "processes": str(self.processes), - "temperatures": {k: str(v) for k, v in self.temperatures.items()}, + "temperatures": temperatures, } diff --git a/homeassistant/components/tesla_fleet/button.py b/homeassistant/components/tesla_fleet/button.py index 548bf065397964..87cd95576d2b06 100644 --- a/homeassistant/components/tesla_fleet/button.py +++ b/homeassistant/components/tesla_fleet/button.py @@ -20,8 +20,9 @@ PARALLEL_UPDATES = 0 -async def do_nothing() -> None: - """Do nothing.""" +async def do_nothing() -> dict[str, dict[str, bool]]: + """Do nothing with a positive result.""" + return {"response": {"result": True}} @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 1a6eb0fb8c8564..ba7d930fcd0558 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -378,7 +378,17 @@ class TeslemetryTimeEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, ), - SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM), + SensorEntityDescription( + key="island_status", + device_class=SensorDeviceClass.ENUM, + options=[ + "on_grid", + "off_grid", + "off_grid_intentional", + "off_grid_unintentional", + "island_status_unknown", + ], + ), ) WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index b7ba06fbce41bc..9c3fc09b07bae1 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -392,6 +392,16 @@ "grid_services_power": { "name": "Grid services power" }, + "island_status": { + "name": "Island status", + "state": { + "island_status_unknown": "Unknown", + "on_grid": "On grid", + "off_grid": "Off grid", + "off_grid_intentional": "Off grid intentional", + "off_grid_unintentional": "Off grid unintentional" + } + }, "load_power": { "name": "Load power" }, diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 52c03c8700b362..336a6b9404cc14 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", diff --git a/homeassistant/components/touchline_sl/manifest.json b/homeassistant/components/touchline_sl/manifest.json index 2329cb67e17409..dd591cbf03883a 100644 --- a/homeassistant/components/touchline_sl/manifest.json +++ b/homeassistant/components/touchline_sl/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/touchline_sl", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["pytouchlinesl==0.1.7"] + "requirements": ["pytouchlinesl==0.1.8"] } diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 3bd6aba5c26d23..f86992ea0cfd65 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -15,7 +15,7 @@ HVACAction, HVACMode, ) -from homeassistant.const import PRECISION_WHOLE +from homeassistant.const import PRECISION_TENTHS from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -64,7 +64,7 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - _attr_precision = PRECISION_WHOLE + _attr_precision = PRECISION_TENTHS # This disables the warning for async_turn_{on,off}, can be removed later. _enable_turn_on_off_backwards_compatibility = False diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 81506c41a6d514..ab1eac7d0c0449 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.4"] + "requirements": ["python-kasa[speedups]==0.7.5"] } diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 99d81848a91708..4d3710f7570c01 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -17,6 +17,17 @@ from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType from .util import remap_value +_DPTYPE_MAPPING: dict[str, DPType] = { + "Bitmap": DPType.RAW, + "bitmap": DPType.RAW, + "bool": DPType.BOOLEAN, + "enum": DPType.ENUM, + "json": DPType.JSON, + "raw": DPType.RAW, + "string": DPType.STRING, + "value": DPType.INTEGER, +} + @dataclass class IntegerTypeData: @@ -256,7 +267,13 @@ def get_dptype( order = ["function", "status_range"] for key in order: if dpcode in getattr(self.device, key): - return DPType(getattr(self.device, key)[dpcode].type) + current_type = getattr(self.device, key)[dpcode].type + try: + return DPType(current_type) + except ValueError: + # Sometimes, we get ill-formed DPTypes from the cloud, + # this fixes them and maps them to the correct DPType. + return _DPTYPE_MAPPING.get(current_type) return None diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index aaef111a3517bd..9238c825390d43 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -42,7 +42,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "discovery_started": "Discovery started" + "discovery_started": "Discovery started", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/venstar/entity.py b/homeassistant/components/venstar/entity.py index 630da05324eb3d..b8a4b971a7f53b 100644 --- a/homeassistant/components/venstar/entity.py +++ b/homeassistant/components/venstar/entity.py @@ -34,11 +34,11 @@ def _handle_coordinator_update(self) -> None: @property def device_info(self) -> DeviceInfo: """Return the device information for this entity.""" - fw_ver_major, fw_ver_minor = self._client.get_firmware_ver() + firmware_version = self._client.get_firmware_ver() return DeviceInfo( identifiers={(DOMAIN, self._config.entry_id)}, name=self._client.name, manufacturer="Venstar", model=f"{self._client.model}-{self._client.get_type()}", - sw_version=f"{fw_ver_major}.{fw_ver_minor}", + sw_version=f"{firmware_version[0]}.{firmware_version[1]}", ) diff --git a/homeassistant/components/vicare/types.py b/homeassistant/components/vicare/types.py index 7e1ec7f8beee0a..dc105a86aa9d5e 100644 --- a/homeassistant/components/vicare/types.py +++ b/homeassistant/components/vicare/types.py @@ -1,6 +1,7 @@ """Types for the ViCare integration.""" from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass import enum from typing import Any @@ -48,8 +49,12 @@ def from_ha_preset( ) -> str | None: """Return the mapped ViCare heating program for the Home Assistant preset.""" for program in supported_heating_programs: - if VICARE_TO_HA_PRESET_HEATING.get(HeatingProgram(program)) == ha_preset: - return program + with suppress(ValueError): + if ( + VICARE_TO_HA_PRESET_HEATING.get(HeatingProgram(program)) + == ha_preset + ): + return program return None diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 4b4673b771eb48..09257652ece24a 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -27,7 +27,8 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 20fd72845aee17..4c78f82bfe7ffe 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -802,7 +802,8 @@ def _async_add_workout_entities() -> None: if not entities: LOGGER.warning( - "No data found for Withings entry %s, sensors will be added when new data is available" + "No data found for Withings entry %s, sensors will be added when new data is available", + entry.title, ) async_add_entities(entities) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index fb86b16c3be15a..5e5f18aeab8894 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -20,7 +20,9 @@ "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "Authenticated account does not match the account to be reauthenticated. Please log in with the correct account." }, "create_entry": { "default": "Successfully authenticated with Withings." diff --git a/homeassistant/components/wmspro/config_flow.py b/homeassistant/components/wmspro/config_flow.py index ba3b5ef367d1af..19b9ab28e6af61 100644 --- a/homeassistant/components/wmspro/config_flow.py +++ b/homeassistant/components/wmspro/config_flow.py @@ -75,7 +75,7 @@ async def async_step_user( if self.source == dhcp.DOMAIN: discovery_info: DhcpServiceInfo = self.init_data - data_values = {CONF_HOST: discovery_info.hostname or discovery_info.ip} + data_values = {CONF_HOST: discovery_info.ip} else: data_values = {CONF_HOST: SUGGESTED_HOST} diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json index 3e0c4e21e6c76d..f174bcc89c7660 100644 --- a/homeassistant/components/wmspro/manifest.json +++ b/homeassistant/components/wmspro/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/wmspro", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["pywmspro==0.2.0"] + "requirements": ["pywmspro==0.2.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 1201354bab2ed2..cf3afb5fc37e8e 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.57"] + "requirements": ["holidays==0.58"] } diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index d0ee6c030a6915..eaa5ac50c80998 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -20,7 +20,9 @@ "yxc_control_url_missing": "The control URL is not given in the ssdp description." }, "error": { - "no_musiccast_device": "This device seems to be no MusicCast Device." + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_musiccast_device": "This device seems to be no MusicCast Device.", + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "entity": { diff --git a/homeassistant/components/zwave_js/config_validation.py b/homeassistant/components/zwave_js/config_validation.py index 6c060f90ce5d1b..30bc2f1678916a 100644 --- a/homeassistant/components/zwave_js/config_validation.py +++ b/homeassistant/components/zwave_js/config_validation.py @@ -34,6 +34,8 @@ def boolean(value: Any) -> bool: VALUE_SCHEMA = vol.Any( boolean, + float, + int, vol.Coerce(int), vol.Coerce(float), BITMASK_SCHEMA, diff --git a/homeassistant/const.py b/homeassistant/const.py index 26049ed326bb93..b539cbc6068fd0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index f627f1f0f47dd1..a2d9b663cecbac 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -267,6 +267,11 @@ } ZEROCONF = { + "_PowerView-G3._tcp.local.": [ + { + "domain": "hunterdouglas_powerview", + }, + ], "_Volumio._tcp.local.": [ { "domain": "volumio", @@ -695,11 +700,6 @@ "domain": "plugwise", }, ], - "_powerview-g3._tcp.local.": [ - { - "domain": "hunterdouglas_powerview", - }, - ], "_powerview._tcp.local.": [ { "domain": "hunterdouglas_powerview", diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9f8eb628e63bdf..6d56fe708d04df 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -9,6 +9,7 @@ from collections.abc import Callable, Generator, Iterable from contextlib import AbstractContextManager from contextvars import ContextVar +from copy import deepcopy from datetime import date, datetime, time, timedelta from functools import cache, cached_property, lru_cache, partial, wraps import json @@ -2166,7 +2167,8 @@ def merge_response(value: ServiceResponse) -> list[Any]: is_single_list = False response_items: list = [] - for entity_id, entity_response in value.items(): # pylint: disable=too-many-nested-blocks + input_service_response = deepcopy(value) + for entity_id, entity_response in input_service_response.items(): # pylint: disable=too-many-nested-blocks if not isinstance(entity_response, dict): raise TypeError("Response is not a dictionary") for value_key, type_response in entity_response.items(): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1da76f572a16ca..159463e8928510 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20241002.2 +home-assistant-frontend==20241002.3 home-assistant-intents==2024.10.2 httpx==0.27.2 ifaddr==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 955aac83f367a3..a79ffb0fe57b2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.10.1" +version = "2024.10.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 2563b7a1eb92e3..4818ded19dc852 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ AIOSomecomfort==0.0.25 Adax-local==0.1.5 # homeassistant.components.doorbird -DoorBirdPy==3.0.2 +DoorBirdPy==3.0.4 # homeassistant.components.homekit HAP-python==4.9.1 @@ -45,7 +45,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.1 +PyChromecast==14.0.3 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -176,7 +176,7 @@ aio-georss-gdacs==0.10 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.5 +aioairzone-cloud==0.6.6 # homeassistant.components.airzone aioairzone==0.9.3 @@ -198,7 +198,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.9.3 +aioautomower==2024.10.0 # homeassistant.components.azure_devops aioazuredevops==2.2.1 @@ -380,7 +380,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.3.1 +aiostreammagic==2.5.0 # homeassistant.components.switcher_kis aioswitcher==4.0.3 @@ -419,7 +419,7 @@ aiowithings==3.0.3 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.9.0 +airgradient==0.9.1 # homeassistant.components.airly airly==1.1.0 @@ -532,7 +532,7 @@ autarco==3.0.0 axis==62 # homeassistant.components.fujitsu_fglair -ayla-iot-unofficial==1.4.1 +ayla-iot-unofficial==1.4.2 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -933,7 +933,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.6.6 +fyta_cli==0.6.7 # homeassistant.components.google_translate gTTS==2.2.4 @@ -1114,10 +1114,10 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.57 +holidays==0.58 # homeassistant.components.frontend -home-assistant-frontend==20241002.2 +home-assistant-frontend==20241002.3 # homeassistant.components.conversation home-assistant-intents==2024.10.2 @@ -1176,7 +1176,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.5 +imgw_pib==1.0.6 # homeassistant.components.incomfort incomfort-client==0.6.3-1 @@ -1387,7 +1387,7 @@ mopeka-iot-ble==0.8.0 motionblinds==0.6.25 # homeassistant.components.motionblinds_ble -motionblindsble==0.1.1 +motionblindsble==0.1.2 # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1484,7 +1484,7 @@ numato-gpio==0.13.0 numpy==1.26.4 # homeassistant.components.nyt_games -nyt_games==0.4.2 +nyt_games==0.4.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1544,7 +1544,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.8.0 +opower==0.8.3 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1710,7 +1710,7 @@ pyCEC==0.5.2 pyControl4==1.2.0 # homeassistant.components.duotecno -pyDuotecno==2024.9.0 +pyDuotecno==2024.10.0 # homeassistant.components.electrasmart pyElectra==1.2.4 @@ -1780,7 +1780,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==1.0.2 +pyblu==1.0.3 # homeassistant.components.neato pybotvac==0.0.25 @@ -1855,7 +1855,7 @@ pyebox==1.1.4 pyecoforest==0.4.0 # homeassistant.components.econet -pyeconet==0.1.22 +pyeconet==0.1.23 # homeassistant.components.ista_ecotrend pyecotrend-ista==3.3.1 @@ -2244,7 +2244,7 @@ pysmarty2==0.10.1 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.2 +pysmlight==0.1.3 # homeassistant.components.snmp pysnmp==6.2.6 @@ -2340,10 +2340,10 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.4 +python-kasa[speedups]==0.7.5 # homeassistant.components.linkplay -python-linkplay==0.0.12 +python-linkplay==0.0.15 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -2413,7 +2413,7 @@ pytomorrowio==0.3.6 pytouchline==0.7 # homeassistant.components.touchline_sl -pytouchlinesl==0.1.7 +pytouchlinesl==0.1.8 # homeassistant.components.traccar # homeassistant.components.traccar_server @@ -2477,7 +2477,7 @@ pywilight==0.0.74 pywizlight==0.5.14 # homeassistant.components.wmspro -pywmspro==0.2.0 +pywmspro==0.2.1 # homeassistant.components.ws66i pyws66i==1.1 @@ -2676,7 +2676,7 @@ soco==0.30.4 solaredge-local==0.2.3 # homeassistant.components.solarlog -solarlog_cli==0.3.0 +solarlog_cli==0.3.1 # homeassistant.components.solax solax==3.1.1 @@ -2992,7 +2992,7 @@ xiaomi-ble==0.32.0 xknx==3.2.0 # homeassistant.components.knx -xknxproject==3.8.0 +xknxproject==3.8.1 # homeassistant.components.fritz # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bee8274ca604da..ed42a88ef621a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,7 +13,7 @@ AIOSomecomfort==0.0.25 Adax-local==0.1.5 # homeassistant.components.doorbird -DoorBirdPy==3.0.2 +DoorBirdPy==3.0.4 # homeassistant.components.homekit HAP-python==4.9.1 @@ -42,7 +42,7 @@ PlexAPI==4.15.16 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.1 +PyChromecast==14.0.3 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -164,7 +164,7 @@ aio-georss-gdacs==0.10 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.5 +aioairzone-cloud==0.6.6 # homeassistant.components.airzone aioairzone==0.9.3 @@ -186,7 +186,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.9.3 +aioautomower==2024.10.0 # homeassistant.components.azure_devops aioazuredevops==2.2.1 @@ -362,7 +362,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.3.1 +aiostreammagic==2.5.0 # homeassistant.components.switcher_kis aioswitcher==4.0.3 @@ -401,7 +401,7 @@ aiowithings==3.0.3 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.9.0 +airgradient==0.9.1 # homeassistant.components.airly airly==1.1.0 @@ -481,7 +481,7 @@ autarco==3.0.0 axis==62 # homeassistant.components.fujitsu_fglair -ayla-iot-unofficial==1.4.1 +ayla-iot-unofficial==1.4.2 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -786,7 +786,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.6.6 +fyta_cli==0.6.7 # homeassistant.components.google_translate gTTS==2.2.4 @@ -940,10 +940,10 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.57 +holidays==0.58 # homeassistant.components.frontend -home-assistant-frontend==20241002.2 +home-assistant-frontend==20241002.3 # homeassistant.components.conversation home-assistant-intents==2024.10.2 @@ -987,7 +987,7 @@ idasen-ha==2.6.2 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.5 +imgw_pib==1.0.6 # homeassistant.components.incomfort incomfort-client==0.6.3-1 @@ -1156,7 +1156,7 @@ mopeka-iot-ble==0.8.0 motionblinds==0.6.25 # homeassistant.components.motionblinds_ble -motionblindsble==0.1.1 +motionblindsble==0.1.2 # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1232,7 +1232,7 @@ numato-gpio==0.13.0 numpy==1.26.4 # homeassistant.components.nyt_games -nyt_games==0.4.2 +nyt_games==0.4.3 # homeassistant.components.google oauth2client==4.1.3 @@ -1274,7 +1274,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.7 # homeassistant.components.opower -opower==0.8.0 +opower==0.8.3 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1396,7 +1396,7 @@ pyCEC==0.5.2 pyControl4==1.2.0 # homeassistant.components.duotecno -pyDuotecno==2024.9.0 +pyDuotecno==2024.10.0 # homeassistant.components.electrasmart pyElectra==1.2.4 @@ -1448,7 +1448,7 @@ pybalboa==1.0.2 pyblackbird==0.6 # homeassistant.components.bluesound -pyblu==1.0.2 +pyblu==1.0.3 # homeassistant.components.neato pybotvac==0.0.25 @@ -1493,7 +1493,7 @@ pydroid-ipcam==2.0.0 pyecoforest==0.4.0 # homeassistant.components.econet -pyeconet==0.1.22 +pyeconet==0.1.23 # homeassistant.components.ista_ecotrend pyecotrend-ista==3.3.1 @@ -1798,7 +1798,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.2 +pysmlight==0.1.3 # homeassistant.components.snmp pysnmp==6.2.6 @@ -1861,10 +1861,10 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.4 +python-kasa[speedups]==0.7.5 # homeassistant.components.linkplay -python-linkplay==0.0.12 +python-linkplay==0.0.15 # homeassistant.components.matter python-matter-server==6.5.2 @@ -1919,7 +1919,7 @@ pytile==2023.12.0 pytomorrowio==0.3.6 # homeassistant.components.touchline_sl -pytouchlinesl==0.1.7 +pytouchlinesl==0.1.8 # homeassistant.components.traccar # homeassistant.components.traccar_server @@ -1977,7 +1977,7 @@ pywilight==0.0.74 pywizlight==0.5.14 # homeassistant.components.wmspro -pywmspro==0.2.0 +pywmspro==0.2.1 # homeassistant.components.ws66i pyws66i==1.1 @@ -2122,7 +2122,7 @@ snapcast==2.3.6 soco==0.30.4 # homeassistant.components.solarlog -solarlog_cli==0.3.0 +solarlog_cli==0.3.1 # homeassistant.components.solax solax==3.1.1 @@ -2381,7 +2381,7 @@ xiaomi-ble==0.32.0 xknx==3.2.0 # homeassistant.components.knx -xknxproject==3.8.0 +xknxproject==3.8.1 # homeassistant.components.fritz # homeassistant.components.rest diff --git a/script/licenses.py b/script/licenses.py index f39dcf13c14353..b04b3cd272602b 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -165,6 +165,8 @@ def from_dict(cls, data: dict[str, str]) -> PackageDefinition: "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 "vincenty", # Public domain "zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46 + # Using License-Expression (with hatchling) + "ftfy", # Apache-2.0 } TODO = { diff --git a/tests/components/hunterdouglas_powerview/const.py b/tests/components/hunterdouglas_powerview/const.py index 5a912a63a17939..db8adc57e5a28a 100644 --- a/tests/components/hunterdouglas_powerview/const.py +++ b/tests/components/hunterdouglas_powerview/const.py @@ -41,7 +41,7 @@ ip_address="1.2.3.4", ip_addresses=[IPv4Address("1.2.3.4")], hostname="mock_hostname", - name="Powerview Generation 3._powerview-g3._tcp.local.", + name="Powerview Generation 3._PowerView-G3._tcp.local.", port=None, properties={}, type="mock_type", diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index adf70fb0aab8ce..e79bd1f8145d22 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -20,7 +20,7 @@ 'labels': set({ }), 'manufacturer': 'Husqvarna', - 'model': 'HUSQVARNA AUTOMOWER® 450XH', + 'model': 'AUTOMOWER® 450XH', 'model_id': None, 'name': 'Test Mower 1', 'name_by_user': None, diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index c3f77ca5007cce..568d97b8d468a8 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -117,6 +117,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result2["data"][CONF_ACCOUNT] == FIXTURE_USER_INPUT[CONF_ACCOUNT] async def test_reauth_authorization_error(hass: HomeAssistant) -> None: diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 0ff9353695748f..096c78e1c4a69d 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -137,6 +137,23 @@ async def test_zero_conf(hass: HomeAssistant) -> None: assert result["data"] == CONF_DATA +async def test_url_rewrite(hass: HomeAssistant) -> None: + """Test auth flow url rewrite.""" + with patch( + "homeassistant.components.radarr.config_flow.RadarrClient.async_try_zeroconf", + return_value=("v3", API_KEY, "/test"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_URL: "https://192.168.1.100/test", CONF_VERIFY_SSL: False}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_URL] == "https://192.168.1.100:443/test" + + @pytest.mark.freeze_time("2021-12-03 00:00:00+00:00") async def test_full_reauth_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 90f2fd2a956b23..1296c2f58c5c42 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -8,7 +8,8 @@ import ring_doorbell from homeassistant.components.ring import DOMAIN -from homeassistant.const import CONF_USERNAME +from homeassistant.components.ring.const import CONF_CONFIG_ENTRY_MINOR_VERSION +from homeassistant.const import CONF_DEVICE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant from .device_mocks import get_devices_data, get_mock_devices @@ -16,6 +17,8 @@ from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 +MOCK_HARDWARE_ID = "foo-bar" + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -116,10 +119,13 @@ def mock_config_entry() -> MockConfigEntry: title="Ring", domain=DOMAIN, data={ + CONF_DEVICE_ID: MOCK_HARDWARE_ID, CONF_USERNAME: "foo@bar.com", "token": {"access_token": "mock-token"}, }, unique_id="foo@bar.com", + version=1, + minor_version=CONF_CONFIG_ENTRY_MINOR_VERSION, ) diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index d27c4878aea5ed..d13a78538bb9ad 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,16 +1,18 @@ """Test the Ring config flow.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest import ring_doorbell from homeassistant import config_entries from homeassistant.components.ring import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import MOCK_HARDWARE_ID + from tests.common import MockConfigEntry @@ -27,17 +29,19 @@ async def test_form( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "hello@home-assistant.io", "password": "test-password"}, - ) - await hass.async_block_till_done() + with patch("uuid.uuid4", return_value=MOCK_HARDWARE_ID): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "hello@home-assistant.io", "password": "test-password"}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "hello@home-assistant.io" assert result2["data"] == { - "username": "hello@home-assistant.io", - "token": {"access_token": "mock-token"}, + CONF_DEVICE_ID: MOCK_HARDWARE_ID, + CONF_USERNAME: "hello@home-assistant.io", + CONF_TOKEN: {"access_token": "mock-token"}, } assert len(mock_setup_entry.mock_calls) == 1 @@ -80,13 +84,14 @@ async def test_form_2fa( assert result["errors"] == {} mock_ring_auth.async_fetch_token.side_effect = ring_doorbell.Requires2FAError - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "foo@bar.com", - CONF_PASSWORD: "fake-password", - }, - ) + with patch("uuid.uuid4", return_value=MOCK_HARDWARE_ID): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "foo@bar.com", + CONF_PASSWORD: "fake-password", + }, + ) await hass.async_block_till_done() mock_ring_auth.async_fetch_token.assert_called_once_with( "foo@bar.com", "fake-password", None @@ -107,8 +112,9 @@ async def test_form_2fa( assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "foo@bar.com" assert result3["data"] == { - "username": "foo@bar.com", - "token": "new-foobar", + CONF_DEVICE_ID: MOCK_HARDWARE_ID, + CONF_USERNAME: "foo@bar.com", + CONF_TOKEN: "new-foobar", } assert len(mock_setup_entry.mock_calls) == 1 @@ -154,8 +160,9 @@ async def test_reauth( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { - "username": "foo@bar.com", - "token": "new-foobar", + CONF_DEVICE_ID: MOCK_HARDWARE_ID, + CONF_USERNAME: "foo@bar.com", + CONF_TOKEN: "new-foobar", } assert len(mock_setup_entry.mock_calls) == 1 @@ -216,8 +223,9 @@ async def test_reauth_error( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { - "username": "foo@bar.com", - "token": "new-foobar", + CONF_DEVICE_ID: MOCK_HARDWARE_ID, + CONF_USERNAME: "foo@bar.com", + CONF_TOKEN: "new-foobar", } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 5ac9e444ccae42..1b5ee68c659c31 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,5 +1,7 @@ """The tests for the Ring component.""" +from unittest.mock import AsyncMock, patch + from freezegun.api import FrozenDateTimeFactory import pytest from ring_doorbell import AuthenticationError, Ring, RingError, RingTimeout @@ -12,11 +14,12 @@ from homeassistant.components.ring.const import CONF_LISTEN_CREDENTIALS, SCAN_INTERVAL from homeassistant.components.ring.coordinator import RingEventListener from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_DEVICE_ID, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from .conftest import MOCK_HARDWARE_ID from .device_mocks import FRONT_DOOR_DEVICE_ID from tests.common import MockConfigEntry, async_fire_time_changed @@ -450,3 +453,32 @@ async def test_no_listen_start( assert "Ring event listener failed to start after 10 seconds" in [ record.message for record in caplog.records if record.levelname == "WARNING" ] + + +async def test_migrate_create_device_id( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test migration creates new device id created.""" + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + with patch("uuid.uuid4", return_value=MOCK_HARDWARE_ID): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 2 + assert CONF_DEVICE_ID in entry.data + assert entry.data[CONF_DEVICE_ID] == MOCK_HARDWARE_ID + + assert "Migration to version 1.2 complete" in caplog.text diff --git a/tests/components/schlage/test_init.py b/tests/components/schlage/test_init.py index 1f18bdde21868b..e40fc83a7acb3e 100644 --- a/tests/components/schlage/test_init.py +++ b/tests/components/schlage/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.schlage.const import DOMAIN, UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceRegistry from tests.common import MockConfigEntry, async_fire_time_changed @@ -125,6 +126,10 @@ async def test_auto_add_device( """Test new devices are auto-added to the device registry.""" device = device_registry.async_get_device(identifiers={(DOMAIN, "test")}) assert device is not None + all_devices = dr.async_entries_for_config_entry( + device_registry, mock_added_config_entry.entry_id + ) + assert len(all_devices) == 1 mock_lock_attrs["device_id"] = "test2" new_mock_lock = create_autospec(Lock) @@ -139,19 +144,21 @@ async def test_auto_add_device( new_device = device_registry.async_get_device(identifiers={(DOMAIN, "test2")}) assert new_device is not None + all_devices = dr.async_entries_for_config_entry( + device_registry, mock_added_config_entry.entry_id + ) + assert len(all_devices) == 2 + async def test_auto_remove_device( hass: HomeAssistant, device_registry: DeviceRegistry, mock_added_config_entry: ConfigEntry, mock_schlage: Mock, - mock_lock: Mock, - mock_lock_attrs: dict[str, Any], freezer: FrozenDateTimeFactory, ) -> None: """Test new devices are auto-added to the device registry.""" - device = device_registry.async_get_device(identifiers={(DOMAIN, "test")}) - assert device is not None + assert device_registry.async_get_device(identifiers={(DOMAIN, "test")}) is not None mock_schlage.locks.return_value = [] @@ -160,5 +167,8 @@ async def test_auto_remove_device( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - new_device = device_registry.async_get_device(identifiers={(DOMAIN, "test")}) - assert new_device is None + assert device_registry.async_get_device(identifiers={(DOMAIN, "test")}) is None + all_devices = dr.async_entries_for_config_entry( + device_registry, mock_added_config_entry.entry_id + ) + assert len(all_devices) == 0 diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 04e0a1b7de8b7c..37f080d2de21d0 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -4682,6 +4682,65 @@ async def test_validate_statistics_state_class_removed( await assert_validation_result(hass, client, {}, {}) +@pytest.mark.parametrize( + ("units", "attributes", "unit"), + [ + (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_state_class_removed_issue_cleaned_up( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + units, + attributes, + unit, +) -> None: + """Test validate_statistics.""" + now = get_start_time(dt_util.utcnow()) + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(hass, client, {}, {}) + + # No statistics, valid state - empty response + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) + await hass.async_block_till_done() + await assert_validation_result(hass, client, {}, {}) + + # Statistics has run, empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(hass, client, {}, {}) + + # State update with invalid state class, expect error + _attributes = dict(attributes) + _attributes.pop("state_class") + hass.states.async_set( + "sensor.test", 12, attributes=_attributes, timestamp=now.timestamp() + ) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "state_class_removed", + } + ], + } + await assert_validation_result(hass, client, expected, {"state_class_removed"}) + + # Remove the statistics - empty response + get_instance(hass).async_clear_statistics(["sensor.test"]) + await async_recorder_block_till_done(hass) + await assert_validation_result(hass, client, {}, {}) + + @pytest.mark.parametrize( ("units", "attributes", "unit"), [ @@ -5371,3 +5430,51 @@ def _fetch_states() -> list[State]: assert len(states) == 1 assert ATTR_OPTIONS not in states[0].attributes assert ATTR_FRIENDLY_NAME in states[0].attributes + + +async def test_clean_up_repairs( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test cleaning up repairs.""" + await async_setup_component(hass, "sensor", {}) + issue_registry = ir.async_get(hass) + client = await hass_ws_client() + + # Create some issues + def create_issue(domain: str, issue_id: str, data: dict | None) -> None: + ir.async_create_issue( + hass, + domain, + issue_id, + data=data, + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="", + ) + + create_issue("test", "test_issue", None) + create_issue(DOMAIN, "test_issue_1", None) + create_issue(DOMAIN, "test_issue_2", {"issue_type": "another_issue"}) + create_issue(DOMAIN, "test_issue_3", {"issue_type": "state_class_removed"}) + create_issue(DOMAIN, "test_issue_4", {"issue_type": "units_changed"}) + + # Check the issues + assert set(issue_registry.issues) == { + ("test", "test_issue"), + ("sensor", "test_issue_1"), + ("sensor", "test_issue_2"), + ("sensor", "test_issue_3"), + ("sensor", "test_issue_4"), + } + + # Request update of issues + await client.send_json_auto_id({"type": "recorder/update_statistics_issues"}) + response = await client.receive_json() + assert response["success"] + + # Check the issues + assert set(issue_registry.issues) == { + ("test", "test_issue"), + ("sensor", "test_issue_1"), + ("sensor", "test_issue_2"), + } diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index aefb99cf1b1059..80837c718a9e20 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -6,7 +6,7 @@ "manufacturer": "SMA", "name": "SMA Device Name", "type": "Sunny Boy 3.6", - "serial": "123456789", + "serial": 123456789, } MOCK_USER_INPUT = { diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index a54f478a31dc4e..dd47a0f10557f7 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -22,9 +22,10 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, title=MOCK_DEVICE["name"], - unique_id=MOCK_DEVICE["serial"], + unique_id=str(MOCK_DEVICE["serial"]), data=MOCK_USER_INPUT, source=config_entries.SOURCE_IMPORT, + minor_version=2, ) diff --git a/tests/components/sma/test_init.py b/tests/components/sma/test_init.py new file mode 100644 index 00000000000000..0cc82f49a41f8f --- /dev/null +++ b/tests/components/sma/test_init.py @@ -0,0 +1,27 @@ +"""Test the sma init file.""" + +from homeassistant.components.sma.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.core import HomeAssistant + +from . import MOCK_DEVICE, MOCK_USER_INPUT, _patch_async_setup_entry + +from tests.common import MockConfigEntry + + +async def test_migrate_entry_minor_version_1_2(hass: HomeAssistant) -> None: + """Test migrating a 1.1 config entry to 1.2.""" + with _patch_async_setup_entry(): + entry = MockConfigEntry( + domain=DOMAIN, + title=MOCK_DEVICE["name"], + unique_id=MOCK_DEVICE["serial"], # Not converted to str + data=MOCK_USER_INPUT, + source=SOURCE_IMPORT, + minor_version=1, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.unique_id == str(MOCK_DEVICE["serial"]) diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index 9f95e04a38fa09..38356a00de767e 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -1,55 +1,4 @@ # serializer version: 1 -# name: test_all_entities[sensor.inverter_1_consumption_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inverter_1_consumption_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption total', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_total', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-inverter_1-consumption_total', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.inverter_1_consumption_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Inverter 1 Consumption total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inverter_1_consumption_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '354.687', - }) -# --- # name: test_all_entities[sensor.inverter_1_consumption_year-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -85,7 +34,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-inverter_1-consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_1_consumption_year', 'unit_of_measurement': , }) # --- @@ -135,7 +84,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'current_power', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-inverter_1-current_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_1_current_power', 'unit_of_measurement': , }) # --- @@ -190,7 +139,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-inverter_2-consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_2_consumption_year', 'unit_of_measurement': , }) # --- @@ -240,7 +189,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'current_power', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-inverter_2-current_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_2_current_power', 'unit_of_measurement': , }) # --- @@ -291,7 +240,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'alternator_loss', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-alternator_loss', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_alternator_loss', 'unit_of_measurement': , }) # --- @@ -345,7 +294,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'capacity', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-capacity', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_capacity', 'unit_of_measurement': '%', }) # --- @@ -396,7 +345,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_ac', 'unit_of_measurement': , }) # --- @@ -451,7 +400,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_day', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_day', 'unit_of_measurement': , }) # --- @@ -505,7 +454,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_month', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_month', 'unit_of_measurement': , }) # --- @@ -561,7 +510,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_total', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_total', 'unit_of_measurement': , }) # --- @@ -616,7 +565,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_year', 'unit_of_measurement': , }) # --- @@ -670,7 +619,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'consumption_yesterday', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-consumption_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_yesterday', 'unit_of_measurement': , }) # --- @@ -723,7 +672,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'efficiency', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-efficiency', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_efficiency', 'unit_of_measurement': '%', }) # --- @@ -772,7 +721,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'total_power', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-total_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_total_power', 'unit_of_measurement': , }) # --- @@ -820,7 +769,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'last_update', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-last_updated', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_last_updated', 'unit_of_measurement': None, }) # --- @@ -869,7 +818,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'power_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-power_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_ac', 'unit_of_measurement': , }) # --- @@ -920,7 +869,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'power_available', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-power_available', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_available', 'unit_of_measurement': , }) # --- @@ -971,7 +920,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'power_dc', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-power_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_dc', 'unit_of_measurement': , }) # --- @@ -1022,7 +971,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'self_consumption_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-self_consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_self_consumption_year', 'unit_of_measurement': , }) # --- @@ -1076,7 +1025,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'usage', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-usage', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_usage', 'unit_of_measurement': '%', }) # --- @@ -1127,7 +1076,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-voltage_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_ac', 'unit_of_measurement': , }) # --- @@ -1178,7 +1127,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-voltage_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_dc', 'unit_of_measurement': , }) # --- @@ -1233,7 +1182,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'yield_day', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-yield_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_day', 'unit_of_measurement': , }) # --- @@ -1287,7 +1236,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'yield_month', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-yield_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_month', 'unit_of_measurement': , }) # --- @@ -1343,7 +1292,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'yield_total', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-yield_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_total', 'unit_of_measurement': , }) # --- @@ -1395,7 +1344,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'yield_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-yield_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_year', 'unit_of_measurement': , }) # --- @@ -1449,7 +1398,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'yield_yesterday', - 'unique_id': 'ce5f5431554d101905d31797e1232da8-yield_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_yesterday', 'unit_of_measurement': , }) # --- diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index 303074e3c2cb93..75d942fc601184 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -62,3 +62,58 @@ }), }) # --- +# name: test_diagnostics_missing_items[test_diagnostics_missing_items] + dict({ + 'coordinators': dict({ + 'data': dict({ + 'addresses': None, + 'boot_time': '2024-02-24 15:00:00+00:00', + 'cpu_percent': '10.0', + 'disk_usage': dict({ + '/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + }), + 'io_counters': None, + 'load': '(1, 2, 3)', + 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', + 'processes': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", + 'swap': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', + 'temperatures': dict({ + 'cpu0-thermal': "[shwtemp(label='cpu0-thermal', current=50.0, high=60.0, critical=70.0)]", + }), + }), + 'last_update_success': True, + }), + 'entry': dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'systemmonitor', + 'minor_version': 3, + 'options': dict({ + 'binary_sensor': dict({ + 'process': list([ + 'python3', + 'pip', + ]), + }), + 'resources': list([ + 'disk_use_percent_/', + 'disk_use_percent_/home/notexist/', + 'memory_free_', + 'network_out_eth0', + 'process_python3', + ]), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'System Monitor', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py index b0f4fca3d0c850..26e421e65745ad 100644 --- a/tests/components/systemmonitor/test_diagnostics.py +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -2,6 +2,7 @@ from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from syrupy.filters import props @@ -24,3 +25,26 @@ async def test_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, mock_added_config_entry ) == snapshot(exclude=props("last_update", "entry_id", "created_at", "modified_at")) + + +async def test_diagnostics_missing_items( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_psutil: Mock, + mock_os: Mock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: + """Test diagnostics.""" + mock_psutil.net_if_addrs.return_value = None + mock_psutil.net_io_counters.return_value = None + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot( + exclude=props("last_update", "entry_id", "created_at", "modified_at"), + name="test_diagnostics_missing_items", + ) diff --git a/tests/components/tesla_fleet/test_button.py b/tests/components/tesla_fleet/test_button.py index 8b83011e6f4f82..addba00b93defb 100644 --- a/tests/components/tesla_fleet/test_button.py +++ b/tests/components/tesla_fleet/test_button.py @@ -28,6 +28,13 @@ async def test_button( await setup_platform(hass, normal_config_entry, [Platform.BUTTON]) assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ["button.test_wake"]}, + blocking=True, + ) + @pytest.mark.parametrize( ("name", "func"), diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 36ce65b2c89f88..96cebc2b01f855 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -1751,13 +1751,19 @@ 'state': '0.074', }) # --- -# name: test_sensors[sensor.energy_site_load_power-entry] +# name: test_sensors[sensor.energy_site_island_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'options': list([ + 'on_grid', + 'off_grid', + 'off_grid_intentional', + 'off_grid_unintentional', + 'island_status_unknown', + ]), }), 'config_entry_id': , 'device_class': None, @@ -1765,7 +1771,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.energy_site_load_power', + 'entity_id': 'sensor.energy_site_island_status', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1774,69 +1780,75 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Load power', + 'original_name': 'Island status', 'platform': 'teslemetry', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'load_power', - 'unique_id': '123456-load_power', - 'unit_of_measurement': , + 'translation_key': 'island_status', + 'unique_id': '123456-island_status', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.energy_site_load_power-state] +# name: test_sensors[sensor.energy_site_island_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Energy Site Load power', - 'state_class': , - 'unit_of_measurement': , + 'device_class': 'enum', + 'friendly_name': 'Energy Site Island status', + 'options': list([ + 'on_grid', + 'off_grid', + 'off_grid_intentional', + 'off_grid_unintentional', + 'island_status_unknown', + ]), }), 'context': , - 'entity_id': 'sensor.energy_site_load_power', + 'entity_id': 'sensor.energy_site_island_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6.245', + 'state': 'on_grid', }) # --- -# name: test_sensors[sensor.energy_site_load_power-statealt] +# name: test_sensors[sensor.energy_site_island_status-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Energy Site Load power', - 'state_class': , - 'unit_of_measurement': , + 'device_class': 'enum', + 'friendly_name': 'Energy Site Island status', + 'options': list([ + 'on_grid', + 'off_grid', + 'off_grid_intentional', + 'off_grid_unintentional', + 'island_status_unknown', + ]), }), 'context': , - 'entity_id': 'sensor.energy_site_load_power', + 'entity_id': 'sensor.energy_site_island_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6.245', + 'state': 'on_grid', }) # --- -# name: test_sensors[sensor.energy_site_none-entry] +# name: test_sensors[sensor.energy_site_load_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.energy_site_none', + 'entity_id': 'sensor.energy_site_load_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1845,44 +1857,54 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': None, + 'original_name': 'Load power', 'platform': 'teslemetry', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'island_status', - 'unique_id': '123456-island_status', - 'unit_of_measurement': None, + 'translation_key': 'load_power', + 'unique_id': '123456-load_power', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.energy_site_none-state] +# name: test_sensors[sensor.energy_site_load_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Energy Site None', + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.energy_site_none', + 'entity_id': 'sensor.energy_site_load_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on_grid', + 'state': '6.245', }) # --- -# name: test_sensors[sensor.energy_site_none-statealt] +# name: test_sensors[sensor.energy_site_load_power-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Energy Site None', + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.energy_site_none', + 'entity_id': 'sensor.energy_site_load_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on_grid', + 'state': '6.245', }) # --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index ad863fc79ae725..8236f332046e11 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -42,7 +42,7 @@ # name: test_states[climate.thermostat-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_temperature': 20, + 'current_temperature': 20.2, 'friendly_name': 'thermostat', 'hvac_action': , 'hvac_modes': list([ @@ -52,7 +52,7 @@ 'max_temp': 65536, 'min_temp': None, 'supported_features': , - 'temperature': 22, + 'temperature': 22.2, }), 'context': , 'entity_id': 'climate.thermostat', diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py index 2f24fa829f9f74..3a54048e1d64e9 100644 --- a/tests/components/tplink/test_climate.py +++ b/tests/components/tplink/test_climate.py @@ -45,11 +45,11 @@ async def mocked_hub(hass: HomeAssistant) -> Device: features = [ _mocked_feature( - "temperature", value=20, category=Feature.Category.Primary, unit="celsius" + "temperature", value=20.2, category=Feature.Category.Primary, unit="celsius" ), _mocked_feature( "target_temperature", - value=22, + value=22.2, type_=Feature.Type.Number, category=Feature.Category.Primary, unit="celsius", @@ -94,8 +94,8 @@ async def test_climate( state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_HVAC_ACTION] is HVACAction.HEATING - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20 - assert state.attributes[ATTR_TEMPERATURE] == 22 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.2 + assert state.attributes[ATTR_TEMPERATURE] == 22.2 async def test_states( diff --git a/tests/components/vicare/test_types.py b/tests/components/vicare/test_types.py index 13d8255cf8d9ba..c411213f13e848 100644 --- a/tests/components/vicare/test_types.py +++ b/tests/components/vicare/test_types.py @@ -39,7 +39,7 @@ async def test_ha_preset_to_heating_program( ha_preset: str | None, expected_result: str | None, ) -> None: - """Testing HA Preset tp ViCare HeatingProgram.""" + """Testing HA Preset to ViCare HeatingProgram.""" supported_programs = [ HeatingProgram.COMFORT, @@ -52,6 +52,17 @@ async def test_ha_preset_to_heating_program( ) +async def test_ha_preset_to_heating_program_error() -> None: + """Testing HA Preset to ViCare HeatingProgram.""" + + supported_programs = [ + "test", + ] + assert ( + HeatingProgram.from_ha_preset(HeatingProgram.NORMAL, supported_programs) is None + ) + + @pytest.mark.parametrize( ("vicare_mode", "expected_result"), [ diff --git a/tests/components/wmspro/snapshots/test_diagnostics.ambr b/tests/components/wmspro/snapshots/test_diagnostics.ambr index 6a87c0416ab0a2..00cb62e18c44b5 100644 --- a/tests/components/wmspro/snapshots/test_diagnostics.ambr +++ b/tests/components/wmspro/snapshots/test_diagnostics.ambr @@ -149,6 +149,8 @@ }), 'status': dict({ }), + 'unknownProducts': dict({ + }), }), '97358': dict({ 'actions': dict({ @@ -203,6 +205,8 @@ }), 'status': dict({ }), + 'unknownProducts': dict({ + }), }), }), 'host': 'webcontrol', diff --git a/tests/components/zwave_js/test_config_validation.py b/tests/components/zwave_js/test_config_validation.py index 8428972bde1963..cebbde3c9b13f0 100644 --- a/tests/components/zwave_js/test_config_validation.py +++ b/tests/components/zwave_js/test_config_validation.py @@ -1,27 +1,31 @@ """Test the Z-Wave JS config validation helpers.""" +from typing import Any + import pytest import voluptuous as vol -from homeassistant.components.zwave_js.config_validation import boolean +from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA, boolean -def test_boolean_validation() -> None: - """Test boolean config validator.""" - # test bool - assert boolean(True) - assert not boolean(False) - # test strings - assert boolean("TRUE") - assert not boolean("FALSE") - assert boolean("ON") - assert not boolean("NO") - # ensure 1's and 0's don't get converted to bool - with pytest.raises(vol.Invalid): - boolean("1") - with pytest.raises(vol.Invalid): - boolean("0") - with pytest.raises(vol.Invalid): - boolean(1) +@pytest.mark.parametrize( + ("test_cases", "expected_value"), + [ + ([True, "true", "yes", "on", "ON", "enable"], True), + ([False, "false", "no", "off", "NO", "disable"], False), + ([1.1, "1.1"], 1.1), + ([1.0, "1.0"], 1.0), + ([1, "1"], 1), + ], +) +def test_validation(test_cases: list[Any], expected_value: Any) -> None: + """Test config validation.""" + for case in test_cases: + assert VALUE_SCHEMA(case) == expected_value + + +@pytest.mark.parametrize("value", ["invalid", "1", "0", 1, 0]) +def test_invalid_boolean_validation(value: str | int) -> None: + """Test invalid cases for boolean config validator.""" with pytest.raises(vol.Invalid): - boolean(0) + boolean(value) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 339b372f137356..9a594408465c4c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -6564,3 +6564,21 @@ def test_warn_no_hass(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> template.Template("blah", hass) assert message not in caplog.text caplog.clear() + + +async def test_merge_response_not_mutate_original_object( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test the merge_response does not mutate original service response value.""" + + value = '{"calendar.family": {"events": [{"summary": "An event"}]}' + _template = ( + "{% set calendar_response = " + value + "} %}" + "{{ merge_response(calendar_response) }}" + # We should be able to merge the same response again + # as the merge is working on a copy of the original object (response) + "{{ merge_response(calendar_response) }}" + ) + + tpl = template.Template(_template, hass) + assert tpl.async_render()