diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index cc100c48fd8..7c08df39000 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: "3.13" + DEFAULT_PYTHON: "3.12" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 48e37717232..2c80c32245c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.27.3 + uses: github/codeql-action/init@v3.27.1 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.27.3 + uses: github/codeql-action/analyze@v3.27.1 with: category: "/language:python" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56fbabe8087..519674b9894 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,7 +90,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$ + files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$ - id: hassfest-mypy-config name: hassfest-mypy-config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config diff --git a/CODEOWNERS b/CODEOWNERS index e204463695e..022eda00123 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,8 +40,6 @@ build.json @home-assistant/supervisor # Integrations /homeassistant/components/abode/ @shred86 /tests/components/abode/ @shred86 -/homeassistant/components/acaia/ @zweckj -/tests/components/acaia/ @zweckj /homeassistant/components/accuweather/ @bieniu /tests/components/accuweather/ @bieniu /homeassistant/components/acmeda/ @atmurray @@ -1346,8 +1344,6 @@ build.json @home-assistant/supervisor /tests/components/siren/ @home-assistant/core @raman325 /homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sky_hub/ @rogerselwyn -/homeassistant/components/sky_remote/ @dunnmj @saty9 -/tests/components/sky_remote/ @dunnmj @saty9 /homeassistant/components/skybell/ @tkdrob /tests/components/skybell/ @tkdrob /homeassistant/components/slack/ @tkdrob @fletcherau @@ -1489,8 +1485,8 @@ build.json @home-assistant/supervisor /tests/components/tedee/ @patrickhilker @zweckj /homeassistant/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike -/homeassistant/components/template/ @PhracturedBlue @home-assistant/core -/tests/components/template/ @PhracturedBlue @home-assistant/core +/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core +/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core /homeassistant/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_wall_connector/ @einarhauks diff --git a/build.yaml b/build.yaml index a8755bbbf5c..13618740ab8 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 1034223051c..dcfb6685627 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -515,7 +515,7 @@ async def async_from_config_dict( issue_registry.async_create_issue( hass, core.DOMAIN, - f"python_version_{required_python_version}", + "python_version", is_fixable=False, severity=issue_registry.IssueSeverity.WARNING, breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE, diff --git a/homeassistant/brands/sky.json b/homeassistant/brands/sky.json deleted file mode 100644 index 3ab0cbbe5bd..00000000000 --- a/homeassistant/brands/sky.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "domain": "sky", - "name": "Sky", - "integrations": ["sky_hub", "sky_remote"] -} diff --git a/homeassistant/components/acaia/__init__.py b/homeassistant/components/acaia/__init__.py deleted file mode 100644 index dfdb4cb935d..00000000000 --- a/homeassistant/components/acaia/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Initialize the Acaia component.""" - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant - -from .coordinator import AcaiaConfigEntry, AcaiaCoordinator - -PLATFORMS = [ - Platform.BUTTON, -] - - -async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool: - """Set up acaia as config entry.""" - - coordinator = AcaiaCoordinator(hass, entry) - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool: - """Unload a config entry.""" - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/acaia/button.py b/homeassistant/components/acaia/button.py deleted file mode 100644 index 50671eecbba..00000000000 --- a/homeassistant/components/acaia/button.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Button entities for Acaia scales.""" - -from collections.abc import Callable, Coroutine -from dataclasses import dataclass -from typing import Any - -from aioacaia.acaiascale import AcaiaScale - -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .coordinator import AcaiaConfigEntry -from .entity import AcaiaEntity - - -@dataclass(kw_only=True, frozen=True) -class AcaiaButtonEntityDescription(ButtonEntityDescription): - """Description for acaia button entities.""" - - press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]] - - -BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = ( - AcaiaButtonEntityDescription( - key="tare", - translation_key="tare", - press_fn=lambda scale: scale.tare(), - ), - AcaiaButtonEntityDescription( - key="reset_timer", - translation_key="reset_timer", - press_fn=lambda scale: scale.reset_timer(), - ), - AcaiaButtonEntityDescription( - key="start_stop", - translation_key="start_stop", - press_fn=lambda scale: scale.start_stop_timer(), - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: AcaiaConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up button entities and services.""" - - coordinator = entry.runtime_data - async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS) - - -class AcaiaButton(AcaiaEntity, ButtonEntity): - """Representation of an Acaia button.""" - - entity_description: AcaiaButtonEntityDescription - - async def async_press(self) -> None: - """Handle the button press.""" - await self.entity_description.press_fn(self._scale) diff --git a/homeassistant/components/acaia/config_flow.py b/homeassistant/components/acaia/config_flow.py deleted file mode 100644 index 36727059c8a..00000000000 --- a/homeassistant/components/acaia/config_flow.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Config flow for Acaia integration.""" - -import logging -from typing import Any - -from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice -from aioacaia.helpers import is_new_scale -import voluptuous as vol - -from homeassistant.components.bluetooth import ( - BluetoothServiceInfoBleak, - async_discovered_service_info, -) -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_ADDRESS, CONF_NAME -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.selector import ( - SelectOptionDict, - SelectSelector, - SelectSelectorConfig, - SelectSelectorMode, -) - -from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for acaia.""" - - def __init__(self) -> None: - """Initialize the config flow.""" - self._discovered: dict[str, Any] = {} - self._discovered_devices: dict[str, str] = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle a flow initialized by the user.""" - - errors: dict[str, str] = {} - - if user_input is not None: - mac = format_mac(user_input[CONF_ADDRESS]) - try: - is_new_style_scale = await is_new_scale(mac) - except AcaiaDeviceNotFound: - errors["base"] = "device_not_found" - except AcaiaError: - _LOGGER.exception("Error occurred while connecting to the scale") - errors["base"] = "unknown" - except AcaiaUnknownDevice: - return self.async_abort(reason="unsupported_device") - else: - await self.async_set_unique_id(mac) - self._abort_if_unique_id_configured() - - if not errors: - return self.async_create_entry( - title=self._discovered_devices[user_input[CONF_ADDRESS]], - data={ - CONF_ADDRESS: mac, - CONF_IS_NEW_STYLE_SCALE: is_new_style_scale, - }, - ) - - for device in async_discovered_service_info(self.hass): - self._discovered_devices[device.address] = device.name - - if not self._discovered_devices: - return self.async_abort(reason="no_devices_found") - - options = [ - SelectOptionDict( - value=device_mac, - label=f"{device_name} ({device_mac})", - ) - for device_mac, device_name in self._discovered_devices.items() - ] - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_ADDRESS): SelectSelector( - SelectSelectorConfig( - options=options, - mode=SelectSelectorMode.DROPDOWN, - ) - ) - } - ), - errors=errors, - ) - - async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfoBleak - ) -> ConfigFlowResult: - """Handle a discovered Bluetooth device.""" - - self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address) - self._discovered[CONF_NAME] = discovery_info.name - - await self.async_set_unique_id(mac) - self._abort_if_unique_id_configured() - - try: - self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale( - discovery_info.address - ) - except AcaiaDeviceNotFound: - _LOGGER.debug("Device not found during discovery") - return self.async_abort(reason="device_not_found") - except AcaiaError: - _LOGGER.debug( - "Error occurred while connecting to the scale during discovery", - exc_info=True, - ) - return self.async_abort(reason="unknown") - except AcaiaUnknownDevice: - _LOGGER.debug("Unsupported device during discovery") - return self.async_abort(reason="unsupported_device") - - return await self.async_step_bluetooth_confirm() - - async def async_step_bluetooth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle confirmation of Bluetooth discovery.""" - - if user_input is not None: - return self.async_create_entry( - title=self._discovered[CONF_NAME], - data={ - CONF_ADDRESS: self._discovered[CONF_ADDRESS], - CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE], - }, - ) - - self.context["title_placeholders"] = placeholders = { - CONF_NAME: self._discovered[CONF_NAME] - } - - self._set_confirm_only() - return self.async_show_form( - step_id="bluetooth_confirm", - description_placeholders=placeholders, - ) diff --git a/homeassistant/components/acaia/const.py b/homeassistant/components/acaia/const.py deleted file mode 100644 index c603578763d..00000000000 --- a/homeassistant/components/acaia/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants for component.""" - -DOMAIN = "acaia" -CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale" diff --git a/homeassistant/components/acaia/coordinator.py b/homeassistant/components/acaia/coordinator.py deleted file mode 100644 index bd915b42408..00000000000 --- a/homeassistant/components/acaia/coordinator.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Coordinator for Acaia integration.""" - -from __future__ import annotations - -from datetime import timedelta -import logging - -from aioacaia.acaiascale import AcaiaScale -from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import CONF_IS_NEW_STYLE_SCALE - -SCAN_INTERVAL = timedelta(seconds=15) - -_LOGGER = logging.getLogger(__name__) - -type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator] - - -class AcaiaCoordinator(DataUpdateCoordinator[None]): - """Class to handle fetching data from the scale.""" - - config_entry: AcaiaConfigEntry - - def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None: - """Initialize coordinator.""" - super().__init__( - hass, - _LOGGER, - name="acaia coordinator", - update_interval=SCAN_INTERVAL, - config_entry=entry, - ) - - self._scale = AcaiaScale( - address_or_ble_device=entry.data[CONF_ADDRESS], - name=entry.title, - is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE], - notify_callback=self.async_update_listeners, - ) - - @property - def scale(self) -> AcaiaScale: - """Return the scale object.""" - return self._scale - - async def _async_update_data(self) -> None: - """Fetch data.""" - - # scale is already connected, return - if self._scale.connected: - return - - # scale is not connected, try to connect - try: - await self._scale.connect(setup_tasks=False) - except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex: - _LOGGER.debug( - "Could not connect to scale: %s, Error: %s", - self.config_entry.data[CONF_ADDRESS], - ex, - ) - self._scale.device_disconnected_handler(notify=False) - return - - # connected, set up background tasks - if not self._scale.heartbeat_task or self._scale.heartbeat_task.done(): - self._scale.heartbeat_task = self.config_entry.async_create_background_task( - hass=self.hass, - target=self._scale.send_heartbeats(), - name="acaia_heartbeat_task", - ) - - if not self._scale.process_queue_task or self._scale.process_queue_task.done(): - self._scale.process_queue_task = ( - self.config_entry.async_create_background_task( - hass=self.hass, - target=self._scale.process_queue(), - name="acaia_process_queue_task", - ) - ) diff --git a/homeassistant/components/acaia/entity.py b/homeassistant/components/acaia/entity.py deleted file mode 100644 index 8a2108d2687..00000000000 --- a/homeassistant/components/acaia/entity.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Base class for Acaia entities.""" - -from dataclasses import dataclass - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import AcaiaCoordinator - - -@dataclass -class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]): - """Common elements for all entities.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: AcaiaCoordinator, - entity_description: EntityDescription, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self.entity_description = entity_description - self._scale = coordinator.scale - self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}" - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._scale.mac)}, - manufacturer="Acaia", - model=self._scale.model, - suggested_area="Kitchen", - ) - - @property - def available(self) -> bool: - """Returns whether entity is available.""" - return super().available and self._scale.connected diff --git a/homeassistant/components/acaia/icons.json b/homeassistant/components/acaia/icons.json deleted file mode 100644 index aeab07ee912..00000000000 --- a/homeassistant/components/acaia/icons.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "entity": { - "button": { - "tare": { - "default": "mdi:scale-balance" - }, - "reset_timer": { - "default": "mdi:timer-refresh" - }, - "start_stop": { - "default": "mdi:timer-play" - } - } - } -} diff --git a/homeassistant/components/acaia/manifest.json b/homeassistant/components/acaia/manifest.json deleted file mode 100644 index c907a70a38e..00000000000 --- a/homeassistant/components/acaia/manifest.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "domain": "acaia", - "name": "Acaia", - "bluetooth": [ - { - "manufacturer_id": 16962 - }, - { - "local_name": "ACAIA*" - }, - { - "local_name": "PYXIS-*" - }, - { - "local_name": "LUNAR-*" - }, - { - "local_name": "PROCHBT001" - } - ], - "codeowners": ["@zweckj"], - "config_flow": true, - "dependencies": ["bluetooth_adapters"], - "documentation": "https://www.home-assistant.io/integrations/acaia", - "integration_type": "device", - "iot_class": "local_push", - "loggers": ["aioacaia"], - "requirements": ["aioacaia==0.1.6"] -} diff --git a/homeassistant/components/acaia/strings.json b/homeassistant/components/acaia/strings.json deleted file mode 100644 index f6a1aeb66fd..00000000000 --- a/homeassistant/components/acaia/strings.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "config": { - "flow_title": "{name}", - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "unsupported_device": "This device is not supported." - }, - "error": { - "device_not_found": "Device could not be found.", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "bluetooth_confirm": { - "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" - }, - "user": { - "description": "[%key:component::bluetooth::config::step::user::description%]", - "data": { - "address": "[%key:common::config_flow::data::device%]" - } - } - } - }, - "entity": { - "button": { - "tare": { - "name": "Tare" - }, - "reset_timer": { - "name": "Reset timer" - }, - "start_stop": { - "name": "Start/stop timer" - } - } - } -} diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 6bf374087a6..10fb20bb2ce 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.6"] + "requirements": ["aioairzone==0.9.5"] } diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index a9e433a3650..2946fc64941 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -6,7 +6,7 @@ import asyncio from datetime import timedelta from functools import partial import logging -from typing import TYPE_CHECKING, Any, Final, final +from typing import Any, Final, final from propcache import cached_property import voluptuous as vol @@ -221,15 +221,9 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A @property def state(self) -> str | None: """Return the current state.""" - if (alarm_state := self.alarm_state) is not None: - return alarm_state - if self._attr_state is not None: - # Backwards compatibility for integrations that set state directly - # Should be removed in 2025.11 - if TYPE_CHECKING: - assert isinstance(self._attr_state, str) - return self._attr_state - return None + if (alarm_state := self.alarm_state) is None: + return None + return alarm_state @cached_property def alarm_state(self) -> AlarmControlPanelState | None: diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 907fda4c7f8..200cb4a3f65 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -32,9 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_handle_create_service(call: ServiceCall) -> None: """Service handler for creating backups.""" - await backup_manager.async_create_backup(on_progress=None) - if backup_task := backup_manager.backup_task: - await backup_task + await backup_manager.async_create_backup() hass.services.async_register(DOMAIN, "create", async_handle_create_service) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index ddc0a1eac3f..4300f75eed0 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -4,7 +4,6 @@ from __future__ import annotations import abc import asyncio -from collections.abc import Callable from dataclasses import asdict, dataclass import hashlib import io @@ -35,13 +34,6 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER BUF_SIZE = 2**20 * 4 # 4MB -@dataclass(slots=True) -class NewBackup: - """New backup class.""" - - slug: str - - @dataclass(slots=True) class Backup: """Backup class.""" @@ -57,15 +49,6 @@ class Backup: return {**asdict(self), "path": self.path.as_posix()} -@dataclass(slots=True) -class BackupProgress: - """Backup progress class.""" - - done: bool - stage: str | None - success: bool | None - - class BackupPlatformProtocol(Protocol): """Define the format that backup platforms can have.""" @@ -82,7 +65,7 @@ class BaseBackupManager(abc.ABC): def __init__(self, hass: HomeAssistant) -> None: """Initialize the backup manager.""" self.hass = hass - self.backup_task: asyncio.Task | None = None + self.backing_up = False self.backups: dict[str, Backup] = {} self.loaded_platforms = False self.platforms: dict[str, BackupPlatformProtocol] = {} @@ -150,12 +133,7 @@ class BaseBackupManager(abc.ABC): """Restore a backup.""" @abc.abstractmethod - async def async_create_backup( - self, - *, - on_progress: Callable[[BackupProgress], None] | None, - **kwargs: Any, - ) -> NewBackup: + async def async_create_backup(self, **kwargs: Any) -> Backup: """Generate a backup.""" @abc.abstractmethod @@ -314,36 +292,17 @@ class BackupManager(BaseBackupManager): await self.hass.async_add_executor_job(_move_and_cleanup) await self.load_backups() - async def async_create_backup( - self, - *, - on_progress: Callable[[BackupProgress], None] | None, - **kwargs: Any, - ) -> NewBackup: + async def async_create_backup(self, **kwargs: Any) -> Backup: """Generate a backup.""" - if self.backup_task: + if self.backing_up: raise HomeAssistantError("Backup already in progress") - backup_name = f"Core {HAVERSION}" - date_str = dt_util.now().isoformat() - slug = _generate_slug(date_str, backup_name) - self.backup_task = self.hass.async_create_task( - self._async_create_backup(backup_name, date_str, slug, on_progress), - name="backup_manager_create_backup", - eager_start=False, # To ensure the task is not started before we return - ) - return NewBackup(slug=slug) - async def _async_create_backup( - self, - backup_name: str, - date_str: str, - slug: str, - on_progress: Callable[[BackupProgress], None] | None, - ) -> Backup: - """Generate a backup.""" - success = False try: + self.backing_up = True await self.async_pre_backup_actions() + backup_name = f"Core {HAVERSION}" + date_str = dt_util.now().isoformat() + slug = _generate_slug(date_str, backup_name) backup_data = { "slug": slug, @@ -370,12 +329,9 @@ class BackupManager(BaseBackupManager): if self.loaded_backups: self.backups[slug] = backup LOGGER.debug("Generated new backup with slug %s", slug) - success = True return backup finally: - if on_progress: - on_progress(BackupProgress(done=True, stage=None, success=success)) - self.backup_task = None + self.backing_up = False await self.async_post_backup_actions() def _mkdir_and_generate_backup_contents( diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index a7c61b7c66c..3ac8a7ace3e 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -8,7 +8,6 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from .const import DATA_MANAGER, LOGGER -from .manager import BackupProgress @callback @@ -41,7 +40,7 @@ async def handle_info( msg["id"], { "backups": list(backups.values()), - "backing_up": manager.backup_task is not None, + "backing_up": manager.backing_up, }, ) @@ -114,11 +113,7 @@ async def handle_create( msg: dict[str, Any], ) -> None: """Generate a backup.""" - - def on_progress(progress: BackupProgress) -> None: - connection.send_message(websocket_api.event_message(msg["id"], progress)) - - backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress) + backup = await hass.data[DATA_MANAGER].async_create_backup() connection.send_result(msg["id"], backup) @@ -132,6 +127,7 @@ async def handle_backup_start( ) -> None: """Backup start notification.""" manager = hass.data[DATA_MANAGER] + manager.backing_up = True LOGGER.debug("Backup start notification") try: @@ -153,6 +149,7 @@ async def handle_backup_end( ) -> None: """Backup end notification.""" manager = hass.data[DATA_MANAGER] + manager.backing_up = False LOGGER.debug("Backup end notification") try: diff --git a/homeassistant/components/cambridge_audio/manifest.json b/homeassistant/components/cambridge_audio/manifest.json index c359ca14a21..edacd17f54d 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.8.5"], + "requirements": ["aiostreammagic==2.8.4"], "zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."] } diff --git a/homeassistant/components/cambridge_audio/select.py b/homeassistant/components/cambridge_audio/select.py index c99abc853e5..ca6eebdec6b 100644 --- a/homeassistant/components/cambridge_audio/select.py +++ b/homeassistant/components/cambridge_audio/select.py @@ -51,13 +51,8 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = ( CambridgeAudioSelectEntityDescription( key="display_brightness", translation_key="display_brightness", - options=[ - DisplayBrightness.BRIGHT.value, - DisplayBrightness.DIM.value, - DisplayBrightness.OFF.value, - ], + options=[x.value for x in DisplayBrightness], entity_category=EntityCategory.CONFIG, - load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE, value_fn=lambda client: client.display.brightness, set_value_fn=lambda client, value: client.set_display_brightness( DisplayBrightness(value) diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index d627a888169..0612c96e40c 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod import asyncio from collections.abc import Awaitable, Callable, Iterable from dataclasses import asdict, dataclass, field -from functools import cache, partial, wraps +from functools import cache, partial import logging from typing import TYPE_CHECKING, Any, Protocol @@ -205,49 +205,6 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None: ) -type WsCommandWithCamera = Callable[ - [websocket_api.ActiveConnection, dict[str, Any], Camera], - Awaitable[None], -] - - -def require_webrtc_support( - error_code: str, -) -> Callable[[WsCommandWithCamera], websocket_api.AsyncWebSocketCommandHandler]: - """Validate that the camera supports WebRTC.""" - - def decorate( - func: WsCommandWithCamera, - ) -> websocket_api.AsyncWebSocketCommandHandler: - """Decorate func.""" - - @wraps(func) - async def validate( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], - ) -> None: - """Validate that the camera supports WebRTC.""" - entity_id = msg["entity_id"] - camera = get_camera_from_entity_id(hass, entity_id) - if camera.frontend_stream_type != StreamType.WEB_RTC: - connection.send_error( - msg["id"], - error_code, - ( - "Camera does not support WebRTC," - f" frontend_stream_type={camera.frontend_stream_type}" - ), - ) - return - - await func(connection, msg, camera) - - return validate - - return decorate - - @websocket_api.websocket_command( { vol.Required("type"): "camera/webrtc/offer", @@ -256,9 +213,8 @@ def require_webrtc_support( } ) @websocket_api.async_response -@require_webrtc_support("webrtc_offer_failed") async def ws_webrtc_offer( - connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle the signal path for a WebRTC stream. @@ -270,7 +226,20 @@ async def ws_webrtc_offer( Async friendly. """ + entity_id = msg["entity_id"] offer = msg["offer"] + camera = get_camera_from_entity_id(hass, entity_id) + if camera.frontend_stream_type != StreamType.WEB_RTC: + connection.send_error( + msg["id"], + "webrtc_offer_failed", + ( + "Camera does not support WebRTC," + f" frontend_stream_type={camera.frontend_stream_type}" + ), + ) + return + session_id = ulid() connection.subscriptions[msg["id"]] = partial( camera.close_webrtc_session, session_id @@ -309,11 +278,23 @@ async def ws_webrtc_offer( } ) @websocket_api.async_response -@require_webrtc_support("webrtc_get_client_config_failed") async def ws_get_client_config( - connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle get WebRTC client config websocket command.""" + entity_id = msg["entity_id"] + camera = get_camera_from_entity_id(hass, entity_id) + if camera.frontend_stream_type != StreamType.WEB_RTC: + connection.send_error( + msg["id"], + "webrtc_get_client_config_failed", + ( + "Camera does not support WebRTC," + f" frontend_stream_type={camera.frontend_stream_type}" + ), + ) + return + config = camera.async_get_webrtc_client_configuration().to_frontend_dict() connection.send_result( msg["id"], @@ -330,11 +311,23 @@ async def ws_get_client_config( } ) @websocket_api.async_response -@require_webrtc_support("webrtc_candidate_failed") async def ws_candidate( - connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle WebRTC candidate websocket command.""" + entity_id = msg["entity_id"] + camera = get_camera_from_entity_id(hass, entity_id) + if camera.frontend_stream_type != StreamType.WEB_RTC: + connection.send_error( + msg["id"], + "webrtc_candidate_failed", + ( + "Camera does not support WebRTC," + f" frontend_stream_type={camera.frontend_stream_type}" + ), + ) + return + await camera.async_on_webrtc_candidate( msg["session_id"], RTCIceCandidate(msg["candidate"]) ) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 4838d19537a..a7110c35795 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -16,11 +16,11 @@ from hassil.expression import Expression, ListReference, Sequence from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList from hassil.recognize import ( MISSING_ENTITY, + MatchEntity, RecognizeResult, + UnmatchedTextEntity, recognize_all, - recognize_best, ) -from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.util import merge_dict from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml @@ -499,7 +499,6 @@ class DefaultAgent(ConversationEntity): maybe_result: RecognizeResult | None = None best_num_matched_entities = 0 best_num_unmatched_entities = 0 - best_num_unmatched_ranges = 0 for result in recognize_all( user_input.text, lang_intents.intents, @@ -518,14 +517,10 @@ class DefaultAgent(ConversationEntity): num_matched_entities += 1 num_unmatched_entities = 0 - num_unmatched_ranges = 0 for unmatched_entity in result.unmatched_entities_list: if isinstance(unmatched_entity, UnmatchedTextEntity): if unmatched_entity.text != MISSING_ENTITY: num_unmatched_entities += 1 - elif isinstance(unmatched_entity, UnmatchedRangeEntity): - num_unmatched_ranges += 1 - num_unmatched_entities += 1 else: num_unmatched_entities += 1 @@ -537,24 +532,15 @@ class DefaultAgent(ConversationEntity): (num_matched_entities == best_num_matched_entities) and (num_unmatched_entities < best_num_unmatched_entities) ) - or ( - # Prefer unmatched ranges - (num_matched_entities == best_num_matched_entities) - and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges > best_num_unmatched_ranges) - ) or ( # More literal text matched (num_matched_entities == best_num_matched_entities) and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges == best_num_unmatched_ranges) and (result.text_chunks_matched > maybe_result.text_chunks_matched) ) or ( # Prefer match failures with entities (result.text_chunks_matched == maybe_result.text_chunks_matched) - and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges == best_num_unmatched_ranges) and ( ("name" in result.entities) or ("name" in result.unmatched_entities) @@ -564,7 +550,6 @@ class DefaultAgent(ConversationEntity): maybe_result = result best_num_matched_entities = num_matched_entities best_num_unmatched_entities = num_unmatched_entities - best_num_unmatched_ranges = num_unmatched_ranges return maybe_result @@ -577,15 +562,76 @@ class DefaultAgent(ConversationEntity): language: str, ) -> RecognizeResult | None: """Search intents for a strict match to user input.""" - return recognize_best( + custom_found = False + name_found = False + best_results: list[RecognizeResult] = [] + best_name_quality: int | None = None + best_text_chunks_matched: int | None = None + for result in recognize_all( user_input.text, lang_intents.intents, slot_lists=slot_lists, intent_context=intent_context, language=language, - best_metadata_key=METADATA_CUSTOM_SENTENCE, - best_slot_name="name", - ) + ): + # Prioritize user intents + is_custom = ( + result.intent_metadata is not None + and result.intent_metadata.get(METADATA_CUSTOM_SENTENCE) + ) + + if custom_found and not is_custom: + continue + + if not custom_found and is_custom: + custom_found = True + # Clear builtin results + name_found = False + best_results = [] + best_name_quality = None + best_text_chunks_matched = None + + # Prioritize results with a "name" slot + name = result.entities.get("name") + is_name = name and not name.is_wildcard + + if name_found and not is_name: + continue + + if not name_found and is_name: + name_found = True + # Clear non-name results + best_results = [] + best_text_chunks_matched = None + + if is_name: + # Prioritize results with a better "name" slot + name_quality = len(cast(MatchEntity, name).value.split()) + if (best_name_quality is None) or (name_quality > best_name_quality): + best_name_quality = name_quality + # Clear worse name results + best_results = [] + best_text_chunks_matched = None + elif name_quality < best_name_quality: + continue + + # Prioritize results with more literal text + # This causes wildcards to match last. + if (best_text_chunks_matched is None) or ( + result.text_chunks_matched > best_text_chunks_matched + ): + best_results = [result] + best_text_chunks_matched = result.text_chunks_matched + elif result.text_chunks_matched == best_text_chunks_matched: + # Accumulate results with the same number of literal text matched. + # We will resolve the ambiguity below. + best_results.append(result) + + if best_results: + # Successful strict match + return best_results[0] + + return None async def _build_speech( self, diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index 5e5800ad6f1..df1ffc7f74f 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -6,8 +6,12 @@ from collections.abc import Iterable from typing import Any from aiohttp import web -from hassil.recognize import MISSING_ENTITY, RecognizeResult -from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity +from hassil.recognize import ( + MISSING_ENTITY, + RecognizeResult, + UnmatchedRangeEntity, + UnmatchedTextEntity, +) import voluptuous as vol from homeassistant.components import http, websocket_api diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1676cdf8254..8b5c6ef173f 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.0.1", "home-assistant-intents==2024.11.13"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"] } diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index a4f64ffbad9..ec7ecc76da0 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -4,8 +4,7 @@ from __future__ import annotations from typing import Any -from hassil.recognize import RecognizeResult -from hassil.util import PUNCTUATION_ALL +from hassil.recognize import PUNCTUATION, RecognizeResult import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -21,7 +20,7 @@ from .const import DATA_DEFAULT_ENTITY, DOMAIN def has_no_punctuation(value: list[str]) -> list[str]: """Validate result does not contain punctuation.""" for sentence in value: - if PUNCTUATION_ALL.search(sentence): + if PUNCTUATION.search(sentence): raise vol.Invalid("sentence should not contain punctuation") return value diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py index 84b27161edd..86c555ec151 100644 --- a/homeassistant/components/eq3btsmart/__init__.py +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -21,7 +21,6 @@ from .models import Eq3Config, Eq3ConfigEntryData PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, - Platform.NUMBER, Platform.SWITCH, ] diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py index 78292940e60..64bc1cf497c 100644 --- a/homeassistant/components/eq3btsmart/const.py +++ b/homeassistant/components/eq3btsmart/const.py @@ -24,11 +24,6 @@ ENTITY_KEY_WINDOW = "window" ENTITY_KEY_LOCK = "lock" ENTITY_KEY_BOOST = "boost" ENTITY_KEY_AWAY = "away" -ENTITY_KEY_COMFORT = "comfort" -ENTITY_KEY_ECO = "eco" -ENTITY_KEY_OFFSET = "offset" -ENTITY_KEY_WINDOW_OPEN_TEMPERATURE = "window_open_temperature" -ENTITY_KEY_WINDOW_OPEN_TIMEOUT = "window_open_timeout" GET_DEVICE_TIMEOUT = 5 # seconds @@ -82,5 +77,3 @@ DEFAULT_SCAN_INTERVAL = 10 # seconds SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected" SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected" - -EQ3BT_STEP = 0.5 diff --git a/homeassistant/components/eq3btsmart/icons.json b/homeassistant/components/eq3btsmart/icons.json index e6eb7532f37..fb0862f14bc 100644 --- a/homeassistant/components/eq3btsmart/icons.json +++ b/homeassistant/components/eq3btsmart/icons.json @@ -8,23 +8,6 @@ } } }, - "number": { - "comfort": { - "default": "mdi:sun-thermometer" - }, - "eco": { - "default": "mdi:snowflake-thermometer" - }, - "offset": { - "default": "mdi:thermometer-plus" - }, - "window_open_temperature": { - "default": "mdi:window-open-variant" - }, - "window_open_timeout": { - "default": "mdi:timer-refresh" - } - }, "switch": { "away": { "default": "mdi:home-account", diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index b30f806bf63..bd3f14939ca 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "loggers": ["eq3btsmart"], "quality_scale": "silver", - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"] + "requirements": ["eq3btsmart==1.2.1", "bleak-esphome==1.1.0"] } diff --git a/homeassistant/components/eq3btsmart/models.py b/homeassistant/components/eq3btsmart/models.py index 858465effa8..8ea0955dbdd 100644 --- a/homeassistant/components/eq3btsmart/models.py +++ b/homeassistant/components/eq3btsmart/models.py @@ -2,6 +2,7 @@ from dataclasses import dataclass +from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP from eq3btsmart.thermostat import Thermostat from .const import ( @@ -22,6 +23,8 @@ class Eq3Config: target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR external_temp_sensor: str = "" scan_interval: int = DEFAULT_SCAN_INTERVAL + default_away_hours: float = DEFAULT_AWAY_HOURS + default_away_temperature: float = DEFAULT_AWAY_TEMP @dataclass(slots=True) diff --git a/homeassistant/components/eq3btsmart/number.py b/homeassistant/components/eq3btsmart/number.py deleted file mode 100644 index 2e069180fa3..00000000000 --- a/homeassistant/components/eq3btsmart/number.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Platform for eq3 number entities.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from eq3btsmart import Thermostat -from eq3btsmart.const import ( - EQ3BT_MAX_OFFSET, - EQ3BT_MAX_TEMP, - EQ3BT_MIN_OFFSET, - EQ3BT_MIN_TEMP, -) -from eq3btsmart.models import Presets - -from homeassistant.components.number import ( - NumberDeviceClass, - NumberEntity, - NumberEntityDescription, - NumberMode, -) -from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import Eq3ConfigEntry -from .const import ( - ENTITY_KEY_COMFORT, - ENTITY_KEY_ECO, - ENTITY_KEY_OFFSET, - ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, - ENTITY_KEY_WINDOW_OPEN_TIMEOUT, - EQ3BT_STEP, -) -from .entity import Eq3Entity - - -@dataclass(frozen=True, kw_only=True) -class Eq3NumberEntityDescription(NumberEntityDescription): - """Entity description for eq3 number entities.""" - - value_func: Callable[[Presets], float] - value_set_func: Callable[ - [Thermostat], - Callable[[float], Awaitable[None]], - ] - mode: NumberMode = NumberMode.BOX - entity_category: EntityCategory | None = EntityCategory.CONFIG - - -NUMBER_ENTITY_DESCRIPTIONS = [ - Eq3NumberEntityDescription( - key=ENTITY_KEY_COMFORT, - value_func=lambda presets: presets.comfort_temperature.value, - value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature, - translation_key=ENTITY_KEY_COMFORT, - native_min_value=EQ3BT_MIN_TEMP, - native_max_value=EQ3BT_MAX_TEMP, - native_step=EQ3BT_STEP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - ), - Eq3NumberEntityDescription( - key=ENTITY_KEY_ECO, - value_func=lambda presets: presets.eco_temperature.value, - value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature, - translation_key=ENTITY_KEY_ECO, - native_min_value=EQ3BT_MIN_TEMP, - native_max_value=EQ3BT_MAX_TEMP, - native_step=EQ3BT_STEP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - ), - Eq3NumberEntityDescription( - key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, - value_func=lambda presets: presets.window_open_temperature.value, - value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature, - translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE, - native_min_value=EQ3BT_MIN_TEMP, - native_max_value=EQ3BT_MAX_TEMP, - native_step=EQ3BT_STEP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - ), - Eq3NumberEntityDescription( - key=ENTITY_KEY_OFFSET, - value_func=lambda presets: presets.offset_temperature.value, - value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset, - translation_key=ENTITY_KEY_OFFSET, - native_min_value=EQ3BT_MIN_OFFSET, - native_max_value=EQ3BT_MAX_OFFSET, - native_step=EQ3BT_STEP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - ), - Eq3NumberEntityDescription( - key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, - value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration, - value_func=lambda presets: presets.window_open_time.value.total_seconds() / 60, - translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT, - native_min_value=0, - native_max_value=60, - native_step=5, - native_unit_of_measurement=UnitOfTime.MINUTES, - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - entry: Eq3ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the entry.""" - - async_add_entities( - Eq3NumberEntity(entry, entity_description) - for entity_description in NUMBER_ENTITY_DESCRIPTIONS - ) - - -class Eq3NumberEntity(Eq3Entity, NumberEntity): - """Base class for all eq3 number entities.""" - - entity_description: Eq3NumberEntityDescription - - def __init__( - self, entry: Eq3ConfigEntry, entity_description: Eq3NumberEntityDescription - ) -> None: - """Initialize the entity.""" - - super().__init__(entry, entity_description.key) - self.entity_description = entity_description - - @property - def native_value(self) -> float: - """Return the state of the entity.""" - - if TYPE_CHECKING: - assert self._thermostat.status is not None - assert self._thermostat.status.presets is not None - - return self.entity_description.value_func(self._thermostat.status.presets) - - async def async_set_native_value(self, value: float) -> None: - """Set the state of the entity.""" - - await self.entity_description.value_set_func(self._thermostat)(value) - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - - return ( - self._thermostat.status is not None - and self._thermostat.status.presets is not None - and self._attr_available - ) diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json index acfd5082f45..03c3b21b964 100644 --- a/homeassistant/components/eq3btsmart/strings.json +++ b/homeassistant/components/eq3btsmart/strings.json @@ -25,23 +25,6 @@ "name": "Daylight saving time" } }, - "number": { - "comfort": { - "name": "Comfort temperature" - }, - "eco": { - "name": "Eco temperature" - }, - "offset": { - "name": "Offset temperature" - }, - "window_open_temperature": { - "name": "Window open temperature" - }, - "window_open_timeout": { - "name": "Window open timeout" - } - }, "switch": { "lock": { "name": "Lock" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 920ecda1c52..ec9ffdd7554 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -57,8 +57,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - _host: str - @staticmethod @callback def async_get_options_flow( @@ -69,6 +67,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize FRITZ!Box Tools flow.""" + self._host: str | None = None self._name: str = "" self._password: str = "" self._use_tls: bool = False @@ -113,6 +112,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" + assert self._host current_host = await self.hass.async_add_executor_job( socket.gethostbyname, self._host ) @@ -154,17 +154,15 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "") - host = ssdp_location.hostname - if not host or ipaddress.ip_address(host).is_link_local: - return self.async_abort(reason="ignore_ip6_link_local") - - self._host = host + self._host = ssdp_location.hostname self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] ) - uuid: str | None + if not self._host or ipaddress.ip_address(self._host).is_link_local: + return self.async_abort(reason="ignore_ip6_link_local") + if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index ffec4a9ea29..76754fc5082 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -43,11 +43,10 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - _name: str - def __init__(self) -> None: """Initialize flow.""" self._host: str | None = None + self._name: str | None = None self._password: str | None = None self._username: str | None = None @@ -159,6 +158,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): result = await self.async_try_connect() if result == RESULT_SUCCESS: + assert self._name is not None return self._get_entry(self._name) if result != RESULT_INVALID_AUTH: return self.async_abort(reason=result) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index f1f6e44abc1..fc91ef5e546 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -4,7 +4,6 @@ import logging import shutil from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError -from awesomeversion import AwesomeVersion from go2rtc_client import Go2RtcRestClient from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.ws import ( @@ -33,23 +32,13 @@ from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import ( - config_validation as cv, - discovery_flow, - issue_registry as ir, -) +from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import ( - CONF_DEBUG_UI, - DEBUG_UI_URL_MESSAGE, - DOMAIN, - HA_MANAGED_URL, - RECOMMENDED_VERSION, -) +from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL from .server import Server _LOGGER = logging.getLogger(__name__) @@ -158,21 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Validate the server URL try: client = Go2RtcRestClient(async_get_clientsession(hass), url) - version = await client.validate_server_version() - if version < AwesomeVersion(RECOMMENDED_VERSION): - ir.async_create_issue( - hass, - DOMAIN, - "recommended_version", - is_fixable=False, - is_persistent=False, - severity=ir.IssueSeverity.WARNING, - translation_key="recommended_version", - translation_placeholders={ - "recommended_version": RECOMMENDED_VERSION, - "current_version": str(version), - }, - ) + await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index 3c1c84c42b5..d33ae3e3897 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,4 +6,3 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" -RECOMMENDED_VERSION = "1.9.7" diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index 201b7168847..ea9308e5e18 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/go2rtc", "integration_type": "system", "iot_class": "local_polling", - "requirements": ["go2rtc-client==0.1.1"], + "requirements": ["go2rtc-client==0.1.0"], "single_config_entry": true } diff --git a/homeassistant/components/go2rtc/strings.json b/homeassistant/components/go2rtc/strings.json deleted file mode 100644 index e350c19af96..00000000000 --- a/homeassistant/components/go2rtc/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "recommended_version": { - "title": "Outdated go2rtc server detected", - "description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`." - } - } -} diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index fb8c9f76d79..f893b04b2d1 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber): self.entity_description = description self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" - async def async_set_native_value(self, value: float) -> None: + def set_native_value(self, value: float) -> None: """Update the current value.""" self._attr_native_value = value self.entity_description.store_value_fn(self.coordinator, self._shade.id, value) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index c19f37a040d..458ff50dac9 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -8,7 +8,6 @@ from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, - TimeoutException, ) from aioautomower.model import MowerAttributes from aioautomower.session import AutomowerSession @@ -23,7 +22,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAX_WS_RECONNECT_TIME = 600 SCAN_INTERVAL = timedelta(minutes=8) -DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): @@ -42,8 +40,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib update_interval=SCAN_INTERVAL, ) self.api = api + self.ws_connected: bool = False - self.reconnect_time = DEFAULT_RECONNECT_TIME async def _async_update_data(self) -> dict[str, MowerAttributes]: """Subscribe for websocket and poll data from the API.""" @@ -68,28 +66,24 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib hass: HomeAssistant, entry: ConfigEntry, automower_client: AutomowerSession, + reconnect_time: int = 2, ) -> None: """Listen with the client.""" try: await automower_client.auth.websocket_connect() - # Reset reconnect time after successful connection - self.reconnect_time = DEFAULT_RECONNECT_TIME + reconnect_time = 2 await automower_client.start_listening() except HusqvarnaWSServerHandshakeError as err: _LOGGER.debug( - "Failed to connect to websocket. Trying to reconnect: %s", - err, - ) - except TimeoutException as err: - _LOGGER.debug( - "Failed to listen to websocket. Trying to reconnect: %s", - err, + "Failed to connect to websocket. Trying to reconnect: %s", err ) + if not hass.is_stopping: - await asyncio.sleep(self.reconnect_time) - self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME) - entry.async_create_background_task( - hass, - self.client_listen(hass, entry, automower_client), - "reconnect_task", + await asyncio.sleep(reconnect_time) + reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME) + await self.client_listen( + hass=hass, + entry=entry, + automower_client=automower_client, + reconnect_time=reconnect_time, ) diff --git a/homeassistant/components/huum/__init__.py b/homeassistant/components/huum/__init__.py index 75faf1923df..c533ca34ef3 100644 --- a/homeassistant/components/huum/__init__.py +++ b/homeassistant/components/huum/__init__.py @@ -3,23 +3,30 @@ from __future__ import annotations import logging - -from huum.exceptions import Forbidden, NotAuthenticated -from huum.huum import Huum +import sys from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, PLATFORMS +if sys.version_info < (3, 13): + from huum.exceptions import Forbidden, NotAuthenticated + from huum.huum import Huum + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huum from a config entry.""" + if sys.version_info >= (3, 13): + raise HomeAssistantError( + "Huum is not supported on Python 3.13. Please use Python 3.12." + ) + username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/huum/climate.py b/homeassistant/components/huum/climate.py index df740aea3d1..b659e33038a 100644 --- a/homeassistant/components/huum/climate.py +++ b/homeassistant/components/huum/climate.py @@ -3,13 +3,9 @@ from __future__ import annotations import logging +import sys from typing import Any -from huum.const import SaunaStatus -from huum.exceptions import SafetyException -from huum.huum import Huum -from huum.schemas import HuumStatusResponse - from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -24,6 +20,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN +if sys.version_info < (3, 13): + from huum.const import SaunaStatus + from huum.exceptions import SafetyException + from huum.huum import Huum + from huum.schemas import HuumStatusResponse + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index 6a5fd96b99d..10c31378184 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -3,10 +3,9 @@ from __future__ import annotations import logging +import sys from typing import Any -from huum.exceptions import Forbidden, NotAuthenticated -from huum.huum import Huum import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -15,6 +14,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +if sys.version_info < (3, 13): + from huum.exceptions import Forbidden, NotAuthenticated + from huum.huum import Huum + _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/huum/manifest.json b/homeassistant/components/huum/manifest.json index 38562e1a072..025d1b97f21 100644 --- a/homeassistant/components/huum/manifest.json +++ b/homeassistant/components/huum/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huum", "iot_class": "cloud_polling", - "requirements": ["huum==0.7.12"] + "requirements": ["huum==0.7.11;python_version<'3.13'"] } diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index eb26ef48e4e..27f911822b5 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -20,8 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import device_registry as dr from .const import ( ADD_ENTITIES_CALLBACKS, @@ -42,26 +41,15 @@ from .helpers import ( register_lcn_address_devices, register_lcn_host_device, ) -from .services import register_services +from .services import SERVICES from .websocket import register_panel_and_ws_api _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the LCN component.""" - hass.data.setdefault(DOMAIN, {}) - - await register_services(hass) - await register_panel_and_ws_api(hass) - - return True - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a connection to PCHK host from a config entry.""" + hass.data.setdefault(DOMAIN, {}) if config_entry.entry_id in hass.data[DOMAIN]: return False @@ -121,6 +109,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) lcn_connection.register_for_inputs(input_received) + # register service calls + for service_name, service in SERVICES: + if not hass.services.has_service(DOMAIN, service_name): + hass.services.async_register( + DOMAIN, service_name, service(hass).async_call_service, service.schema + ) + + await register_panel_and_ws_api(hass) + return True @@ -171,6 +168,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> host = hass.data[DOMAIN].pop(config_entry.entry_id) await host[CONNECTION].async_close() + # unregister service calls + if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload + for service_name, _ in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 92f5863c47e..611a7353bcd 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -429,11 +429,3 @@ SERVICES = ( (LcnService.DYN_TEXT, DynText), (LcnService.PCK, Pck), ) - - -async def register_services(hass: HomeAssistant) -> None: - """Register services for LCN.""" - for service_name, service in SERVICES: - hass.services.async_register( - DOMAIN, service_name, service(hass).async_call_service, service.schema - ) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 6316eb72096..16e7bf552ba 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.12.2", "mill-local==0.3.0"] + "requirements": ["millheater==0.11.8", "mill-local==0.3.0"] } diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index 65e6652407f..23401f30abc 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -4,8 +4,9 @@ "after_dependencies": ["media_source", "media_player"], "codeowners": ["@music-assistant"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/music_assistant", + "documentation": "https://music-assistant.io", "iot_class": "local_push", + "issue_tracker": "https://github.com/music-assistant/hass-music-assistant/issues", "loggers": ["music_assistant"], "requirements": ["music-assistant-client==1.0.5"], "zeroconf": ["_mass._tcp.local."] diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 7921bdb6ed5..23a46c5e1c9 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.11.1"] + "requirements": ["reolink-aio==0.10.4"] } diff --git a/homeassistant/components/ring/event.py b/homeassistant/components/ring/event.py index 71a4bc8aea5..e6d9d25542f 100644 --- a/homeassistant/components/ring/event.py +++ b/homeassistant/components/ring/event.py @@ -96,7 +96,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity) @callback def _handle_coordinator_update(self) -> None: - if (alert := self._get_coordinator_alert()) and not alert.is_update: + if alert := self._get_coordinator_alert(): self._async_handle_event(alert.kind) super()._handle_coordinator_update() diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index e431c680081..63c47cb2979 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell==0.9.12"] + "requirements": ["ring-doorbell==0.9.9"] } diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index fe592074f71..20bc50f9855 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -106,12 +107,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def _async_update_data(self) -> DeviceProp: """Update data via library.""" try: - # Update device props and standard api information - await self._update_device_prop() - # Set the new map id from the updated device props + await asyncio.gather(*(self._update_device_prop(), self.get_rooms())) self._set_current_map() - # Get the rooms for that map id. - await self.get_rooms() except RoborockException as ex: raise UpdateFailed(ex) from ex return self.roborock_device_info.props diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index 73cb95d2d7c..3dfe0e72a7b 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -135,9 +135,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): RoborockCommand.LOAD_MULTI_MAP, [map_id], ) - # Update the current map id manually so that nothing gets broken - # if another service hits the api. - self.coordinator.current_map = map_id # We need to wait after updating the map # so that other commands will be executed correctly. await asyncio.sleep(MAP_SLEEP) @@ -151,9 +148,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): @property def current_option(self) -> str | None: """Get the current status of the select entity from device_status.""" - if ( - (current_map := self.coordinator.current_map) is not None - and current_map in self.coordinator.maps - ): # 63 means it is searching for a map. + if (current_map := self.coordinator.current_map) is not None: return self.coordinator.maps[current_map].name return None diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index 8d56f3a5563..2066b65221e 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioruckus"], - "requirements": ["aioruckus==0.42"] + "requirements": ["aioruckus==0.41"] } diff --git a/homeassistant/components/sky_remote/__init__.py b/homeassistant/components/sky_remote/__init__.py deleted file mode 100644 index 4daad78c558..00000000000 --- a/homeassistant/components/sky_remote/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -"""The Sky Remote Control integration.""" - -import logging - -from skyboxremote import RemoteControl, SkyBoxConnectionError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady - -PLATFORMS = [Platform.REMOTE] - -_LOGGER = logging.getLogger(__name__) - - -type SkyRemoteConfigEntry = ConfigEntry[RemoteControl] - - -async def async_setup_entry(hass: HomeAssistant, entry: SkyRemoteConfigEntry) -> bool: - """Set up Sky remote.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - - _LOGGER.debug("Setting up Host: %s, Port: %s", host, port) - remote = RemoteControl(host, port) - try: - await remote.check_connectable() - except SkyBoxConnectionError as e: - raise ConfigEntryNotReady from e - - entry.runtime_data = remote - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sky_remote/config_flow.py b/homeassistant/components/sky_remote/config_flow.py deleted file mode 100644 index a55dfb2a52b..00000000000 --- a/homeassistant/components/sky_remote/config_flow.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Config flow for sky_remote.""" - -import logging -from typing import Any - -from skyboxremote import RemoteControl, SkyBoxConnectionError -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv - -from .const import DEFAULT_PORT, DOMAIN, LEGACY_PORT - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - } -) - - -async def async_find_box_port(host: str) -> int: - """Find port box uses for communication.""" - logging.debug("Attempting to find port to connect to %s on", host) - remote = RemoteControl(host, DEFAULT_PORT) - try: - await remote.check_connectable() - except SkyBoxConnectionError: - # Try legacy port if the default one failed - remote = RemoteControl(host, LEGACY_PORT) - await remote.check_connectable() - return LEGACY_PORT - return DEFAULT_PORT - - -class SkyRemoteConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Sky Remote.""" - - VERSION = 1 - MINOR_VERSION = 1 - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the user step.""" - - errors: dict[str, str] = {} - if user_input is not None: - logging.debug("user_input: %s", user_input) - self._async_abort_entries_match(user_input) - try: - port = await async_find_box_port(user_input[CONF_HOST]) - except SkyBoxConnectionError: - logging.exception("while finding port of skybox") - errors["base"] = "cannot_connect" - else: - return self.async_create_entry( - title=user_input[CONF_HOST], - data={**user_input, CONF_PORT: port}, - ) - - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) diff --git a/homeassistant/components/sky_remote/const.py b/homeassistant/components/sky_remote/const.py deleted file mode 100644 index e67744a741b..00000000000 --- a/homeassistant/components/sky_remote/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants.""" - -DOMAIN = "sky_remote" - -DEFAULT_PORT = 49160 -LEGACY_PORT = 5900 diff --git a/homeassistant/components/sky_remote/manifest.json b/homeassistant/components/sky_remote/manifest.json deleted file mode 100644 index b00ff309b10..00000000000 --- a/homeassistant/components/sky_remote/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "sky_remote", - "name": "Sky Remote Control", - "codeowners": ["@dunnmj", "@saty9"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/sky_remote", - "integration_type": "device", - "iot_class": "assumed_state", - "requirements": ["skyboxremote==0.0.6"] -} diff --git a/homeassistant/components/sky_remote/remote.py b/homeassistant/components/sky_remote/remote.py deleted file mode 100644 index 05a464f73a6..00000000000 --- a/homeassistant/components/sky_remote/remote.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Home Assistant integration to control a sky box using the remote platform.""" - -from collections.abc import Iterable -import logging -from typing import Any - -from skyboxremote import VALID_KEYS, RemoteControl - -from homeassistant.components.remote import RemoteEntity -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import SkyRemoteConfigEntry -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config: SkyRemoteConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Sky remote platform.""" - async_add_entities( - [SkyRemote(config.runtime_data, config.entry_id)], - True, - ) - - -class SkyRemote(RemoteEntity): - """Representation of a Sky Remote.""" - - _attr_has_entity_name = True - _attr_name = None - - def __init__(self, remote: RemoteControl, unique_id: str) -> None: - """Initialize the Sky Remote.""" - self._remote = remote - self._attr_unique_id = unique_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer="SKY", - model="Sky Box", - name=remote.host, - ) - - def turn_on(self, activity: str | None = None, **kwargs: Any) -> None: - """Send the power on command.""" - self.send_command(["sky"]) - - def turn_off(self, activity: str | None = None, **kwargs: Any) -> None: - """Send the power command.""" - self.send_command(["power"]) - - def send_command(self, command: Iterable[str], **kwargs: Any) -> None: - """Send a list of commands to the device.""" - for cmd in command: - if cmd not in VALID_KEYS: - raise ServiceValidationError( - f"{cmd} is not in Valid Keys: {VALID_KEYS}" - ) - try: - self._remote.send_keys(command) - except ValueError as err: - _LOGGER.error("Invalid command: %s. Error: %s", command, err) - return - _LOGGER.debug("Successfully sent command %s", command) diff --git a/homeassistant/components/sky_remote/strings.json b/homeassistant/components/sky_remote/strings.json deleted file mode 100644 index af794490c43..00000000000 --- a/homeassistant/components/sky_remote/strings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - }, - "step": { - "user": { - "title": "Add Sky Remote", - "data": { - "host": "[%key:common::config_flow::data::host%]" - }, - "data_description": { - "host": "Hostname or IP address of your Sky device" - } - } - } - } -} diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json index 341a300a26e..188459b4f16 100644 --- a/homeassistant/components/smarty/strings.json +++ b/homeassistant/components/smarty/strings.json @@ -28,10 +28,6 @@ "deprecated_yaml_import_issue_auth_error": { "title": "YAML import failed due to an authentication error", "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." - }, - "deprecated_yaml_import_issue_cannot_connect": { - "title": "YAML import failed due to a connection error", - "description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." } }, "entity": { diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index eca4f465435..191d10a70dd 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -17,8 +17,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "unknown": "Unexpected error" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } }, "entity": { diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 5235bd5230b..a899b562f36 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -34,6 +34,7 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): _app_code: str _app_token: str _captcha_image: str + _phone_number: str def __init__(self) -> None: """Initialize flow.""" @@ -49,7 +50,6 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): self._slnet_token_expires = None self._captcha_sid: str | None = None self._captcha_code: str | None = None - self._phone_number = None self._auth = StarlineAuth() diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 00da729dccd..78625192e4a 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -37,13 +37,13 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "incorrect_pin": "Incorrect PIN", "bad_pin_format": "PIN should be 4 digits", + "two_factor_request_failed": "Request for 2FA code failed, please try again", "bad_validation_code_format": "Validation code should be 6 digits", "incorrect_validation_code": "Incorrect validation code" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "two_factor_request_failed": "Request for 2FA code failed, please try again" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "options": { diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index f1225f74f06..57188aebaa3 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -2,7 +2,7 @@ "domain": "template", "name": "Template", "after_dependencies": ["group"], - "codeowners": ["@PhracturedBlue", "@home-assistant/core"], + "codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"], "config_flow": true, "dependencies": ["blueprint"], "documentation": "https://www.home-assistant.io/integrations/template", diff --git a/homeassistant/components/tesla_fleet/oauth.py b/homeassistant/components/tesla_fleet/oauth.py index 8b43460436b..00976abf56f 100644 --- a/homeassistant/components/tesla_fleet/oauth.py +++ b/homeassistant/components/tesla_fleet/oauth.py @@ -49,7 +49,6 @@ class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementati def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return { - "prompt": "login", "scope": " ".join(SCOPES), "code_challenge": self.code_challenge, # PKCE } @@ -84,4 +83,4 @@ class TeslaUserImplementation(AuthImplementation): @property def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" - return {"prompt": "login", "scope": " ".join(SCOPES)} + return {"scope": " ".join(SCOPES)} diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 307fcaf0ea8..fb76253eb3d 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -22,7 +22,7 @@ from .const import _LOGGER, DOMAIN, LINE_TYPES from .coordinator import VodafoneStationRouter NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"] -UPTIME_DEVIATION = 60 +UPTIME_DEVIATION = 45 @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 07e132a0b5b..741b277d84d 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -1,5 +1,4 @@ { - "title": "Water heater", "device_automation": { "action_type": { "turn_on": "[%key:common::device_automation::action_type::turn_on%]", @@ -8,7 +7,7 @@ }, "entity_component": { "_": { - "name": "[%key:component::water_heater::title%]", + "name": "Water heater", "state": { "off": "[%key:common::state::off%]", "eco": "Eco", diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index f9e8328ae53..a0a86be5da3 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.1.3"] + "requirements": ["aiowithings==3.1.1"] } diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index f3f7f38772d..1c7e0d105c4 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -33,7 +33,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt as dt_util @@ -105,26 +104,25 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: yellow_radio.description = "Yellow Zigbee module" yellow_radio.manufacturer = "Nabu Casa" - if is_hassio(hass): - # Present the multi-PAN addon as a setup option, if it's available - multipan_manager = ( - await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(hass) + # Present the multi-PAN addon as a setup option, if it's available + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) + + try: + addon_info = await multipan_manager.async_get_addon_info() + except (AddonError, KeyError): + addon_info = None + + if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: + addon_port = ListPortInfo( + device=silabs_multiprotocol_addon.get_zigbee_socket(), + skip_link_detection=True, ) - try: - addon_info = await multipan_manager.async_get_addon_info() - except (AddonError, KeyError): - addon_info = None - - if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: - addon_port = ListPortInfo( - device=silabs_multiprotocol_addon.get_zigbee_socket(), - skip_link_detection=True, - ) - - addon_port.description = "Multiprotocol add-on" - addon_port.manufacturer = "Nabu Casa" - ports.append(addon_port) + addon_port.description = "Multiprotocol add-on" + addon_port.manufacturer = "Nabu Casa" + ports.append(addon_port) return ports diff --git a/homeassistant/const.py b/homeassistant/const.py index 4082a076b94..558e7ec2b0b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -29,9 +29,9 @@ PATCH_VERSION: Final = "0.dev0" __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) -REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) +REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2025.2" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index a105efc2685..c4612898cb2 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -8,26 +8,6 @@ from __future__ import annotations from typing import Final BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ - { - "domain": "acaia", - "manufacturer_id": 16962, - }, - { - "domain": "acaia", - "local_name": "ACAIA*", - }, - { - "domain": "acaia", - "local_name": "PYXIS-*", - }, - { - "domain": "acaia", - "local_name": "LUNAR-*", - }, - { - "domain": "acaia", - "local_name": "PROCHBT001", - }, { "domain": "airthings_ble", "manufacturer_id": 820, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ffe61b915c6..cbd30b560ce 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,7 +24,6 @@ FLOWS = { ], "integration": [ "abode", - "acaia", "accuweather", "acmeda", "adax", @@ -538,7 +537,6 @@ FLOWS = { "simplefin", "simplepush", "simplisafe", - "sky_remote", "skybell", "slack", "sleepiq", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f007db87868..a1fdb9478f3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -11,12 +11,6 @@ "config_flow": true, "iot_class": "cloud_push" }, - "acaia": { - "name": "Acaia", - "integration_type": "device", - "config_flow": true, - "iot_class": "local_push" - }, "accuweather": { "name": "AccuWeather", "integration_type": "service", @@ -5614,22 +5608,11 @@ "config_flow": false, "iot_class": "local_push" }, - "sky": { - "name": "Sky", - "integrations": { - "sky_hub": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling", - "name": "Sky Hub" - }, - "sky_remote": { - "integration_type": "device", - "config_flow": true, - "iot_class": "assumed_state", - "name": "Sky Remote Control" - } - } + "sky_hub": { + "name": "Sky Hub", + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_polling" }, "skybeacon": { "name": "Skybeacon", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5bc539beb86..956ea032fe7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.1.1 -aiohttp==3.11.0 +aiohttp==3.11.0rc2 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 @@ -28,14 +28,14 @@ ciso8601==2.3.1 cryptography==43.0.1 dbus-fast==2.24.3 fnv-hash-fast==1.0.2 -go2rtc-client==0.1.1 +go2rtc-client==0.1.0 ha-ffmpeg==3.2.2 habluetooth==3.6.0 hass-nabucasa==0.84.0 -hassil==2.0.1 +hassil==1.7.4 home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241106.2 -home-assistant-intents==2024.11.13 +home-assistant-intents==2024.11.6 httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 @@ -181,8 +181,8 @@ chacha20poly1305-reuseable>=0.13.0 # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 pycountry>=23.12.11 -# scapy==2.6.0 causes CI failures due to a race condition -scapy>=2.6.1 +# scapy<2.5.0 will not work with python3.12 +scapy>=2.5.0 # tuf isn't updated to deal with breaking changes in securesystemslib==1.0. # Only tuf>=4 includes a constraint to <1.0. diff --git a/pyproject.toml b/pyproject.toml index ebf22a93d7d..8e588ce0b0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Topic :: Home Automation", ] requires-python = ">=3.12.0" @@ -29,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.0", + "aiohttp==3.11.0rc2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index b97c8dc57a0..ac7c00b8050 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.0 +aiohttp==3.11.0rc2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 65ef5f1ebf2..b7a979050bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,9 +172,6 @@ aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.10 -# homeassistant.components.acaia -aioacaia==0.1.6 - # homeassistant.components.airq aioairq==0.3.2 @@ -182,7 +179,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.6 +aioairzone==0.9.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -357,7 +354,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.42 +aioruckus==0.41 # homeassistant.components.russound_rio aiorussound==4.1.0 @@ -384,7 +381,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.5 +aiostreammagic==2.8.4 # homeassistant.components.switcher_kis aioswitcher==4.4.0 @@ -420,7 +417,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.3 +aiowithings==3.1.1 # homeassistant.components.yandex_transport aioymaps==1.2.5 @@ -863,7 +860,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.4.1 +eq3btsmart==1.2.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -996,7 +993,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.1 +go2rtc-client==0.1.0 # homeassistant.components.goalzero goalzero==0.2.2 @@ -1096,7 +1093,7 @@ hass-nabucasa==0.84.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==2.0.1 +hassil==1.7.4 # homeassistant.components.jewish_calendar hdate==0.10.9 @@ -1133,7 +1130,7 @@ holidays==0.60 home-assistant-frontend==20241106.2 # homeassistant.components.conversation -home-assistant-intents==2024.11.13 +home-assistant-intents==2024.11.6 # homeassistant.components.home_connect homeconnect==0.8.0 @@ -1151,7 +1148,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.12 +huum==0.7.11;python_version<'3.13' # homeassistant.components.hyperion hyperion-py==0.7.5 @@ -1376,7 +1373,7 @@ microBeesPy==0.3.2 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.2 +millheater==0.11.8 # homeassistant.components.minio minio==7.1.12 @@ -2556,7 +2553,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.1 +reolink-aio==0.10.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2565,7 +2562,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.12 +ring-doorbell==0.9.9 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2676,9 +2673,6 @@ simplisafe-python==2024.01.0 # homeassistant.components.sisyphus sisyphus-control==3.1.4 -# homeassistant.components.sky_remote -skyboxremote==0.0.6 - # homeassistant.components.slack slackclient==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b61e65f3c68..ec6be67d4b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,9 +160,6 @@ aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.10 -# homeassistant.components.acaia -aioacaia==0.1.6 - # homeassistant.components.airq aioairq==0.3.2 @@ -170,7 +167,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.6 +aioairzone==0.9.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -339,7 +336,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.42 +aioruckus==0.41 # homeassistant.components.russound_rio aiorussound==4.1.0 @@ -366,7 +363,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.5 +aiostreammagic==2.8.4 # homeassistant.components.switcher_kis aioswitcher==4.4.0 @@ -402,7 +399,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.3 +aiowithings==3.1.1 # homeassistant.components.yandex_transport aioymaps==1.2.5 @@ -732,7 +729,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.4.1 +eq3btsmart==1.2.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -846,7 +843,7 @@ gios==5.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.1 +go2rtc-client==0.1.0 # homeassistant.components.goalzero goalzero==0.2.2 @@ -931,7 +928,7 @@ habluetooth==3.6.0 hass-nabucasa==0.84.0 # homeassistant.components.conversation -hassil==2.0.1 +hassil==1.7.4 # homeassistant.components.jewish_calendar hdate==0.10.9 @@ -959,7 +956,7 @@ holidays==0.60 home-assistant-frontend==20241106.2 # homeassistant.components.conversation -home-assistant-intents==2024.11.13 +home-assistant-intents==2024.11.6 # homeassistant.components.home_connect homeconnect==0.8.0 @@ -974,7 +971,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.12 +huum==0.7.11;python_version<'3.13' # homeassistant.components.hyperion hyperion-py==0.7.5 @@ -1145,7 +1142,7 @@ microBeesPy==0.3.2 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.2 +millheater==0.11.8 # homeassistant.components.minio minio==7.1.12 @@ -2047,13 +2044,13 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.1 +reolink-aio==0.10.4 # homeassistant.components.rflink rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.12 +ring-doorbell==0.9.9 # homeassistant.components.roku rokuecp==0.19.3 @@ -2134,9 +2131,6 @@ simplepush==2.2.3 # homeassistant.components.simplisafe simplisafe-python==2024.01.0 -# homeassistant.components.sky_remote -skyboxremote==0.0.6 - # homeassistant.components.slack slackclient==2.5.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7d53741c661..c5611069bf5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -214,8 +214,8 @@ chacha20poly1305-reuseable>=0.13.0 # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 pycountry>=23.12.11 -# scapy==2.6.0 causes CI failures due to a race condition -scapy>=2.6.1 +# scapy<2.5.0 will not work with python3.12 +scapy>=2.5.0 # tuf isn't updated to deal with breaking changes in securesystemslib==1.0. # Only tuf>=4 includes a constraint to <1.0. diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 57d86bc4def..9d38d8f7128 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from pathlib import Path from homeassistant import core -from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION as GO2RTC_VERSION from homeassistant.const import Platform from homeassistant.util import executor, thread from script.gen_requirements_all import gather_recursive_requirements @@ -80,7 +79,7 @@ WORKDIR /config _HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.13-alpine +FROM python:3.12-alpine ENV \ UV_SYSTEM_PYTHON=true \ @@ -113,6 +112,8 @@ LABEL "com.github.actions.icon"="terminal" LABEL "com.github.actions.color"="gray-dark" """ +_GO2RTC_VERSION = "1.9.7" + def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: package_versions: dict[str, str] = {} @@ -161,8 +162,6 @@ def _generate_hassfest_dockerimage( packages.update( gather_recursive_requirements(platform.value, already_checked_domains) ) - # Add go2rtc requirements as this file needs the go2rtc integration - packages.update(gather_recursive_requirements("go2rtc", already_checked_domains)) return File( _HASSFEST_TEMPLATE.format( @@ -198,7 +197,7 @@ def _generate_files(config: Config) -> list[File]: DOCKERFILE_TEMPLATE.format( timeout=timeout, **package_versions, - go2rtc=GO2RTC_VERSION, + go2rtc=_GO2RTC_VERSION, ), config.root / "Dockerfile", ), diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 0fa0a1a89fa..9bad1e8aecc 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -1,7 +1,7 @@ # Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.13-alpine +FROM python:3.12-alpine ENV \ UV_SYSTEM_PYTHON=true \ @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4013c8a6c19..6d2f4087f59 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -268,6 +268,7 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( ) ], vol.Required("documentation"): vol.All(vol.Url(), documentation_url), + vol.Optional("issue_tracker"): vol.Url(), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("requirements"): [str], vol.Optional("dependencies"): [str], @@ -303,7 +304,6 @@ def manifest_schema(value: dict[str, Any]) -> vol.Schema: CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend( { vol.Optional("version"): vol.All(str, verify_version), - vol.Optional("issue_tracker"): vol.Url(), vol.Optional("import_executor"): bool, } ) diff --git a/tests/components/acaia/__init__.py b/tests/components/acaia/__init__.py deleted file mode 100644 index f4eaa39e615..00000000000 --- a/tests/components/acaia/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Common test tools for the acaia integration.""" - -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Set up the acaia integration for testing.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/acaia/conftest.py b/tests/components/acaia/conftest.py deleted file mode 100644 index 1dc6ff31051..00000000000 --- a/tests/components/acaia/conftest.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Common fixtures for the acaia tests.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, patch - -from aioacaia.acaiascale import AcaiaDeviceState -from aioacaia.const import UnitMass as AcaiaUnitOfMass -import pytest - -from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN -from homeassistant.const import CONF_ADDRESS -from homeassistant.core import HomeAssistant - -from . import setup_integration - -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.acaia.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_verify() -> Generator[AsyncMock]: - """Override is_new_scale check.""" - with patch( - "homeassistant.components.acaia.config_flow.is_new_scale", return_value=True - ) as mock_verify: - yield mock_verify - - -@pytest.fixture -def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: - """Return the default mocked config entry.""" - return MockConfigEntry( - title="LUNAR-DDEEFF", - domain=DOMAIN, - version=1, - data={ - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_IS_NEW_STYLE_SCALE: True, - }, - unique_id="aa:bb:cc:dd:ee:ff", - ) - - -@pytest.fixture -async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_scale: MagicMock -) -> None: - """Set up the acaia integration for testing.""" - await setup_integration(hass, mock_config_entry) - - -@pytest.fixture -def mock_scale() -> Generator[MagicMock]: - """Return a mocked acaia scale client.""" - with ( - patch( - "homeassistant.components.acaia.coordinator.AcaiaScale", - autospec=True, - ) as scale_mock, - ): - scale = scale_mock.return_value - scale.connected = True - scale.mac = "aa:bb:cc:dd:ee:ff" - scale.model = "Lunar" - scale.timer_running = True - scale.heartbeat_task = None - scale.process_queue_task = None - scale.device_state = AcaiaDeviceState( - battery_level=42, units=AcaiaUnitOfMass.GRAMS - ) - scale.weight = 123.45 - yield scale diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr deleted file mode 100644 index cd91ca1a17a..00000000000 --- a/tests/components/acaia/snapshots/test_button.ambr +++ /dev/null @@ -1,139 +0,0 @@ -# serializer version: 1 -# name: test_buttons[button.lunar_ddeeff_reset_timer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': None, - 'entity_id': 'button.lunar_ddeeff_reset_timer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reset timer', - 'platform': 'acaia', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'reset_timer', - 'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[button.lunar_ddeeff_reset_timer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Reset timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_reset_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[button.lunar_ddeeff_start_stop_timer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': None, - 'entity_id': 'button.lunar_ddeeff_start_stop_timer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Start/stop timer', - 'platform': 'acaia', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'start_stop', - 'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[button.lunar_ddeeff_start_stop_timer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_start_stop_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[button.lunar_ddeeff_tare-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': None, - 'entity_id': 'button.lunar_ddeeff_tare', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tare', - 'platform': 'acaia', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tare', - 'unique_id': 'aa:bb:cc:dd:ee:ff_tare', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[button.lunar_ddeeff_tare-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Tare', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_tare', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr deleted file mode 100644 index 1cc3d8dbbc0..00000000000 --- a/tests/components/acaia/snapshots/test_init.ambr +++ /dev/null @@ -1,33 +0,0 @@ -# serializer version: 1 -# name: test_device - DeviceRegistryEntrySnapshot({ - 'area_id': 'kitchen', - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'acaia', - 'aa:bb:cc:dd:ee:ff', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Acaia', - 'model': 'Lunar', - 'model_id': None, - 'name': 'LUNAR-DDEEFF', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': 'Kitchen', - 'sw_version': None, - 'via_device_id': None, - }) -# --- diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py deleted file mode 100644 index f68f85e253d..00000000000 --- a/tests/components/acaia/test_button.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Tests for the acaia buttons.""" - -from datetime import timedelta -from unittest.mock import MagicMock, patch - -from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion - -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_UNAVAILABLE, - STATE_UNKNOWN, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform - -BUTTONS = ( - "tare", - "reset_timer", - "start_stop_timer", -) - - -async def test_buttons( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_scale: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the acaia buttons.""" - - with patch("homeassistant.components.acaia.PLATFORMS", [Platform.BUTTON]): - await setup_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_button_presses( - hass: HomeAssistant, - mock_scale: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the acaia button presses.""" - - await setup_integration(hass, mock_config_entry) - - for button in BUTTONS: - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - { - ATTR_ENTITY_ID: f"button.lunar_ddeeff_{button}", - }, - blocking=True, - ) - - function = getattr(mock_scale, button) - function.assert_called_once() - - -async def test_buttons_unavailable_on_disconnected_scale( - hass: HomeAssistant, - mock_scale: MagicMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test the acaia buttons are unavailable when the scale is disconnected.""" - - await setup_integration(hass, mock_config_entry) - - for button in BUTTONS: - state = hass.states.get(f"button.lunar_ddeeff_{button}") - assert state - assert state.state == STATE_UNKNOWN - - mock_scale.connected = False - freezer.tick(timedelta(minutes=10)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - for button in BUTTONS: - state = hass.states.get(f"button.lunar_ddeeff_{button}") - assert state - assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/acaia/test_config_flow.py b/tests/components/acaia/test_config_flow.py deleted file mode 100644 index 2bf4b1dbe8a..00000000000 --- a/tests/components/acaia/test_config_flow.py +++ /dev/null @@ -1,242 +0,0 @@ -"""Test the acaia config flow.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, patch - -from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice -import pytest - -from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN -from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER -from homeassistant.const import CONF_ADDRESS -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo - -from tests.common import MockConfigEntry - -service_info = BluetoothServiceInfo( - name="LUNAR-DDEEFF", - address="aa:bb:cc:dd:ee:ff", - rssi=-63, - manufacturer_data={}, - service_data={}, - service_uuids=[], - source="local", -) - - -@pytest.fixture -def mock_discovered_service_info() -> Generator[AsyncMock]: - """Override getting Bluetooth service info.""" - with patch( - "homeassistant.components.acaia.config_flow.async_discovered_service_info", - return_value=[service_info], - ) as mock_discovered_service_info: - yield mock_discovered_service_info - - -async def test_form( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_verify: AsyncMock, - mock_discovered_service_info: AsyncMock, -) -> None: - """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - user_input = { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - } - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "LUNAR-DDEEFF" - assert result2["data"] == { - **user_input, - CONF_IS_NEW_STYLE_SCALE: True, - } - - -async def test_bluetooth_discovery( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_verify: AsyncMock, -) -> None: - """Test we can discover a device.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "bluetooth_confirm" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={}, - ) - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == service_info.name - assert result2["data"] == { - CONF_ADDRESS: service_info.address, - CONF_IS_NEW_STYLE_SCALE: True, - } - - -@pytest.mark.parametrize( - ("exception", "error"), - [ - (AcaiaDeviceNotFound("Error"), "device_not_found"), - (AcaiaError, "unknown"), - (AcaiaUnknownDevice, "unsupported_device"), - ], -) -async def test_bluetooth_discovery_errors( - hass: HomeAssistant, - mock_verify: AsyncMock, - exception: Exception, - error: str, -) -> None: - """Test abortions of Bluetooth discovery.""" - mock_verify.side_effect = exception - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == error - - -async def test_already_configured( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_verify: AsyncMock, - mock_discovered_service_info: AsyncMock, -) -> None: - """Ensure we can't add the same device twice.""" - - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_configured" - - -async def test_already_configured_bluetooth_discovery( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure configure device is not discovered again.""" - - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.parametrize( - ("exception", "error"), - [ - (AcaiaDeviceNotFound("Error"), "device_not_found"), - (AcaiaError, "unknown"), - ], -) -async def test_recoverable_config_flow_errors( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_verify: AsyncMock, - mock_discovered_service_info: AsyncMock, - exception: Exception, - error: str, -) -> None: - """Test recoverable errors.""" - mock_verify.side_effect = exception - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": error} - - # recover - mock_verify.side_effect = None - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, - ) - assert result3["type"] is FlowResultType.CREATE_ENTRY - - -async def test_unsupported_device( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_verify: AsyncMock, - mock_discovered_service_info: AsyncMock, -) -> None: - """Test flow aborts on unsupported device.""" - mock_verify.side_effect = AcaiaUnknownDevice - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - }, - ) - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "unsupported_device" - - -async def test_no_bluetooth_devices( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_discovered_service_info: AsyncMock, -) -> None: - """Test flow aborts on unsupported device.""" - mock_discovered_service_info.return_value = [] - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "no_devices_found" diff --git a/tests/components/acaia/test_init.py b/tests/components/acaia/test_init.py deleted file mode 100644 index 8ad988d3b9b..00000000000 --- a/tests/components/acaia/test_init.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Test init of acaia integration.""" - -from datetime import timedelta -from unittest.mock import MagicMock - -from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError -from freezegun.api import FrozenDateTimeFactory -import pytest -from syrupy import SnapshotAssertion - -from homeassistant.components.acaia.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from tests.common import MockConfigEntry, async_fire_time_changed - -pytestmark = pytest.mark.usefixtures("init_integration") - - -async def test_load_unload_config_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test loading and unloading the integration.""" - - assert mock_config_entry.state is ConfigEntryState.LOADED - - await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - "exception", [AcaiaError, AcaiaDeviceNotFound("Boom"), TimeoutError] -) -async def test_update_exception_leads_to_active_disconnect( - hass: HomeAssistant, - mock_scale: MagicMock, - freezer: FrozenDateTimeFactory, - exception: Exception, -) -> None: - """Test scale gets disconnected on exception.""" - - mock_scale.connect.side_effect = exception - mock_scale.connected = False - - freezer.tick(timedelta(minutes=10)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - mock_scale.device_disconnected_handler.assert_called_once() - - -async def test_device( - mock_scale: MagicMock, - device_registry: dr.DeviceRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Snapshot the device from registry.""" - - device = device_registry.async_get_device({(DOMAIN, mock_scale.mac)}) - assert device - assert device == snapshot diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index 89a2a2a2b1a..90b23f87ab1 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -489,96 +489,3 @@ async def test_alarm_control_panel_log_deprecated_state_warning_using_attr_state ) # Test we only log once assert "Entities should implement the 'alarm_state' property and" not in caplog.text - - -async def test_alarm_control_panel_deprecated_state_does_not_break_state( - hass: HomeAssistant, - code_format: CodeFormat | None, - supported_features: AlarmControlPanelEntityFeature, - code_arm_required: bool, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test using _attr_state attribute does not break state.""" - - async def async_setup_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setups( - config_entry, [ALARM_CONTROL_PANEL_DOMAIN] - ) - return True - - mock_integration( - hass, - MockModule( - TEST_DOMAIN, - async_setup_entry=async_setup_entry_init, - ), - ) - - class MockLegacyAlarmControlPanel(MockAlarmControlPanel): - """Mocked alarm control entity.""" - - def __init__( - self, - supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature( - 0 - ), - code_format: CodeFormat | None = None, - code_arm_required: bool = True, - ) -> None: - """Initialize the alarm control.""" - self._attr_state = "armed_away" - super().__init__(supported_features, code_format, code_arm_required) - - def alarm_disarm(self, code: str | None = None) -> None: - """Mock alarm disarm calls.""" - self._attr_state = "disarmed" - - entity = MockLegacyAlarmControlPanel( - supported_features=supported_features, - code_format=code_format, - code_arm_required=code_arm_required, - ) - - async def async_setup_entry_platform( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Set up test alarm control panel platform via config entry.""" - async_add_entities([entity]) - - mock_platform( - hass, - f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}", - MockPlatform(async_setup_entry=async_setup_entry_platform), - ) - - with patch.object( - MockLegacyAlarmControlPanel, - "__module__", - "tests.custom_components.test.alarm_control_panel", - ): - config_entry = MockConfigEntry(domain=TEST_DOMAIN) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity.entity_id) - assert state is not None - assert state.state == "armed_away" - - with patch.object( - MockLegacyAlarmControlPanel, - "__module__", - "tests.custom_components.test.alarm_control_panel", - ): - await help_test_async_alarm_control_panel_service( - hass, entity.entity_id, SERVICE_ALARM_DISARM - ) - - state = hass.states.get(entity.entity_id) - assert state is not None - assert state.state == "disarmed" diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index b806c6faf23..131444c17ac 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -697,7 +697,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any area called Are', + 'speech': 'Sorry, I am not aware of any area called are', }), }), }), @@ -741,7 +741,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any area called Are', + 'speech': 'Sorry, I am not aware of any area called are', }), }), }), diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py deleted file mode 100644 index 631c774e63c..00000000000 --- a/tests/components/backup/conftest.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Test fixtures for the Backup integration.""" - -from __future__ import annotations - -from collections.abc import Generator -from pathlib import Path -from unittest.mock import MagicMock, Mock, patch - -import pytest - -from homeassistant.core import HomeAssistant - - -@pytest.fixture(name="mocked_json_bytes") -def mocked_json_bytes_fixture() -> Generator[Mock]: - """Mock json_bytes.""" - with patch( - "homeassistant.components.backup.manager.json_bytes", - return_value=b"{}", # Empty JSON - ) as mocked_json_bytes: - yield mocked_json_bytes - - -@pytest.fixture(name="mocked_tarfile") -def mocked_tarfile_fixture() -> Generator[Mock]: - """Mock tarfile.""" - with patch( - "homeassistant.components.backup.manager.SecureTarFile" - ) as mocked_tarfile: - yield mocked_tarfile - - -@pytest.fixture(name="mock_backup_generation") -def mock_backup_generation_fixture( - hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock -) -> Generator[None]: - """Mock backup generator.""" - - def _mock_iterdir(path: Path) -> list[Path]: - if not path.name.endswith("testing_config"): - return [] - return [ - Path("test.txt"), - Path(".DS_Store"), - Path(".storage"), - ] - - with ( - patch("pathlib.Path.iterdir", _mock_iterdir), - patch("pathlib.Path.stat", MagicMock(st_size=123)), - patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), - patch( - "pathlib.Path.is_dir", - lambda x: x.name == ".storage", - ), - patch( - "pathlib.Path.exists", - lambda x: x != Path(hass.config.path("backups")), - ), - patch( - "pathlib.Path.is_symlink", - lambda _: False, - ), - patch( - "pathlib.Path.mkdir", - MagicMock(), - ), - patch( - "homeassistant.components.backup.manager.HAVERSION", - "2025.1.0", - ), - ): - yield diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 42eb524e529..096df37d704 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -210,23 +210,16 @@ dict({ 'id': 1, 'result': dict({ - 'slug': '27f5c632', + 'date': '1970-01-01T00:00:00.000Z', + 'name': 'Test', + 'path': 'abc123.tar', + 'size': 0.0, + 'slug': 'abc123', }), 'success': True, 'type': 'result', }) # --- -# name: test_generate[without_hassio].1 - dict({ - 'event': dict({ - 'done': True, - 'stage': None, - 'success': True, - }), - 'id': 1, - 'type': 'event', - }) -# --- # name: test_info[with_hassio] dict({ 'error': dict({ diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 9d24964aedf..a3f70267643 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -2,7 +2,7 @@ from __future__ import annotations -import asyncio +from pathlib import Path from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch import aiohttp @@ -10,10 +10,7 @@ from multidict import CIMultiDict, CIMultiDictProxy import pytest from homeassistant.components.backup import BackupManager -from homeassistant.components.backup.manager import ( - BackupPlatformProtocol, - BackupProgress, -) +from homeassistant.components.backup.manager import BackupPlatformProtocol from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -23,30 +20,59 @@ from .common import TEST_BACKUP from tests.common import MockPlatform, mock_platform -async def _mock_backup_generation( - manager: BackupManager, mocked_json_bytes: Mock, mocked_tarfile: Mock -) -> None: +async def _mock_backup_generation(manager: BackupManager): """Mock backup generator.""" - progress: list[BackupProgress] = [] + def _mock_iterdir(path: Path) -> list[Path]: + if not path.name.endswith("testing_config"): + return [] + return [ + Path("test.txt"), + Path(".DS_Store"), + Path(".storage"), + ] - def on_progress(_progress: BackupProgress) -> None: - """Mock progress callback.""" - progress.append(_progress) + with ( + patch( + "homeassistant.components.backup.manager.SecureTarFile" + ) as mocked_tarfile, + patch("pathlib.Path.iterdir", _mock_iterdir), + patch("pathlib.Path.stat", MagicMock(st_size=123)), + patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), + patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), + patch( + "pathlib.Path.exists", + lambda x: x != manager.backup_dir, + ), + patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), + patch( + "pathlib.Path.mkdir", + MagicMock(), + ), + patch( + "homeassistant.components.backup.manager.json_bytes", + return_value=b"{}", # Empty JSON + ) as mocked_json_bytes, + patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ), + ): + await manager.async_create_backup() - assert manager.backup_task is None - await manager.async_create_backup(on_progress=on_progress) - assert manager.backup_task is not None - assert progress == [] - - await manager.backup_task - assert progress == [BackupProgress(done=True, stage=None, success=True)] - - assert mocked_json_bytes.call_count == 1 - backup_json_dict = mocked_json_bytes.call_args[0][0] - assert isinstance(backup_json_dict, dict) - assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"} - assert manager.backup_dir.as_posix() in str(mocked_tarfile.call_args_list[0][0][0]) + assert mocked_json_bytes.call_count == 1 + backup_json_dict = mocked_json_bytes.call_args[0][0] + assert isinstance(backup_json_dict, dict) + assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"} + assert manager.backup_dir.as_posix() in str( + mocked_tarfile.call_args_list[0][0][0] + ) async def _setup_mock_domain( @@ -150,26 +176,21 @@ async def test_getting_backup_that_does_not_exist( async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None: """Test generate backup.""" - event = asyncio.Event() manager = BackupManager(hass) - manager.backup_task = hass.async_create_task(event.wait()) + manager.backing_up = True with pytest.raises(HomeAssistantError, match="Backup already in progress"): - await manager.async_create_backup(on_progress=None) - event.set() + await manager.async_create_backup() -@pytest.mark.usefixtures("mock_backup_generation") async def test_async_create_backup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mocked_json_bytes: Mock, - mocked_tarfile: Mock, ) -> None: """Test generate backup.""" manager = BackupManager(hass) manager.loaded_backups = True - await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) + await _mock_backup_generation(manager) assert "Generated new backup with slug " in caplog.text assert "Creating backup directory" in caplog.text @@ -226,9 +247,7 @@ async def test_not_loading_bad_platforms( ) -async def test_exception_plaform_pre( - hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock -) -> None: +async def test_exception_plaform_pre(hass: HomeAssistant) -> None: """Test exception in pre step.""" manager = BackupManager(hass) manager.loaded_backups = True @@ -245,12 +264,10 @@ async def test_exception_plaform_pre( ) with pytest.raises(HomeAssistantError): - await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) + await _mock_backup_generation(manager) -async def test_exception_plaform_post( - hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock -) -> None: +async def test_exception_plaform_post(hass: HomeAssistant) -> None: """Test exception in post step.""" manager = BackupManager(hass) manager.loaded_backups = True @@ -267,7 +284,7 @@ async def test_exception_plaform_post( ) with pytest.raises(HomeAssistantError): - await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile) + await _mock_backup_generation(manager) async def test_loading_platforms_when_running_async_pre_backup_actions( diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 3e031f172ae..125ba8adaad 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -116,30 +115,29 @@ async def test_remove( @pytest.mark.parametrize( - ("with_hassio", "number_of_messages"), + "with_hassio", [ - pytest.param(True, 1, id="with_hassio"), - pytest.param(False, 2, id="without_hassio"), + pytest.param(True, id="with_hassio"), + pytest.param(False, id="without_hassio"), ], ) -@pytest.mark.usefixtures("mock_backup_generation") async def test_generate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, with_hassio: bool, - number_of_messages: int, ) -> None: """Test generating a backup.""" await setup_backup_integration(hass, with_hassio=with_hassio) client = await hass_ws_client(hass) - freezer.move_to("2024-11-13 12:01:00+01:00") await hass.async_block_till_done() - await client.send_json_auto_id({"type": "backup/generate"}) - for _ in range(number_of_messages): + with patch( + "homeassistant.components.backup.manager.BackupManager.async_create_backup", + return_value=TEST_BACKUP, + ): + await client.send_json_auto_id({"type": "backup/generate"}) assert snapshot == await client.receive_json() diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index 29fb9d61c4e..ba5cf35c52f 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -139,46 +139,42 @@ async def init_test_integration( return test_camera -@pytest.mark.usefixtures("mock_camera", "mock_stream_source") +@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") async def test_async_register_webrtc_provider( hass: HomeAssistant, ) -> None: """Test registering a WebRTC provider.""" + await async_setup_component(hass, "camera", {}) + camera = get_camera_from_entity_id(hass, "camera.demo_camera") - assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} + assert camera.frontend_stream_type is StreamType.HLS provider = SomeTestProvider() unregister = async_register_webrtc_provider(hass, provider) await hass.async_block_till_done() - assert camera.camera_capabilities.frontend_stream_types == { - StreamType.HLS, - StreamType.WEB_RTC, - } + assert camera.frontend_stream_type is StreamType.WEB_RTC # Mark stream as unsupported provider._is_supported = False # Manually refresh the provider await camera.async_refresh_providers() - assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} + assert camera.frontend_stream_type is StreamType.HLS # Mark stream as supported provider._is_supported = True # Manually refresh the provider await camera.async_refresh_providers() - assert camera.camera_capabilities.frontend_stream_types == { - StreamType.HLS, - StreamType.WEB_RTC, - } + assert camera.frontend_stream_type is StreamType.WEB_RTC unregister() await hass.async_block_till_done() - assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} + assert camera.frontend_stream_type is StreamType.HLS -@pytest.mark.usefixtures("mock_camera", "mock_stream_source") +@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") async def test_async_register_webrtc_provider_twice( hass: HomeAssistant, register_test_provider: SomeTestProvider, @@ -196,11 +192,13 @@ async def test_async_register_webrtc_provider_camera_not_loaded( async_register_webrtc_provider(hass, SomeTestProvider()) -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source") async def test_async_register_ice_server( hass: HomeAssistant, ) -> None: """Test registering an ICE server.""" + await async_setup_component(hass, "camera", {}) + # Clear any existing ICE servers hass.data[DATA_ICE_SERVERS].clear() @@ -218,7 +216,7 @@ async def test_async_register_ice_server( unregister = async_register_ice_servers(hass, get_ice_servers) assert not called - camera = get_camera_from_entity_id(hass, "camera.async") + camera = get_camera_from_entity_id(hass, "camera.demo_camera") config = camera.async_get_webrtc_client_configuration() assert config.configuration.ice_servers == [ @@ -279,7 +277,7 @@ async def test_async_register_ice_server( assert config.configuration.ice_servers == [] -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_get_client_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -288,7 +286,7 @@ async def test_ws_get_client_config( client = await hass_ws_client(hass) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} ) msg = await client.receive_json() @@ -322,7 +320,7 @@ async def test_ws_get_client_config( async_register_ice_servers(hass, get_ice_server) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} ) msg = await client.receive_json() @@ -372,7 +370,7 @@ async def test_ws_get_client_config_sync_offer( } -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_get_client_config_custom_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -386,7 +384,7 @@ async def test_ws_get_client_config_custom_config( client = await hass_ws_client(hass) await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.async"} + {"type": "camera/webrtc/get_client_config", "entity_id": "camera.demo_camera"} ) msg = await client.receive_json() @@ -437,7 +435,7 @@ def mock_rtsp_to_webrtc_fixture(hass: HomeAssistant) -> Generator[Mock]: unsub() -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_websocket_webrtc_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -446,7 +444,7 @@ async def test_websocket_webrtc_offer( await client.send_json_auto_id( { "type": "camera/webrtc/offer", - "entity_id": "camera.async", + "entity_id": "camera.demo_camera", "offer": WEBRTC_OFFER, } ) @@ -557,11 +555,11 @@ async def test_websocket_webrtc_offer_webrtc_provider( mock_async_close_session.assert_called_once_with(session_id) +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_websocket_webrtc_offer_invalid_entity( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC with a camera entity that does not exist.""" - await async_setup_component(hass, "camera", {}) client = await hass_ws_client(hass) await client.send_json_auto_id( { @@ -580,7 +578,7 @@ async def test_websocket_webrtc_offer_invalid_entity( } -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_websocket_webrtc_offer_missing_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -607,6 +605,7 @@ async def test_websocket_webrtc_offer_missing_offer( (TimeoutError(), "Timeout handling WebRTC offer"), ], ) +@pytest.mark.usefixtures("mock_camera_webrtc_frontendtype_only") async def test_websocket_webrtc_offer_failure( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -950,7 +949,7 @@ async def test_rtsp_to_webrtc_offer_not_accepted( unsub() -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_webrtc_candidate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -958,13 +957,13 @@ async def test_ws_webrtc_candidate( client = await hass_ws_client(hass) session_id = "session_id" candidate = "candidate" - with patch.object( - get_camera_from_entity_id(hass, "camera.async"), "async_on_webrtc_candidate" + with patch( + "homeassistant.components.camera.Camera.async_on_webrtc_candidate" ) as mock_on_webrtc_candidate: await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.async", + "entity_id": "camera.demo_camera", "session_id": session_id, "candidate": candidate, } @@ -977,7 +976,7 @@ async def test_ws_webrtc_candidate( ) -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_webrtc_candidate_not_supported( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -986,7 +985,7 @@ async def test_ws_webrtc_candidate_not_supported( await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.sync", + "entity_id": "camera.demo_camera", "session_id": "session_id", "candidate": "candidate", } @@ -1029,11 +1028,11 @@ async def test_ws_webrtc_candidate_webrtc_provider( ) +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_webrtc_candidate_invalid_entity( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test ws WebRTC candidate command with a camera entity that does not exist.""" - await async_setup_component(hass, "camera", {}) client = await hass_ws_client(hass) await client.send_json_auto_id( { @@ -1053,7 +1052,7 @@ async def test_ws_webrtc_candidate_invalid_entity( } -@pytest.mark.usefixtures("mock_test_webrtc_cameras") +@pytest.mark.usefixtures("mock_camera_webrtc") async def test_ws_webrtc_canidate_missing_candidate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1062,7 +1061,7 @@ async def test_ws_webrtc_canidate_missing_candidate( await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.async", + "entity_id": "camera.demo_camera", "session_id": "session_id", } ) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 363d39a2e63..5535ec3b976 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -26,12 +26,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - FlowContext, - FlowHandler, - FlowManager, - FlowResultType, -) +from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType from homeassistant.helpers.translation import async_get_translations if TYPE_CHECKING: @@ -562,12 +557,12 @@ def _validate_translation_placeholders( description_placeholders is None or placeholder not in description_placeholders ): - ignore_translations[full_key] = ( + pytest.fail( f"Description not found for placeholder `{placeholder}` in {full_key}" ) -async def _validate_translation( +async def _ensure_translation_exists( hass: HomeAssistant, ignore_translations: dict[str, StoreInfo], category: str, @@ -593,7 +588,7 @@ async def _validate_translation( ignore_translations[full_key] = "used" return - ignore_translations[full_key] = ( + pytest.fail( f"Translation not found for {component}: `{category}.{key}`. " f"Please add to homeassistant/components/{component}/strings.json" ) @@ -609,106 +604,84 @@ def ignore_translations() -> str | list[str]: return [] -async def _check_config_flow_result_translations( - manager: FlowManager, - flow: FlowHandler, - result: FlowResult[FlowContext, str], - ignore_translations: dict[str, str], -) -> None: - if isinstance(manager, ConfigEntriesFlowManager): - category = "config" - integration = flow.handler - elif isinstance(manager, OptionsFlowManager): - category = "options" - integration = flow.hass.config_entries.async_get_entry(flow.handler).domain - else: - return - - # Check if this flow has been seen before - # Gets set to False on first run, and to True on subsequent runs - setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before")) - - if result["type"] is FlowResultType.FORM: - if step_id := result.get("step_id"): - # neither title nor description are required - # - title defaults to integration name - # - description is optional - for header in ("title", "description"): - await _validate_translation( - flow.hass, - ignore_translations, - category, - integration, - f"step.{step_id}.{header}", - result["description_placeholders"], - translation_required=False, - ) - if errors := result.get("errors"): - for error in errors.values(): - await _validate_translation( - flow.hass, - ignore_translations, - category, - integration, - f"error.{error}", - result["description_placeholders"], - ) - return - - if result["type"] is FlowResultType.ABORT: - # We don't need translations for a discovery flow which immediately - # aborts, since such flows won't be seen by users - if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES: - return - await _validate_translation( - flow.hass, - ignore_translations, - category, - integration, - f"abort.{result["reason"]}", - result["description_placeholders"], - ) - - @pytest.fixture(autouse=True) -def check_translations(ignore_translations: str | list[str]) -> Generator[None]: - """Check that translation requirements are met. - - Current checks: - - data entry flow results (ConfigFlow/OptionsFlow) - """ +def check_config_translations(ignore_translations: str | list[str]) -> Generator[None]: + """Ensure config_flow translations are available.""" if not isinstance(ignore_translations, list): ignore_translations = [ignore_translations] _ignore_translations = {k: "unused" for k in ignore_translations} + _original = FlowManager._async_handle_step - # Keep reference to original functions - _original_flow_manager_async_handle_step = FlowManager._async_handle_step - - # Prepare override functions - async def _flow_manager_async_handle_step( + async def _async_handle_step( self: FlowManager, flow: FlowHandler, *args ) -> FlowResult: - result = await _original_flow_manager_async_handle_step(self, flow, *args) - await _check_config_flow_result_translations( - self, flow, result, _ignore_translations - ) + result = await _original(self, flow, *args) + if isinstance(self, ConfigEntriesFlowManager): + category = "config" + component = flow.handler + elif isinstance(self, OptionsFlowManager): + category = "options" + component = flow.hass.config_entries.async_get_entry(flow.handler).domain + else: + return result + + # Check if this flow has been seen before + # Gets set to False on first run, and to True on subsequent runs + setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before")) + + if result["type"] is FlowResultType.FORM: + if step_id := result.get("step_id"): + # neither title nor description are required + # - title defaults to integration name + # - description is optional + for header in ("title", "description"): + await _ensure_translation_exists( + flow.hass, + _ignore_translations, + category, + component, + f"step.{step_id}.{header}", + result["description_placeholders"], + translation_required=False, + ) + if errors := result.get("errors"): + for error in errors.values(): + await _ensure_translation_exists( + flow.hass, + _ignore_translations, + category, + component, + f"error.{error}", + result["description_placeholders"], + ) + return result + + if result["type"] is FlowResultType.ABORT: + # We don't need translations for a discovery flow which immediately + # aborts, since such flows won't be seen by users + if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES: + return result + await _ensure_translation_exists( + flow.hass, + _ignore_translations, + category, + component, + f"abort.{result["reason"]}", + result["description_placeholders"], + ) + return result - # Use override functions with patch( "homeassistant.data_entry_flow.FlowManager._async_handle_step", - _flow_manager_async_handle_step, + _async_handle_step, ): yield - # Run final checks unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"] if unused_ignore: pytest.fail( f"Unused ignore translations: {', '.join(unused_ignore)}. " "Please remove them from the ignore_translations fixture." ) - for description in _ignore_translations.values(): - if description not in {"used", "unused"}: - pytest.fail(description) diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index d9d859113f8..08aca43aba5 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -639,7 +639,7 @@ 'details': dict({ 'brightness': dict({ 'name': 'brightness', - 'text': '100', + 'text': '100%', 'value': 100, }), 'name': dict({ @@ -654,7 +654,7 @@ 'match': True, 'sentence_template': '[] brightness [to] ', 'slots': dict({ - 'brightness': '100', + 'brightness': '100%', 'name': 'test light', }), 'source': 'builtin', diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 3c6b463670a..9f54671d8a1 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -770,8 +770,8 @@ async def test_error_no_device_on_floor_exposed( ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "turn on test light on the ground floor", None, Context(), None @@ -838,8 +838,8 @@ async def test_error_no_domain(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "turn on the fans", None, Context(), None @@ -873,8 +873,8 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "turn on the fans", None, Context(), None @@ -1047,8 +1047,8 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "open the windows", None, Context(), None @@ -1096,8 +1096,8 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "open all the windows", None, Context(), None @@ -1207,8 +1207,8 @@ async def test_error_no_device_class_on_floor_exposed( ) with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=recognize_result, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], ): result = await conversation.async_converse( hass, "open ground floor windows", None, Context(), None @@ -1229,8 +1229,8 @@ async def test_error_no_device_class_on_floor_exposed( async def test_error_no_intent(hass: HomeAssistant) -> None: """Test response with an intent match failure.""" with patch( - "homeassistant.components.conversation.default_agent.recognize_best", - return_value=None, + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[], ): result = await conversation.async_converse( hass, "do something", None, Context(), None diff --git a/tests/components/conversation/test_trace.py b/tests/components/conversation/test_trace.py index 7c00b9a80b2..59cd10d2510 100644 --- a/tests/components/conversation/test_trace.py +++ b/tests/components/conversation/test_trace.py @@ -56,7 +56,7 @@ async def test_converation_trace( "intent_name": "HassListAddItem", "slots": { "name": "Shopping List", - "item": "apples", + "item": "apples ", }, } diff --git a/tests/components/dhcp/conftest.py b/tests/components/dhcp/conftest.py new file mode 100644 index 00000000000..b0fa3f573c5 --- /dev/null +++ b/tests/components/dhcp/conftest.py @@ -0,0 +1,21 @@ +"""Tests for the dhcp integration.""" + +import os +import pathlib + + +def pytest_sessionstart(session): + """Try to avoid flaky FileExistsError in CI. + + Called after the Session object has been created and + before performing collection and entering the run test loop. + + This is needed due to a race condition in scapy v2.6.0 + See https://github.com/secdev/scapy/pull/4558 + + Can be removed when scapy 2.6.1 is released. + """ + for sub_dir in (".cache", ".config"): + path = pathlib.Path(os.path.join(os.path.expanduser("~"), sub_dir)) + if not path.exists(): + path.mkdir(mode=0o700, exist_ok=True) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 84f1b240b88..e3fae8c083e 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -10,7 +10,6 @@ from fritzconnection.core.exceptions import ( ) import pytest -from homeassistant.components import ssdp from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -23,6 +22,7 @@ from homeassistant.components.fritz.const import ( ERROR_UNKNOWN, FRITZ_AUTH_EXCEPTIONS, ) +from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -644,7 +644,7 @@ async def test_ssdp_already_in_progress_host( MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) MOCK_NO_UNIQUE_ID.upnp = MOCK_NO_UNIQUE_ID.upnp.copy() - del MOCK_NO_UNIQUE_ID.upnp[ssdp.ATTR_UPNP_UDN] + del MOCK_NO_UNIQUE_ID.upnp[ATTR_UPNP_UDN] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) @@ -737,23 +737,3 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_OLD_DISCOVERY: False, CONF_CONSIDER_HOME: 37, } - - -async def test_ssdp_ipv6_link_local(hass: HomeAssistant) -> None: - """Test ignoring ipv6-link-local while ssdp discovery.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="https://[fe80::1ff:fe23:4567:890a]:12345/test", - upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "fake_name", - ssdp.ATTR_UPNP_UDN: "uuid:only-a-test", - }, - ), - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "ignore_ip6_link_local" diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index abb139b89bf..42b363b2324 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -3,11 +3,9 @@ from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch -from awesomeversion import AwesomeVersion from go2rtc_client.rest import _StreamClient, _WebRTCClient import pytest -from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION from homeassistant.components.go2rtc.server import Server GO2RTC_PATH = "homeassistant.components.go2rtc" @@ -25,9 +23,7 @@ def rest_client() -> Generator[AsyncMock]: client = mock_client.return_value client.streams = streams = Mock(spec_set=_StreamClient) streams.list.return_value = {} - client.validate_server_version = AsyncMock( - return_value=AwesomeVersion(RECOMMENDED_VERSION) - ) + client.validate_server_version = AsyncMock() client.webrtc = Mock(spec_set=_WebRTCClient) yield client diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 0f1cac6942d..9388110366e 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -6,7 +6,6 @@ from typing import NamedTuple from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError -from awesomeversion import AwesomeVersion from go2rtc_client import Stream from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.models import Producer @@ -37,12 +36,10 @@ from homeassistant.components.go2rtc.const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, - RECOMMENDED_VERSION, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -202,7 +199,6 @@ async def init_test_integration( async def _test_setup_and_signaling( hass: HomeAssistant, - issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, config: ConfigType, @@ -215,7 +211,6 @@ async def _test_setup_and_signaling( assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done(wait_background_tasks=True) - assert issue_registry.async_get_issue(DOMAIN, "recommended_version") is None config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].state == ConfigEntryState.LOADED @@ -311,7 +306,6 @@ async def _test_setup_and_signaling( @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) async def test_setup_go_binary( hass: HomeAssistant, - issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: AsyncMock, @@ -330,13 +324,7 @@ async def test_setup_go_binary( server_start.assert_called_once() await _test_setup_and_signaling( - hass, - issue_registry, - rest_client, - ws_client, - config, - after_setup, - init_test_integration, + hass, rest_client, ws_client, config, after_setup, init_test_integration ) await hass.async_stop() @@ -352,9 +340,8 @@ async def test_setup_go_binary( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup( +async def test_setup_go( hass: HomeAssistant, - issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: Mock, @@ -372,13 +359,7 @@ async def test_setup( server.assert_not_called() await _test_setup_and_signaling( - hass, - issue_registry, - rest_client, - ws_client, - config, - after_setup, - init_test_integration, + hass, rest_client, ws_client, config, after_setup, init_test_integration ) mock_get_binary.assert_not_called() @@ -730,30 +711,3 @@ async def test_config_entry_remove(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert not await hass.config_entries.async_setup(config_entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - - -@pytest.mark.parametrize("config", [{DOMAIN: {CONF_URL: "http://localhost:1984"}}]) -@pytest.mark.usefixtures("server") -async def test_setup_with_recommended_version_repair( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - rest_client: AsyncMock, - config: ConfigType, -) -> None: - """Test setup integration entry fails.""" - rest_client.validate_server_version.return_value = AwesomeVersion("1.9.5") - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done(wait_background_tasks=True) - - # Verify the issue is created - issue = issue_registry.async_get_issue(DOMAIN, "recommended_version") - assert issue - assert issue.is_fixable is False - assert issue.is_persistent is False - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.issue_id == "recommended_version" - assert issue.translation_key == "recommended_version" - assert issue.translation_placeholders == { - "recommended_version": RECOMMENDED_VERSION, - "current_version": "1.9.5", - } diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 0202cec05b9..2814e1558d1 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,6 +1,5 @@ """Test helpers for Husqvarna Automower.""" -import asyncio from collections.abc import Generator import time from unittest.mock import AsyncMock, patch @@ -102,17 +101,10 @@ async def setup_credentials(hass: HomeAssistant) -> None: def mock_automower_client(values) -> Generator[AsyncMock]: """Mock a Husqvarna Automower client.""" - async def listen() -> None: - """Mock listen.""" - listen_block = asyncio.Event() - await listen_block.wait() - pytest.fail("Listen was not cancelled!") - mock = AsyncMock(spec=AutomowerSession) mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) mock.commands = AsyncMock(spec_set=_MowerCommands) mock.get_status.return_value = values - mock.start_listening = AsyncMock(side_effect=listen) with patch( "homeassistant.components.husqvarna_automower.AutomowerSession", diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index ae688571d2c..ca0c2a04af1 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -1,16 +1,14 @@ """Tests for init module.""" -from asyncio import Event -from datetime import datetime +from datetime import datetime, timedelta import http import time -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, - TimeoutException, ) from aioautomower.model import MowerAttributes, WorkArea from freezegun.api import FrozenDateTimeFactory @@ -129,77 +127,28 @@ async def test_update_failed( assert entry.state is entry_state -@patch( - "homeassistant.components.husqvarna_automower.coordinator.DEFAULT_RECONNECT_TIME", 0 -) -@pytest.mark.parametrize( - ("method_path", "exception", "error_msg"), - [ - ( - ["auth", "websocket_connect"], - HusqvarnaWSServerHandshakeError, - "Failed to connect to websocket.", - ), - ( - ["start_listening"], - TimeoutException, - "Failed to listen to websocket.", - ), - ], -) async def test_websocket_not_available( hass: HomeAssistant, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, - method_path: list[str], - exception: type[Exception], - error_msg: str, ) -> None: - """Test trying to reload the websocket.""" - calls = [] - mock_called = Event() - mock_stall = Event() - - async def mock_function(): - mock_called.set() - await mock_stall.wait() - # Raise the first time the method is awaited - if not calls: - calls.append(None) - raise exception("Boom") - if mock_side_effect: - await mock_side_effect() - - # Find the method to mock - mock = mock_automower_client - for itm in method_path: - mock = getattr(mock, itm) - mock_side_effect = mock.side_effect - mock.side_effect = mock_function - - # Setup integration and verify log error message + """Test trying reload the websocket.""" + mock_automower_client.start_listening.side_effect = HusqvarnaWSServerHandshakeError( + "Boom" + ) await setup_integration(hass, mock_config_entry) - await mock_called.wait() - mock_called.clear() - # Allow the exception to be raised - mock_stall.set() - assert mock.call_count == 1 + assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text + assert mock_automower_client.auth.websocket_connect.call_count == 1 + assert mock_automower_client.start_listening.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.LOADED + freezer.tick(timedelta(seconds=2)) + async_fire_time_changed(hass) await hass.async_block_till_done() - assert f"{error_msg} Trying to reconnect: Boom" in caplog.text - - # Simulate a successful connection - caplog.clear() - await mock_called.wait() - mock_called.clear() - await hass.async_block_till_done() - assert mock.call_count == 2 - assert "Trying to reconnect: Boom" not in caplog.text - - # Simulate hass shutting down - await hass.async_stop() - assert mock.call_count == 2 + assert mock_automower_client.auth.websocket_connect.call_count == 2 + assert mock_automower_client.start_listening.call_count == 2 + assert mock_config_entry.state is ConfigEntryState.LOADED async def test_device_info( diff --git a/tests/components/huum/conftest.py b/tests/components/huum/conftest.py new file mode 100644 index 00000000000..da66cc54b72 --- /dev/null +++ b/tests/components/huum/conftest.py @@ -0,0 +1,6 @@ +"""Skip test collection for Python 3.13.""" + +import sys + +if sys.version_info >= (3, 13): + collect_ignore_glob = ["test_*.py"] diff --git a/tests/components/sky_remote/__init__.py b/tests/components/sky_remote/__init__.py deleted file mode 100644 index 83d68330d5b..00000000000 --- a/tests/components/sky_remote/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Tests for the Sky Remote component.""" - -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_mock_entry(hass: HomeAssistant, entry: MockConfigEntry): - """Initialize a mock config entry.""" - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() diff --git a/tests/components/sky_remote/conftest.py b/tests/components/sky_remote/conftest.py deleted file mode 100644 index d6c453d81f7..00000000000 --- a/tests/components/sky_remote/conftest.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Test mocks and fixtures.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from homeassistant.components.sky_remote.const import DEFAULT_PORT, DOMAIN -from homeassistant.const import CONF_HOST, CONF_PORT - -from tests.common import MockConfigEntry - -SAMPLE_CONFIG = {CONF_HOST: "example.com", CONF_PORT: DEFAULT_PORT} - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Mock a config entry.""" - return MockConfigEntry(domain=DOMAIN, data=SAMPLE_CONFIG) - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Stub out setup function.""" - with patch( - "homeassistant.components.sky_remote.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_remote_control(request: pytest.FixtureRequest) -> Generator[MagicMock]: - """Mock skyboxremote library.""" - with ( - patch( - "homeassistant.components.sky_remote.RemoteControl" - ) as mock_remote_control, - patch( - "homeassistant.components.sky_remote.config_flow.RemoteControl", - mock_remote_control, - ), - ): - mock_remote_control._instance_mock = MagicMock(host="example.com") - mock_remote_control._instance_mock.check_connectable = AsyncMock(True) - mock_remote_control.return_value = mock_remote_control._instance_mock - yield mock_remote_control diff --git a/tests/components/sky_remote/test_config_flow.py b/tests/components/sky_remote/test_config_flow.py deleted file mode 100644 index aaeda20788c..00000000000 --- a/tests/components/sky_remote/test_config_flow.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Test the Sky Remote config flow.""" - -from __future__ import annotations - -from unittest.mock import AsyncMock - -import pytest -from skyboxremote import LEGACY_PORT, SkyBoxConnectionError - -from homeassistant.components.sky_remote.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from .conftest import SAMPLE_CONFIG - - -async def test_user_flow( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_remote_control -) -> None: - """Test we can setup an entry.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] is FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == SAMPLE_CONFIG - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_device_exists_abort( - hass: HomeAssistant, mock_config_entry, mock_remote_control -) -> None: - """Test we abort flow if device already configured.""" - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: mock_config_entry.data[CONF_HOST]}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.parametrize("mock_remote_control", [LEGACY_PORT], indirect=True) -async def test_user_flow_legacy_device( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_remote_control, -) -> None: - """Test we can setup an entry with a legacy port.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] is FlowResultType.FORM - - async def mock_check_connectable(): - if mock_remote_control.call_args[0][1] == LEGACY_PORT: - return True - raise SkyBoxConnectionError("Wrong port") - - mock_remote_control._instance_mock.check_connectable = mock_check_connectable - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == {**SAMPLE_CONFIG, CONF_PORT: LEGACY_PORT} - - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize("mock_remote_control", [6], indirect=True) -async def test_user_flow_unconnectable( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_remote_control, -) -> None: - """Test we can setup an entry.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] is FlowResultType.FORM - - mock_remote_control._instance_mock.check_connectable = AsyncMock( - side_effect=SkyBoxConnectionError("Example") - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} - - assert len(mock_setup_entry.mock_calls) == 0 - - mock_remote_control._instance_mock.check_connectable = AsyncMock(True) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: SAMPLE_CONFIG[CONF_HOST]}, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == SAMPLE_CONFIG - - assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sky_remote/test_init.py b/tests/components/sky_remote/test_init.py deleted file mode 100644 index fe316baa6bf..00000000000 --- a/tests/components/sky_remote/test_init.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Tests for the Sky Remote component.""" - -from unittest.mock import AsyncMock - -from skyboxremote import SkyBoxConnectionError - -from homeassistant.components.sky_remote.const import DEFAULT_PORT, DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from . import setup_mock_entry - -from tests.common import MockConfigEntry - - -async def test_setup_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_remote_control, - device_registry: dr.DeviceRegistry, -) -> None: - """Test successful setup of entry.""" - await setup_mock_entry(hass, mock_config_entry) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - mock_remote_control.assert_called_once_with("example.com", DEFAULT_PORT) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.entry_id)} - ) - assert device_entry is not None - assert device_entry.name == "example.com" - - -async def test_setup_unconnectable_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_remote_control, -) -> None: - """Test unsuccessful setup of entry.""" - mock_remote_control._instance_mock.check_connectable = AsyncMock( - side_effect=SkyBoxConnectionError() - ) - - await setup_mock_entry(hass, mock_config_entry) - - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_unload_entry( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_remote_control -) -> None: - """Test unload an entry.""" - await setup_mock_entry(hass, mock_config_entry) - - assert mock_config_entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sky_remote/test_remote.py b/tests/components/sky_remote/test_remote.py deleted file mode 100644 index 301375bc039..00000000000 --- a/tests/components/sky_remote/test_remote.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Test sky_remote remote.""" - -import pytest - -from homeassistant.components.remote import ( - ATTR_COMMAND, - DOMAIN as REMOTE_DOMAIN, - SERVICE_SEND_COMMAND, -) -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError - -from . import setup_mock_entry - -ENTITY_ID = "remote.example_com" - - -async def test_send_command( - hass: HomeAssistant, mock_config_entry, mock_remote_control -) -> None: - """Test "send_command" method.""" - await setup_mock_entry(hass, mock_config_entry) - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_SEND_COMMAND, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["sky"]}, - blocking=True, - ) - mock_remote_control._instance_mock.send_keys.assert_called_once_with(["sky"]) - - -async def test_send_invalid_command( - hass: HomeAssistant, mock_config_entry, mock_remote_control -) -> None: - """Test "send_command" method.""" - await setup_mock_entry(hass, mock_config_entry) - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - REMOTE_DOMAIN, - SERVICE_SEND_COMMAND, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["apple"]}, - blocking=True, - ) - mock_remote_control._instance_mock.send_keys.assert_not_called() diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index e3abb3c98df..149e08014ac 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -100,6 +100,10 @@ async def test_form_invalid_auth( assert result["errors"] == {"base": "invalid_auth"} +@pytest.mark.parametrize( # Remove when translations fixed + "ignore_translations", + ["component.srp_energy.config.abort.unknown"], +) async def test_form_unknown_error( hass: HomeAssistant, mock_srp_energy_config_flow: MagicMock, diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 6abc544c92a..d930aafbdfb 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -192,6 +192,10 @@ async def test_two_factor_request_success( assert len(mock_two_factor_request.mock_calls) == 1 +@pytest.mark.parametrize( # Remove when translations fixed + "ignore_translations", + ["component.subaru.config.abort.two_factor_request_failed"], +) async def test_two_factor_request_fail( hass: HomeAssistant, two_factor_start_form ) -> None: diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 87ba46a4ced..1382c5c2569 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -21,7 +21,7 @@ import zigpy.types from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf -from homeassistant.components.hassio import AddonError, AddonState +from homeassistant.components.hassio import AddonState from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.const import ( @@ -1878,23 +1878,10 @@ async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: ) -async def test_config_flow_ports_no_hassio(hass: HomeAssistant) -> None: - """Test config flow serial port name when this is not a hassio install.""" - - with ( - patch("homeassistant.components.zha.config_flow.is_hassio", return_value=False), - patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), - ): - ports = await config_flow.list_serial_ports(hass) - - assert ports == [] - - async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> None: """Test config flow serial port name for multiprotocol add-on.""" with ( - patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), patch( "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" ) as async_get_addon_info, @@ -1902,28 +1889,16 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> ): async_get_addon_info.return_value.state = AddonState.RUNNING async_get_addon_info.return_value.hostname = "core-silabs-multiprotocol" - ports = await config_flow.list_serial_ports(hass) - assert len(ports) == 1 - assert ports[0].description == "Multiprotocol add-on" - assert ports[0].manufacturer == "Nabu Casa" - assert ports[0].device == "socket://core-silabs-multiprotocol:9999" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) - -async def test_config_flow_port_no_multiprotocol(hass: HomeAssistant) -> None: - """Test config flow serial port listing when addon info fails to load.""" - - with ( - patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), - patch( - "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info", - side_effect=AddonError, - ), - patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), - ): - ports = await config_flow.list_serial_ports(hass) - - assert ports == [] + assert ( + result["data_schema"].schema["path"].container[0] + == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" + ) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))