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/ci.yaml b/.github/workflows/ci.yaml index fa05f6082a2..778ab8b0647 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,7 @@ env: MYPY_CACHE_VERSION: 9 HA_SHORT_VERSION: "2024.12" DEFAULT_PYTHON: "3.12" - ALL_PYTHON_VERSIONS: "['3.12', '3.13']" + ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 48e37717232..176e010c5b9 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.0 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.27.3 + uses: github/codeql-action/analyze@v3.27.0 with: category: "/language:python" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b9f54bba081..0c8df57d5a2 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -112,7 +112,7 @@ jobs: strategy: fail-fast: false matrix: - abi: ["cp312", "cp313"] + abi: ["cp312"] arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository @@ -135,14 +135,14 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2024.11.0 + uses: home-assistant/wheels@2024.07.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" + apk: "libffi-dev;openssl-dev;yaml-dev;nasm" skip-binary: aiohttp;multidict;yarl constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -156,7 +156,7 @@ jobs: strategy: fail-fast: false matrix: - abi: ["cp312", "cp313"] + abi: ["cp312"] arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository @@ -198,7 +198,6 @@ jobs: split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt - name: Create requirements for cython<3 - if: matrix.abi == 'cp312' run: | # Some dependencies still require 'cython<3' # and don't yet use isolated build environments. @@ -209,8 +208,7 @@ jobs: cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt - name: Build wheels (old cython) - uses: home-assistant/wheels@2024.11.0 - if: matrix.abi == 'cp312' + uses: home-assistant/wheels@2024.07.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -225,43 +223,43 @@ jobs: pip: "'cython<3'" - name: Build wheels (part 1) - uses: home-assistant/wheels@2024.11.0 + uses: home-assistant/wheels@2024.07.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" + skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtaa" - name: Build wheels (part 2) - uses: home-assistant/wheels@2024.11.0 + uses: home-assistant/wheels@2024.07.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" + skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtab" - name: Build wheels (part 3) - uses: home-assistant/wheels@2024.11.0 + uses: home-assistant/wheels@2024.07.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" + skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtac" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56fbabe8087..f89dadda43d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.7.2 hooks: - id: ruff args: @@ -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/Dockerfile b/Dockerfile index 15574192093..903a121c032 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ RUN \ "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \ esac \ - && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ + && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && chmod +x /bin/go2rtc \ # Verify go2rtc can be executed && go2rtc --version diff --git a/Dockerfile.dev b/Dockerfile.dev index 48f582a1581..d05c6df425c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -35,9 +35,6 @@ RUN \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Add go2rtc binary -COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc - # Install uv RUN pip3 install uv 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/http.py b/homeassistant/components/backup/http.py index 42693035bd3..4cc4e61c9e4 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -2,26 +2,23 @@ from __future__ import annotations -import asyncio from http import HTTPStatus -from typing import cast -from aiohttp import BodyPartReader from aiohttp.hdrs import CONTENT_DISPOSITION from aiohttp.web import FileResponse, Request, Response -from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify -from .const import DATA_MANAGER +from .const import DOMAIN +from .manager import BaseBackupManager @callback def async_register_http_views(hass: HomeAssistant) -> None: """Register the http views.""" hass.http.register_view(DownloadBackupView) - hass.http.register_view(UploadBackupView) class DownloadBackupView(HomeAssistantView): @@ -39,7 +36,7 @@ class DownloadBackupView(HomeAssistantView): if not request["hass_user"].is_admin: return Response(status=HTTPStatus.UNAUTHORIZED) - manager = request.app[KEY_HASS].data[DATA_MANAGER] + manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN] backup = await manager.async_get_backup(slug=slug) if backup is None or not backup.path.exists(): @@ -51,29 +48,3 @@ class DownloadBackupView(HomeAssistantView): CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" }, ) - - -class UploadBackupView(HomeAssistantView): - """Generate backup view.""" - - url = "/api/backup/upload" - name = "api:backup:upload" - - @require_admin - async def post(self, request: Request) -> Response: - """Upload a backup file.""" - manager = request.app[KEY_HASS].data[DATA_MANAGER] - reader = await request.multipart() - contents = cast(BodyPartReader, await reader.next()) - - try: - await manager.async_receive_backup(contents=contents) - except OSError as err: - return Response( - body=f"Can't write backup file {err}", - status=HTTPStatus.INTERNAL_SERVER_ERROR, - ) - except asyncio.CancelledError: - return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR) - - return Response(status=HTTPStatus.CREATED) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index ddc0a1eac3f..b3cb69861b9 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -4,21 +4,16 @@ from __future__ import annotations import abc import asyncio -from collections.abc import Callable from dataclasses import asdict, dataclass import hashlib import io import json from pathlib import Path -from queue import SimpleQueue -import shutil import tarfile from tarfile import TarError -from tempfile import TemporaryDirectory import time from typing import Any, Protocol, cast -import aiohttp from securetar import SecureTarFile, atomic_contents_add from homeassistant.backup_restore import RESTORE_BACKUP_FILE @@ -35,13 +30,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 +45,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 +61,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] = {} @@ -147,15 +126,10 @@ class BaseBackupManager(abc.ABC): @abc.abstractmethod async def async_restore_backup(self, slug: str, **kwargs: Any) -> None: - """Restore a backup.""" + """Restpre 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 @@ -173,15 +147,6 @@ class BaseBackupManager(abc.ABC): async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None: """Remove a backup.""" - @abc.abstractmethod - async def async_receive_backup( - self, - *, - contents: aiohttp.BodyPartReader, - **kwargs: Any, - ) -> None: - """Receive and store a backup file from upload.""" - class BackupManager(BaseBackupManager): """Backup manager for the Backup integration.""" @@ -257,93 +222,17 @@ class BackupManager(BaseBackupManager): LOGGER.debug("Removed backup located at %s", backup.path) self.backups.pop(slug) - async def async_receive_backup( - self, - *, - contents: aiohttp.BodyPartReader, - **kwargs: Any, - ) -> None: - """Receive and store a backup file from upload.""" - queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = ( - SimpleQueue() - ) - temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory) - target_temp_file = Path( - temp_dir_handler.name, contents.filename or "backup.tar" - ) - - def _sync_queue_consumer() -> None: - with target_temp_file.open("wb") as file_handle: - while True: - if (_chunk_future := queue.get()) is None: - break - _chunk, _future = _chunk_future - if _future is not None: - self.hass.loop.call_soon_threadsafe(_future.set_result, None) - file_handle.write(_chunk) - - fut: asyncio.Future[None] | None = None - try: - fut = self.hass.async_add_executor_job(_sync_queue_consumer) - megabytes_sending = 0 - while chunk := await contents.read_chunk(BUF_SIZE): - megabytes_sending += 1 - if megabytes_sending % 5 != 0: - queue.put_nowait((chunk, None)) - continue - - chunk_future = self.hass.loop.create_future() - queue.put_nowait((chunk, chunk_future)) - await asyncio.wait( - (fut, chunk_future), - return_when=asyncio.FIRST_COMPLETED, - ) - if fut.done(): - # The executor job failed - break - - queue.put_nowait(None) # terminate queue consumer - finally: - if fut is not None: - await fut - - def _move_and_cleanup() -> None: - shutil.move(target_temp_file, self.backup_dir / target_temp_file.name) - temp_dir_handler.cleanup() - - 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 +259,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/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 209311d3e8a..1e06f153cdb 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -17,9 +17,62 @@ from homeassistant.components.media_player import ( class BangOlufsenSource: """Class used for associating device source ids with friendly names. May not include all sources.""" - LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn") - SPDIF: Final[Source] = Source(name="Optical", id="spdif") - URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer") + URI_STREAMER: Final[Source] = Source( + name="Audio Streamer", + id="uriStreamer", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + BLUETOOTH: Final[Source] = Source( + name="Bluetooth", + id="bluetooth", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + CHROMECAST: Final[Source] = Source( + name="Chromecast built-in", + id="chromeCast", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + LINE_IN: Final[Source] = Source( + name="Line-In", + id="lineIn", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + SPDIF: Final[Source] = Source( + name="Optical", + id="spdif", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + NET_RADIO: Final[Source] = Source( + name="B&O Radio", + id="netRadio", + is_seekable=False, + is_enabled=True, + is_playable=True, + ) + DEEZER: Final[Source] = Source( + name="Deezer", + id="deezer", + is_seekable=True, + is_enabled=True, + is_playable=True, + ) + TIDAL: Final[Source] = Source( + name="Tidal", + id="tidal", + is_seekable=True, + is_enabled=True, + is_playable=True, + ) BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = { diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 56aa66d32e8..5dd45573672 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -688,6 +688,36 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def source(self) -> str | None: """Return the current audio source.""" + + # Try to fix some of the source_change chromecast weirdness. + if hasattr(self._playback_metadata, "title"): + # source_change is chromecast but line in is selected. + if self._playback_metadata.title == BangOlufsenSource.LINE_IN.name: + return BangOlufsenSource.LINE_IN.name + + # source_change is chromecast but bluetooth is selected. + if self._playback_metadata.title == BangOlufsenSource.BLUETOOTH.name: + return BangOlufsenSource.BLUETOOTH.name + + # source_change is line in, bluetooth or optical but stale metadata is sent through the WebSocket, + # And the source has not changed. + if self._source_change.id in ( + BangOlufsenSource.BLUETOOTH.id, + BangOlufsenSource.LINE_IN.id, + BangOlufsenSource.SPDIF.id, + ): + return BangOlufsenSource.CHROMECAST.name + + # source_change is chromecast and there is metadata but no artwork. Bluetooth does support metadata but not artwork + # So i assume that it is bluetooth and not chromecast + if ( + hasattr(self._playback_metadata, "art") + and self._playback_metadata.art is not None + and len(self._playback_metadata.art) == 0 + and self._source_change.id == BangOlufsenSource.CHROMECAST.id + ): + return BangOlufsenSource.BLUETOOTH.name + return self._source_change.name @property diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index e0b5989cc80..f20f8188b42 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -10,11 +10,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import ( - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - EntityCategory, - UnitOfTemperature, -) +from homeassistant.const import EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,8 +32,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=TYPE_WIFI_STRENGTH, translation_key="wifi_strength", - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), 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/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 4f2ad0ddcf7..844f0e9f11d 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -440,16 +440,16 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]: @websocket_api.websocket_command( { vol.Required("type"): "cloud/update_prefs", - vol.Optional(PREF_ALEXA_REPORT_STATE): bool, - vol.Optional(PREF_ENABLE_ALEXA): bool, - vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool, vol.Optional(PREF_ENABLE_GOOGLE): bool, + vol.Optional(PREF_ENABLE_ALEXA): bool, + vol.Optional(PREF_ALEXA_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), - vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All( vol.Coerce(tuple), validate_language_voice ), + vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, + vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool, } ) @websocket_api.async_response diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index ae4b2794e1b..a0811393097 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -163,21 +163,21 @@ class CloudPreferences: async def async_update( self, *, - alexa_enabled: bool | UndefinedType = UNDEFINED, - alexa_report_state: bool | UndefinedType = UNDEFINED, - alexa_settings_version: int | UndefinedType = UNDEFINED, - cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED, - cloud_user: str | UndefinedType = UNDEFINED, - cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED, - google_connected: bool | UndefinedType = UNDEFINED, google_enabled: bool | UndefinedType = UNDEFINED, - google_report_state: bool | UndefinedType = UNDEFINED, - google_secure_devices_pin: str | None | UndefinedType = UNDEFINED, - google_settings_version: int | UndefinedType = UNDEFINED, - remote_allow_remote_enable: bool | UndefinedType = UNDEFINED, - remote_domain: str | None | UndefinedType = UNDEFINED, + alexa_enabled: bool | UndefinedType = UNDEFINED, remote_enabled: bool | UndefinedType = UNDEFINED, + google_secure_devices_pin: str | None | UndefinedType = UNDEFINED, + cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED, + cloud_user: str | UndefinedType = UNDEFINED, + alexa_report_state: bool | UndefinedType = UNDEFINED, + google_report_state: bool | UndefinedType = UNDEFINED, tts_default_voice: tuple[str, str] | UndefinedType = UNDEFINED, + remote_domain: str | None | UndefinedType = UNDEFINED, + alexa_settings_version: int | UndefinedType = UNDEFINED, + google_settings_version: int | UndefinedType = UNDEFINED, + google_connected: bool | UndefinedType = UNDEFINED, + remote_allow_remote_enable: bool | UndefinedType = UNDEFINED, + cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED, ) -> None: """Update user preferences.""" prefs = {**self._prefs} @@ -186,21 +186,21 @@ class CloudPreferences: { key: value for key, value in ( - (PREF_ALEXA_REPORT_STATE, alexa_report_state), - (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version), - (PREF_CLOUD_USER, cloud_user), - (PREF_CLOUDHOOKS, cloudhooks), - (PREF_ENABLE_ALEXA, alexa_enabled), - (PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled), (PREF_ENABLE_GOOGLE, google_enabled), + (PREF_ENABLE_ALEXA, alexa_enabled), (PREF_ENABLE_REMOTE, remote_enabled), - (PREF_GOOGLE_CONNECTED, google_connected), - (PREF_GOOGLE_REPORT_STATE, google_report_state), (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), + (PREF_CLOUDHOOKS, cloudhooks), + (PREF_CLOUD_USER, cloud_user), + (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_GOOGLE_REPORT_STATE, google_report_state), + (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version), (PREF_GOOGLE_SETTINGS_VERSION, google_settings_version), - (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), - (PREF_REMOTE_DOMAIN, remote_domain), (PREF_TTS_DEFAULT_VOICE, tts_default_voice), + (PREF_REMOTE_DOMAIN, remote_domain), + (PREF_GOOGLE_CONNECTED, google_connected), + (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), + (PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled), ) if value is not UNDEFINED } @@ -242,7 +242,6 @@ class CloudPreferences: PREF_ALEXA_REPORT_STATE: self.alexa_report_state, PREF_CLOUDHOOKS: self.cloudhooks, PREF_ENABLE_ALEXA: self.alexa_enabled, - PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled, PREF_ENABLE_GOOGLE: self.google_enabled, PREF_ENABLE_REMOTE: self.remote_enabled, PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose, @@ -250,6 +249,7 @@ class CloudPreferences: PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable, PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, + PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled, } @property diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 0d357cce199..622c09f0d38 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -168,7 +168,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=get_extra_name(data) or "Electricity Maps", + title=get_extra_name(data) or "CO2 Signal", data=data, ) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 775bde3c859..90fa6289b8d 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@Petro31"], "documentation": "https://www.home-assistant.io/integrations/compensation", "iot_class": "calculated", - "requirements": ["numpy==2.1.3"] + "requirements": ["numpy==2.1.2"] } diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 4838d19537a..6b5cef89fd6 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 @@ -294,7 +294,7 @@ class DefaultAgent(ConversationEntity): self.hass, language, DOMAIN, [DOMAIN] ) response_text = translations.get( - f"component.{DOMAIN}.conversation.agent.done", "Done" + f"component.{DOMAIN}.agent.done", "Done" ) response.async_set_speech(response_text) @@ -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/doods/manifest.json b/homeassistant/components/doods/manifest.json index 7c85ca63467..fabb2c30190 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==11.0.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.4.0"] } diff --git a/homeassistant/components/ecobee/number.py b/homeassistant/components/ecobee/number.py index ed3744bf11e..ab09407903d 100644 --- a/homeassistant/components/ecobee/number.py +++ b/homeassistant/components/ecobee/number.py @@ -6,14 +6,9 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging -from homeassistant.components.number import ( - NumberDeviceClass, - NumberEntity, - NumberEntityDescription, - NumberMode, -) +from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature, UnitOfTime +from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,30 +54,21 @@ async def async_setup_entry( ) -> None: """Set up the ecobee thermostat number entity.""" data: EcobeeData = hass.data[DOMAIN] + _LOGGER.debug("Adding min time ventilators numbers (if present)") - assert data is not None - - entities: list[NumberEntity] = [ - EcobeeVentilatorMinTime(data, index, numbers) - for index, thermostat in enumerate(data.ecobee.thermostats) - if thermostat["settings"]["ventilatorType"] != "none" - for numbers in VENTILATOR_NUMBERS - ] - - _LOGGER.debug("Adding compressor min temp number (if present)") - entities.extend( + async_add_entities( ( - EcobeeCompressorMinTemp(data, index) + EcobeeVentilatorMinTime(data, index, numbers) for index, thermostat in enumerate(data.ecobee.thermostats) - if thermostat["settings"]["hasHeatPump"] - ) + if thermostat["settings"]["ventilatorType"] != "none" + for numbers in VENTILATOR_NUMBERS + ), + True, ) - async_add_entities(entities, True) - class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity): - """A number class, representing min time for an ecobee thermostat with ventilator attached.""" + """A number class, representing min time for an ecobee thermostat with ventilator attached.""" entity_description: EcobeeNumberEntityDescription @@ -119,53 +105,3 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity): """Set new ventilator Min On Time value.""" self.entity_description.set_fn(self.data, self.thermostat_index, int(value)) self.update_without_throttle = True - - -class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity): - """Minimum outdoor temperature at which the compressor will operate. - - This applies more to air source heat pumps than geothermal. This serves as a safety - feature (compressors have a minimum operating temperature) as well as - providing the ability to choose fuel in a dual-fuel system (i.e. choose between - electrical heat pump and fossil auxiliary heat depending on Time of Use, Solar, - etc.). - Note that python-ecobee-api refers to this as Aux Cutover Threshold, but Ecobee - uses Compressor Protection Min Temp. - """ - - _attr_device_class = NumberDeviceClass.TEMPERATURE - _attr_has_entity_name = True - _attr_icon = "mdi:thermometer-off" - _attr_mode = NumberMode.BOX - _attr_native_min_value = -25 - _attr_native_max_value = 66 - _attr_native_step = 5 - _attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT - _attr_translation_key = "compressor_protection_min_temp" - - def __init__( - self, - data: EcobeeData, - thermostat_index: int, - ) -> None: - """Initialize ecobee compressor min temperature.""" - super().__init__(data, thermostat_index) - self._attr_unique_id = f"{self.base_unique_id}_compressor_protection_min_temp" - self.update_without_throttle = False - - async def async_update(self) -> None: - """Get the latest state from the thermostat.""" - if self.update_without_throttle: - await self.data.update(no_throttle=True) - self.update_without_throttle = False - else: - await self.data.update() - - self._attr_native_value = ( - (self.thermostat["settings"]["compressorProtectionMinTemp"]) / 10 - ) - - def set_native_value(self, value: float) -> None: - """Set new compressor minimum temperature.""" - self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value) - self.update_without_throttle = True diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 8c636bd9b04..18929cb45de 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -33,18 +33,15 @@ }, "number": { "ventilator_min_type_home": { - "name": "Ventilator minimum time home" + "name": "Ventilator min time home" }, "ventilator_min_type_away": { - "name": "Ventilator minimum time away" - }, - "compressor_protection_min_temp": { - "name": "Compressor minimum temperature" + "name": "Ventilator min time away" } }, "switch": { "aux_heat_only": { - "name": "Auxiliary heat only" + "name": "Aux heat only" } } }, diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 0ab9f9a4612..33977b3b0de 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.4.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"] } diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py index 84b27161edd..f63e627ea7d 100644 --- a/homeassistant/components/eq3btsmart/__init__.py +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -15,23 +15,17 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED +from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED from .models import Eq3Config, Eq3ConfigEntryData PLATFORMS = [ - Platform.BINARY_SENSOR, Platform.CLIMATE, - Platform.NUMBER, - Platform.SWITCH, ] _LOGGER = logging.getLogger(__name__) -type Eq3ConfigEntry = ConfigEntry[Eq3ConfigEntryData] - - -async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle config entry setup.""" mac_address: str | None = entry.unique_id @@ -59,11 +53,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool: ble_device=device, ) - entry.runtime_data = Eq3ConfigEntryData( - eq3_config=eq3_config, thermostat=thermostat - ) + eq3_config_entry = Eq3ConfigEntryData(eq3_config=eq3_config, thermostat=thermostat) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = eq3_config_entry + entry.async_on_unload(entry.add_update_listener(update_listener)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_create_background_task( hass, _async_run_thermostat(hass, entry), entry.entry_id ) @@ -71,27 +66,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle config entry unload.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await entry.runtime_data.thermostat.async_disconnect() + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN].pop(entry.entry_id) + await eq3_config_entry.thermostat.async_disconnect() return unload_ok -async def update_listener(hass: HomeAssistant, entry: Eq3ConfigEntry) -> None: +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle config entry update.""" await hass.config_entries.async_reload(entry.entry_id) -async def _async_run_thermostat(hass: HomeAssistant, entry: Eq3ConfigEntry) -> None: +async def _async_run_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: """Run the thermostat.""" - thermostat = entry.runtime_data.thermostat - mac_address = entry.runtime_data.eq3_config.mac_address - scan_interval = entry.runtime_data.eq3_config.scan_interval + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval await _async_reconnect_thermostat(hass, entry) @@ -120,14 +117,13 @@ async def _async_run_thermostat(hass: HomeAssistant, entry: Eq3ConfigEntry) -> N await asyncio.sleep(scan_interval) -async def _async_reconnect_thermostat( - hass: HomeAssistant, entry: Eq3ConfigEntry -) -> None: +async def _async_reconnect_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reconnect the thermostat.""" - thermostat = entry.runtime_data.thermostat - mac_address = entry.runtime_data.eq3_config.mac_address - scan_interval = entry.runtime_data.eq3_config.scan_interval + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval while True: try: diff --git a/homeassistant/components/eq3btsmart/binary_sensor.py b/homeassistant/components/eq3btsmart/binary_sensor.py deleted file mode 100644 index 27525d47972..00000000000 --- a/homeassistant/components/eq3btsmart/binary_sensor.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Platform for eq3 binary sensor entities.""" - -from collections.abc import Callable -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from eq3btsmart.models import Status - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, - BinarySensorEntityDescription, -) -from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import Eq3ConfigEntry -from .const import ENTITY_KEY_BATTERY, ENTITY_KEY_DST, ENTITY_KEY_WINDOW -from .entity import Eq3Entity - - -@dataclass(frozen=True, kw_only=True) -class Eq3BinarySensorEntityDescription(BinarySensorEntityDescription): - """Entity description for eq3 binary sensors.""" - - value_func: Callable[[Status], bool] - - -BINARY_SENSOR_ENTITY_DESCRIPTIONS = [ - Eq3BinarySensorEntityDescription( - value_func=lambda status: status.is_low_battery, - key=ENTITY_KEY_BATTERY, - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - ), - Eq3BinarySensorEntityDescription( - value_func=lambda status: status.is_window_open, - key=ENTITY_KEY_WINDOW, - device_class=BinarySensorDeviceClass.WINDOW, - ), - Eq3BinarySensorEntityDescription( - value_func=lambda status: status.is_dst, - key=ENTITY_KEY_DST, - translation_key=ENTITY_KEY_DST, - entity_category=EntityCategory.DIAGNOSTIC, - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - entry: Eq3ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the entry.""" - - async_add_entities( - Eq3BinarySensorEntity(entry, entity_description) - for entity_description in BINARY_SENSOR_ENTITY_DESCRIPTIONS - ) - - -class Eq3BinarySensorEntity(Eq3Entity, BinarySensorEntity): - """Base class for eQ-3 binary sensor entities.""" - - entity_description: Eq3BinarySensorEntityDescription - - def __init__( - self, - entry: Eq3ConfigEntry, - entity_description: Eq3BinarySensorEntityDescription, - ) -> None: - """Initialize the entity.""" - - super().__init__(entry, entity_description.key) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return the state of the binary sensor.""" - - if TYPE_CHECKING: - assert self._thermostat.status is not None - - return self.entity_description.value_func(self._thermostat.status) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index ae01d0fc9a7..7b8ccb6c990 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -3,6 +3,7 @@ import logging from typing import Any +from eq3btsmart import Thermostat from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode from eq3btsmart.exceptions import Eq3Exception @@ -14,35 +15,45 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify -from . import Eq3ConfigEntry from .const import ( + DEVICE_MODEL, + DOMAIN, EQ_TO_HA_HVAC, HA_TO_EQ_HVAC, + MANUFACTURER, + SIGNAL_THERMOSTAT_CONNECTED, + SIGNAL_THERMOSTAT_DISCONNECTED, CurrentTemperatureSelector, Preset, TargetTemperatureSelector, ) from .entity import Eq3Entity +from .models import Eq3Config, Eq3ConfigEntryData _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: Eq3ConfigEntry, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Handle config entry setup.""" + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( - [Eq3Climate(entry)], + [Eq3Climate(eq3_config_entry.eq3_config, eq3_config_entry.thermostat)], ) @@ -69,6 +80,53 @@ class Eq3Climate(Eq3Entity, ClimateEntity): _attr_preset_mode: str | None = None _target_temperature: float | None = None + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: + """Initialize the climate entity.""" + + super().__init__(eq3_config, thermostat) + self._attr_unique_id = dr.format_mac(eq3_config.mac_address) + self._attr_device_info = DeviceInfo( + name=slugify(self._eq3_config.mac_address), + manufacturer=MANUFACTURER, + model=DEVICE_MODEL, + connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, + ) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + self._thermostat.register_update_callback(self._async_on_updated) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}", + self._async_on_disconnected, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}", + self._async_on_connected, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + self._thermostat.unregister_update_callback(self._async_on_updated) + + @callback + def _async_on_disconnected(self) -> None: + self._attr_available = False + self.async_write_ha_state() + + @callback + def _async_on_connected(self) -> None: + self._attr_available = True + self.async_write_ha_state() + @callback def _async_on_updated(self) -> None: """Handle updated data from the thermostat.""" @@ -79,15 +137,12 @@ class Eq3Climate(Eq3Entity, ClimateEntity): if self._thermostat.device_data is not None: self._async_on_device_updated() - super()._async_on_updated() + self.async_write_ha_state() @callback def _async_on_status_updated(self) -> None: """Handle updated status from the thermostat.""" - if self._thermostat.status is None: - return - self._target_temperature = self._thermostat.status.target_temperature.value self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode] self._attr_current_temperature = self._get_current_temperature() @@ -99,16 +154,13 @@ class Eq3Climate(Eq3Entity, ClimateEntity): def _async_on_device_updated(self) -> None: """Handle updated device data from the thermostat.""" - if self._thermostat.device_data is None: - return - device_registry = dr.async_get(self.hass) if device := device_registry.async_get_device( connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, ): device_registry.async_update_device( device.id, - sw_version=str(self._thermostat.device_data.firmware_version), + sw_version=self._thermostat.device_data.firmware_version, serial_number=self._thermostat.device_data.device_serial.value, ) @@ -213,7 +265,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity): self.async_write_ha_state() try: - await self._thermostat.async_set_temperature(temperature) + await self._thermostat.async_set_temperature(self._target_temperature) except Eq3Exception: _LOGGER.error( "[%s] Failed setting temperature", self._eq3_config.mac_address diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py index 78292940e60..111c4d0eba4 100644 --- a/homeassistant/components/eq3btsmart/const.py +++ b/homeassistant/components/eq3btsmart/const.py @@ -18,20 +18,9 @@ DOMAIN = "eq3btsmart" MANUFACTURER = "eQ-3 AG" DEVICE_MODEL = "CC-RT-BLE-EQ" -ENTITY_KEY_DST = "dst" -ENTITY_KEY_BATTERY = "battery" -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 + EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = { OperationMode.OFF: HVACMode.OFF, OperationMode.ON: HVACMode.HEAT, @@ -82,5 +71,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/entity.py b/homeassistant/components/eq3btsmart/entity.py index e68545c08c7..e8c00d4e3cf 100644 --- a/homeassistant/components/eq3btsmart/entity.py +++ b/homeassistant/components/eq3btsmart/entity.py @@ -1,22 +1,10 @@ """Base class for all eQ-3 entities.""" -from homeassistant.core import callback -from homeassistant.helpers.device_registry import ( - CONNECTION_BLUETOOTH, - DeviceInfo, - format_mac, -) -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify +from eq3btsmart.thermostat import Thermostat -from . import Eq3ConfigEntry -from .const import ( - DEVICE_MODEL, - MANUFACTURER, - SIGNAL_THERMOSTAT_CONNECTED, - SIGNAL_THERMOSTAT_DISCONNECTED, -) +from homeassistant.helpers.entity import Entity + +from .models import Eq3Config class Eq3Entity(Entity): @@ -24,70 +12,8 @@ class Eq3Entity(Entity): _attr_has_entity_name = True - def __init__( - self, - entry: Eq3ConfigEntry, - unique_id_key: str | None = None, - ) -> None: + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: """Initialize the eq3 entity.""" - self._eq3_config = entry.runtime_data.eq3_config - self._thermostat = entry.runtime_data.thermostat - self._attr_device_info = DeviceInfo( - name=slugify(self._eq3_config.mac_address), - manufacturer=MANUFACTURER, - model=DEVICE_MODEL, - connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, - ) - suffix = f"_{unique_id_key}" if unique_id_key else "" - self._attr_unique_id = f"{format_mac(self._eq3_config.mac_address)}{suffix}" - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - - self._thermostat.register_update_callback(self._async_on_updated) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}", - self._async_on_disconnected, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}", - self._async_on_connected, - ) - ) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" - - self._thermostat.unregister_update_callback(self._async_on_updated) - - def _async_on_updated(self) -> None: - """Handle updated data from the thermostat.""" - - self.async_write_ha_state() - - @callback - def _async_on_disconnected(self) -> None: - """Handle disconnection from the thermostat.""" - - self._attr_available = False - self.async_write_ha_state() - - @callback - def _async_on_connected(self) -> None: - """Handle connection to the thermostat.""" - - self._attr_available = True - self.async_write_ha_state() - - @property - def available(self) -> bool: - """Whether the entity is available.""" - - return self._thermostat.status is not None and self._attr_available + self._eq3_config = eq3_config + self._thermostat = thermostat diff --git a/homeassistant/components/eq3btsmart/icons.json b/homeassistant/components/eq3btsmart/icons.json deleted file mode 100644 index e6eb7532f37..00000000000 --- a/homeassistant/components/eq3btsmart/icons.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "entity": { - "binary_sensor": { - "dst": { - "default": "mdi:sun-clock", - "state": { - "off": "mdi:sun-clock-outline" - } - } - }, - "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", - "state": { - "on": "mdi:home-export" - } - }, - "lock": { - "default": "mdi:lock", - "state": { - "off": "mdi:lock-off" - } - }, - "boost": { - "default": "mdi:fire", - "state": { - "off": "mdi:fire-off" - } - } - } - } -} diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index b30f806bf63..e25c675bf82 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.0", "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..5108baa1bcf 100644 --- a/homeassistant/components/eq3btsmart/strings.json +++ b/homeassistant/components/eq3btsmart/strings.json @@ -18,40 +18,5 @@ "error": { "invalid_mac_address": "Invalid MAC address" } - }, - "entity": { - "binary_sensor": { - "dst": { - "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" - }, - "boost": { - "name": "Boost" - }, - "away": { - "name": "Away" - } - } } } diff --git a/homeassistant/components/eq3btsmart/switch.py b/homeassistant/components/eq3btsmart/switch.py deleted file mode 100644 index 7525d8ca494..00000000000 --- a/homeassistant/components/eq3btsmart/switch.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Platform for eq3 switch entities.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from eq3btsmart import Thermostat -from eq3btsmart.models import Status - -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import Eq3ConfigEntry -from .const import ENTITY_KEY_AWAY, ENTITY_KEY_BOOST, ENTITY_KEY_LOCK -from .entity import Eq3Entity - - -@dataclass(frozen=True, kw_only=True) -class Eq3SwitchEntityDescription(SwitchEntityDescription): - """Entity description for eq3 switch entities.""" - - toggle_func: Callable[[Thermostat], Callable[[bool], Awaitable[None]]] - value_func: Callable[[Status], bool] - - -SWITCH_ENTITY_DESCRIPTIONS = [ - Eq3SwitchEntityDescription( - key=ENTITY_KEY_LOCK, - translation_key=ENTITY_KEY_LOCK, - toggle_func=lambda thermostat: thermostat.async_set_locked, - value_func=lambda status: status.is_locked, - ), - Eq3SwitchEntityDescription( - key=ENTITY_KEY_BOOST, - translation_key=ENTITY_KEY_BOOST, - toggle_func=lambda thermostat: thermostat.async_set_boost, - value_func=lambda status: status.is_boost, - ), - Eq3SwitchEntityDescription( - key=ENTITY_KEY_AWAY, - translation_key=ENTITY_KEY_AWAY, - toggle_func=lambda thermostat: thermostat.async_set_away, - value_func=lambda status: status.is_away, - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - entry: Eq3ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the entry.""" - - async_add_entities( - Eq3SwitchEntity(entry, entity_description) - for entity_description in SWITCH_ENTITY_DESCRIPTIONS - ) - - -class Eq3SwitchEntity(Eq3Entity, SwitchEntity): - """Base class for eq3 switch entities.""" - - entity_description: Eq3SwitchEntityDescription - - def __init__( - self, - entry: Eq3ConfigEntry, - entity_description: Eq3SwitchEntityDescription, - ) -> None: - """Initialize the entity.""" - - super().__init__(entry, entity_description.key) - self.entity_description = entity_description - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the switch.""" - - await self.entity_description.toggle_func(self._thermostat)(True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the switch.""" - - await self.entity_description.toggle_func(self._thermostat)(False) - - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - - if TYPE_CHECKING: - assert self._thermostat.status is not None - - return self.entity_description.value_func(self._thermostat.status) diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index 25a506a0052..05735d152cf 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -73,9 +73,11 @@ class EzvizUpdateEntity(EzvizEntity, UpdateEntity): return self.data["version"] @property - def in_progress(self) -> bool: + def in_progress(self) -> bool | int | None: """Update installation progress.""" - return bool(self.data["upgrade_in_progress"]) + if self.data["upgrade_in_progress"]: + return self.data["upgrade_percent"] + return False @property def latest_version(self) -> str | None: @@ -91,13 +93,6 @@ class EzvizUpdateEntity(EzvizEntity, UpdateEntity): return self.data["latest_firmware_info"].get("desc") return None - @property - def update_percentage(self) -> int | None: - """Update installation progress.""" - if self.data["upgrade_in_progress"]: - return self.data["upgrade_percent"] - return None - async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: diff --git a/homeassistant/components/file/__init__.py b/homeassistant/components/file/__init__.py index 7bc206057c8..0c9cfee5f4d 100644 --- a/homeassistant/components/file/__init__.py +++ b/homeassistant/components/file/__init__.py @@ -3,16 +3,88 @@ from copy import deepcopy from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FILE_PATH, CONF_NAME, CONF_PLATFORM, Platform -from homeassistant.core import HomeAssistant +from homeassistant.components.notify import migrate_notify_issue +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_FILE_PATH, + CONF_NAME, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, + Platform, +) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import ( + config_validation as cv, + discovery, + issue_registry as ir, +) +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN +from .notify import PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA +from .sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA + +IMPORT_SCHEMA = { + Platform.SENSOR: SENSOR_PLATFORM_SCHEMA, + Platform.NOTIFY: NOTIFY_PLATFORM_SCHEMA, +} + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the file integration.""" + + hass.data[DOMAIN] = config + if hass.config_entries.async_entries(DOMAIN): + # We skip import in case we already have config entries + return True + # The use of the legacy notify service was deprecated with HA Core 2024.6.0 + # and will be removed with HA Core 2024.12 + migrate_notify_issue(hass, DOMAIN, "File", "2024.12.0") + # The YAML config was imported with HA Core 2024.6.0 and will be removed with + # HA Core 2024.12 + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + learn_more_url="https://www.home-assistant.io/integrations/file/", + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "File", + }, + ) + + # Import the YAML config into separate config entries + platforms_config: dict[Platform, list[ConfigType]] = { + domain: config[domain] for domain in PLATFORMS if domain in config + } + for domain, items in platforms_config.items(): + for item in items: + if item[CONF_PLATFORM] == DOMAIN: + file_config_item = IMPORT_SCHEMA[domain](item) + file_config_item[CONF_PLATFORM] = domain + if CONF_SCAN_INTERVAL in file_config_item: + del file_config_item[CONF_SCAN_INTERVAL] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=file_config_item, + ) + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a file component entry.""" config = {**entry.data, **entry.options} @@ -30,6 +102,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, [Platform(entry.data[CONF_PLATFORM])] ) entry.async_on_unload(entry.add_update_listener(update_listener)) + if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data: + # New notify entities are being setup through the config entry, + # but during the deprecation period we want to keep the legacy notify platform, + # so we forward the setup config through discovery. + # Only the entities from yaml will still be available as legacy service. + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + config, + hass.data[DOMAIN], + ) + ) return True diff --git a/homeassistant/components/file/config_flow.py b/homeassistant/components/file/config_flow.py index 992635d05fd..2b8a9bde749 100644 --- a/homeassistant/components/file/config_flow.py +++ b/homeassistant/components/file/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from copy import deepcopy +import os from typing import Any import voluptuous as vol @@ -15,6 +16,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import ( CONF_FILE_PATH, + CONF_FILENAME, CONF_NAME, CONF_PLATFORM, CONF_UNIT_OF_MEASUREMENT, @@ -130,6 +132,27 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle file sensor config flow.""" return await self._async_handle_step(Platform.SENSOR.value, user_input) + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import `file`` config from configuration.yaml.""" + self._async_abort_entries_match(import_data) + platform = import_data[CONF_PLATFORM] + name: str = import_data.get(CONF_NAME, DEFAULT_NAME) + file_name: str + if platform == Platform.NOTIFY: + file_name = import_data.pop(CONF_FILENAME) + file_path: str = os.path.join(self.hass.config.config_dir, file_name) + import_data[CONF_FILE_PATH] = file_path + else: + file_path = import_data[CONF_FILE_PATH] + title = f"{name} [{file_path}]" + data = deepcopy(import_data) + options = {} + for key, value in import_data.items(): + if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME): + data.pop(key) + options[key] = value + return self.async_create_entry(title=title, data=data, options=options) + class FileOptionsFlowHandler(OptionsFlow): """Handle File options.""" diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 10e3d4a4ac6..9411b7cf1a8 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -2,23 +2,104 @@ from __future__ import annotations +from functools import partial +import logging import os from typing import Any, TextIO +import voluptuous as vol + from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, NotifyEntity, NotifyEntityFeature, + migrate_notify_issue, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FILE_PATH, CONF_NAME +from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util from .const import CONF_TIMESTAMP, DEFAULT_NAME, DOMAIN, FILE_ICON +_LOGGER = logging.getLogger(__name__) + +# The legacy platform schema uses a filename, after import +# The full file path is stored in the config entry +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILENAME): cv.string, + vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, + } +) + + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> FileNotificationService | None: + """Get the file notification service.""" + if discovery_info is None: + # We only set up through discovery + return None + file_path: str = discovery_info[CONF_FILE_PATH] + timestamp: bool = discovery_info[CONF_TIMESTAMP] + + return FileNotificationService(file_path, timestamp) + + +class FileNotificationService(BaseNotificationService): + """Implement the notification service for the File service.""" + + def __init__(self, file_path: str, add_timestamp: bool) -> None: + """Initialize the service.""" + self._file_path = file_path + self.add_timestamp = add_timestamp + + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message to a file.""" + # The use of the legacy notify service was deprecated with HA Core 2024.6.0 + # and will be removed with HA Core 2024.12 + migrate_notify_issue( + self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name + ) + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) + + def send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message to a file.""" + file: TextIO + filepath = self._file_path + try: + with open(filepath, "a", encoding="utf8") as file: + if os.stat(filepath).st_size == 0: + title = ( + f"{kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)} notifications (Log" + f" started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n" + ) + file.write(title) + + if self.add_timestamp: + text = f"{dt_util.utcnow().isoformat()} {message}\n" + else: + text = f"{message}\n" + file.write(text) + except OSError as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="write_access_failed", + translation_placeholders={"filename": filepath, "exc": f"{exc!r}"}, + ) from exc + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 879c06e29f3..e37a3df86a6 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -6,8 +6,12 @@ import logging import os from file_read_backwards import FileReadBackwards +import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_FILE_PATH, @@ -16,13 +20,38 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DEFAULT_NAME, FILE_ICON _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILE_PATH): cv.isfile, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the file sensor from YAML. + + The YAML platform config is automatically + imported to a config entry, this method can be removed + when YAML support is removed. + """ + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json index 8806c67cd96..60ebf451f78 100644 --- a/homeassistant/components/file/strings.json +++ b/homeassistant/components/file/strings.json @@ -18,7 +18,7 @@ }, "data_description": { "file_path": "The local file path to retrieve the sensor value from", - "value_template": "A template to render the sensors value based on the file content", + "value_template": "A template to render the the sensors value based on the file content", "unit_of_measurement": "Unit of measurement for the sensor" } }, 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/generic/manifest.json b/homeassistant/components/generic/manifest.json index c1fbc16d9be..b02a8fa2520 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/generic", "integration_type": "device", "iot_class": "local_push", - "requirements": ["av==13.1.0", "Pillow==11.0.0"] + "requirements": ["av==13.1.0", "Pillow==10.4.0"] } diff --git a/homeassistant/components/generic_hygrostat/strings.json b/homeassistant/components/generic_hygrostat/strings.json index 2be3955eff1..a21ab68c628 100644 --- a/homeassistant/components/generic_hygrostat/strings.json +++ b/homeassistant/components/generic_hygrostat/strings.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Add generic hygrostat", - "description": "Create a humidifier entity that control the humidity via a switch and sensor.", + "description": "Create a entity that control the humidity via a switch and sensor.", "data": { "device_class": "Device class", "dry_tolerance": "Dry tolerance", diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index 51549dc844e..1ddd41de734 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "Add generic thermostat", + "title": "Add generic thermostat helper", "description": "Create a climate entity that controls the temperature via a switch and sensor.", "data": { "ac_mode": "Cooling mode", @@ -17,8 +17,8 @@ "data_description": { "ac_mode": "Set the actuator specified to be treated as a cooling device instead of a heating device.", "heater": "Switch entity used to cool or heat depending on A/C mode.", - "target_sensor": "Temperature sensor that reflects the current temperature.", - "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.", + "target_sensor": "Temperature sensor that reflect the current temperature.", + "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on. This option will be ignored if the keep alive option is set.", "cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.", "hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5." } diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 9ca6ecfcfe0..f3081e50289 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -9,6 +9,7 @@ import aiohttp from geniushubclient import GeniusHub import voluptuous as vol +from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -20,12 +21,20 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, + callback, +) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -36,6 +45,27 @@ SCAN_INTERVAL = timedelta(seconds=60) MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$" +CLOUD_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), + } +) + + +LOCAL_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Any(LOCAL_API_SCHEMA, CLOUD_API_SCHEMA)}, extra=vol.ALLOW_EXTRA +) + ATTR_ZONE_MODE = "mode" ATTR_DURATION = "duration" @@ -70,6 +100,56 @@ PLATFORMS = [ ] +async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None: + """Import a config entry from configuration.yaml.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=base_config[DOMAIN], + ) + if ( + result["type"] is FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Genius Hub", + }, + ) + return + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Genius Hub", + }, + ) + + +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: + """Set up a Genius Hub system.""" + if DOMAIN in base_config: + hass.async_create_task(_async_import(hass, base_config)) + return True + + type GeniusHubConfigEntry = ConfigEntry[GeniusBroker] diff --git a/homeassistant/components/geniushub/config_flow.py b/homeassistant/components/geniushub/config_flow.py index b106f9907bb..601eac6c2f2 100644 --- a/homeassistant/components/geniushub/config_flow.py +++ b/homeassistant/components/geniushub/config_flow.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -122,3 +123,14 @@ class GeniusHubConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="cloud_api", errors=errors, data_schema=CLOUD_API_SCHEMA ) + + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import the yaml config.""" + if CONF_HOST in import_data: + result = await self.async_step_local_api(import_data) + else: + result = await self.async_step_cloud_api(import_data) + if result["type"] is FlowResultType.FORM: + assert result["errors"] + return self.async_abort(reason=result["errors"]["base"]) + return result diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index f1f6e44abc1..04b5b9f9317 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -1,10 +1,12 @@ """The go2rtc component.""" +from __future__ import annotations + +from dataclasses import dataclass 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,11 +35,7 @@ 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 @@ -47,8 +45,8 @@ from .const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, + HA_MANAGED_RTSP_PORT, HA_MANAGED_URL, - RECOMMENDED_VERSION, ) from .server import Server @@ -96,13 +94,22 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) +_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) +@dataclass(frozen=True) +class Go2RtcData: + """Data for go2rtc.""" + + url: str + managed: bool + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up WebRTC.""" url: str | None = None + managed = False if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: await _remove_go2rtc_entries(hass) return True @@ -137,8 +144,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) url = HA_MANAGED_URL + managed = True - hass.data[_DATA_GO2RTC] = url + hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed) discovery_flow.async_create_flow( hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} ) @@ -153,42 +161,32 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up go2rtc from a config entry.""" - url = hass.data[_DATA_GO2RTC] + data = hass.data[_DATA_GO2RTC] # 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), - }, - ) + client = Go2RtcRestClient(async_get_clientsession(hass), data.url) + await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( - f"Could not connect to go2rtc instance on {url}" + f"Could not connect to go2rtc instance on {data.url}" ) from err - _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) + _LOGGER.warning( + "Could not connect to go2rtc instance on %s (%s)", data.url, err + ) return False except Go2RtcVersionError as err: raise ConfigEntryNotReady( f"The go2rtc server version is not supported, {err}" ) from err except Exception as err: # noqa: BLE001 - _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) + _LOGGER.warning( + "Could not connect to go2rtc instance on %s (%s)", data.url, err + ) return False - provider = WebRTCProvider(hass, url) + provider = WebRTCProvider(hass, data) async_register_webrtc_provider(hass, provider) return True @@ -206,12 +204,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None: class WebRTCProvider(CameraWebRTCProvider): """WebRTC provider.""" - def __init__(self, hass: HomeAssistant, url: str) -> None: + def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None: """Initialize the WebRTC provider.""" self._hass = hass - self._url = url + self._data = data self._session = async_get_clientsession(hass) - self._rest_client = Go2RtcRestClient(self._session, url) + self._rest_client = Go2RtcRestClient(self._session, data.url) self._sessions: dict[str, Go2RtcWsClient] = {} @property @@ -233,7 +231,7 @@ class WebRTCProvider(CameraWebRTCProvider): ) -> None: """Handle the WebRTC offer and return the answer via the provided callback.""" self._sessions[session_id] = ws_client = Go2RtcWsClient( - self._session, self._url, source=camera.entity_id + self._session, self._data.url, source=camera.entity_id ) if not (stream_source := await camera.stream_source()): @@ -244,18 +242,34 @@ class WebRTCProvider(CameraWebRTCProvider): streams = await self._rest_client.streams.list() - if (stream := streams.get(camera.entity_id)) is None or not any( - stream_source == producer.url for producer in stream.producers + if self._data.managed: + # HA manages the go2rtc instance + stream_original_name = f"{camera.entity_id}_original" + stream_redirect_sources = [ + f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}", + f"ffmpeg:{stream_original_name}#audio=opus", + ] + + if ( + (stream_org := streams.get(stream_original_name)) is None + or not any( + stream_source == producer.url for producer in stream_org.producers + ) + or (stream_redirect := streams.get(camera.entity_id)) is None + or stream_redirect_sources != [p.url for p in stream_redirect.producers] + ): + await self._rest_client.streams.add(stream_original_name, stream_source) + await self._rest_client.streams.add( + camera.entity_id, stream_redirect_sources + ) + + # go2rtc instance is managed outside HA + elif (stream_org := streams.get(camera.entity_id)) is None or not any( + stream_source == producer.url for producer in stream_org.producers ): await self._rest_client.streams.add( camera.entity_id, - [ - stream_source, - # We are setting any ffmpeg rtsp related logs to debug - # Connection problems to the camera will be logged by the first stream - # Therefore setting it to debug will not hide any important logs - f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", - ], + [stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"], ) @callback diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index 3c1c84c42b5..3c4dc9a9500 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,4 +6,4 @@ 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" +HA_MANAGED_RTSP_PORT = 18554 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/server.py b/homeassistant/components/go2rtc/server.py index 6699ee4d8a2..91f4433546c 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL +from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL _LOGGER = logging.getLogger(__name__) _TERMINATE_TIMEOUT = 5 @@ -33,7 +33,7 @@ api: listen: "{api_ip}:{api_port}" rtsp: - listen: "127.0.0.1:18554" + listen: "127.0.0.1:{rtsp_port}" webrtc: listen: ":18555/tcp" @@ -68,7 +68,9 @@ def _create_temp_file(api_ip: str) -> str: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: file.write( _GO2RTC_CONFIG_FORMAT.format( - api_ip=api_ip, api_port=HA_MANAGED_API_PORT + api_ip=api_ip, + api_port=HA_MANAGED_API_PORT, + rtsp_port=HA_MANAGED_RTSP_PORT, ).encode() ) return file.name 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/google/strings.json b/homeassistant/components/google/strings.json index 2ea45239a53..c029b46051e 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -87,8 +87,8 @@ } }, "create_event": { - "name": "Create event", - "description": "Adds a new calendar event.", + "name": "Creates event", + "description": "Add a new calendar event.", "fields": { "summary": { "name": "Summary", diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 8132ecaae2c..04c85639e07 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -78,7 +78,6 @@ TYPE_AWNING = f"{PREFIX_TYPES}AWNING" TYPE_BLINDS = f"{PREFIX_TYPES}BLINDS" TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA" TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN" -TYPE_CARBON_MONOXIDE_DETECTOR = f"{PREFIX_TYPES}CARBON_MONOXIDE_DETECTOR" TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER" TYPE_DOOR = f"{PREFIX_TYPES}DOOR" TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL" @@ -94,7 +93,6 @@ TYPE_SCENE = f"{PREFIX_TYPES}SCENE" TYPE_SENSOR = f"{PREFIX_TYPES}SENSOR" TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP" TYPE_SHUTTER = f"{PREFIX_TYPES}SHUTTER" -TYPE_SMOKE_DETECTOR = f"{PREFIX_TYPES}SMOKE_DETECTOR" TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER" TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH" TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT" @@ -138,7 +136,6 @@ EVENT_SYNC_RECEIVED = "google_assistant_sync" DOMAIN_TO_GOOGLE_TYPES = { alarm_control_panel.DOMAIN: TYPE_ALARM, - binary_sensor.DOMAIN: TYPE_SENSOR, button.DOMAIN: TYPE_SCENE, camera.DOMAIN: TYPE_CAMERA, climate.DOMAIN: TYPE_THERMOSTAT, @@ -171,14 +168,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, ): TYPE_GARAGE, - ( - binary_sensor.DOMAIN, - binary_sensor.BinarySensorDeviceClass.SMOKE, - ): TYPE_SMOKE_DETECTOR, - ( - binary_sensor.DOMAIN, - binary_sensor.BinarySensorDeviceClass.CO, - ): TYPE_CARBON_MONOXIDE_DETECTOR, (cover.DOMAIN, cover.CoverDeviceClass.AWNING): TYPE_AWNING, (cover.DOMAIN, cover.CoverDeviceClass.CURTAIN): TYPE_CURTAIN, (cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f99f1574038..df56885995a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2706,21 +2706,6 @@ class SensorStateTrait(_Trait): ), } - binary_sensor_types = { - binary_sensor.BinarySensorDeviceClass.CO: ( - "CarbonMonoxideLevel", - ["carbon monoxide detected", "no carbon monoxide detected", "unknown"], - ), - binary_sensor.BinarySensorDeviceClass.SMOKE: ( - "SmokeLevel", - ["smoke detected", "no smoke detected", "unknown"], - ), - binary_sensor.BinarySensorDeviceClass.MOISTURE: ( - "WaterLeak", - ["leak", "no leak", "unknown"], - ), - } - name = TRAIT_SENSOR_STATE commands: list[str] = [] @@ -2743,37 +2728,24 @@ class SensorStateTrait(_Trait): @classmethod def supported(cls, domain, features, device_class, _): """Test if state is supported.""" - return (domain == sensor.DOMAIN and device_class in cls.sensor_types) or ( - domain == binary_sensor.DOMAIN and device_class in cls.binary_sensor_types - ) + return domain == sensor.DOMAIN and device_class in cls.sensor_types def sync_attributes(self) -> dict[str, Any]: """Return attributes for a sync request.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) + data = self.sensor_types.get(device_class) - def create_sensor_state( - name: str, - raw_value_unit: str | None = None, - available_states: list[str] | None = None, - ) -> dict[str, Any]: - sensor_state: dict[str, Any] = { - "name": name, - } - if raw_value_unit: - sensor_state["numericCapabilities"] = {"rawValueUnit": raw_value_unit} - if available_states: - sensor_state["descriptiveCapabilities"] = { - "availableStates": available_states - } - return {"sensorStatesSupported": [sensor_state]} + if device_class is None or data is None: + return {} - if self.state.domain == sensor.DOMAIN: - sensor_data = self.sensor_types.get(device_class) - if device_class is None or sensor_data is None: - return {} - available_states: list[str] | None = None - if device_class == sensor.SensorDeviceClass.AQI: - available_states = [ + sensor_state = { + "name": data[0], + "numericCapabilities": {"rawValueUnit": data[1]}, + } + + if device_class == sensor.SensorDeviceClass.AQI: + sensor_state["descriptiveCapabilities"] = { + "availableStates": [ "healthy", "moderate", "unhealthy for sensitive groups", @@ -2781,53 +2753,30 @@ class SensorStateTrait(_Trait): "very unhealthy", "hazardous", "unknown", - ] - return create_sensor_state(sensor_data[0], sensor_data[1], available_states) - binary_sensor_data = self.binary_sensor_types.get(device_class) - if device_class is None or binary_sensor_data is None: - return {} - return create_sensor_state( - binary_sensor_data[0], available_states=binary_sensor_data[1] - ) + ], + } + + return {"sensorStatesSupported": [sensor_state]} def query_attributes(self) -> dict[str, Any]: """Return the attributes of this trait for this entity.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) + data = self.sensor_types.get(device_class) - def create_sensor_state( - name: str, raw_value: float | None = None, current_state: str | None = None - ) -> dict[str, Any]: - sensor_state: dict[str, Any] = { - "name": name, - "rawValue": raw_value, - } - if current_state: - sensor_state["currentSensorState"] = current_state - return {"currentSensorStateData": [sensor_state]} - - if self.state.domain == sensor.DOMAIN: - sensor_data = self.sensor_types.get(device_class) - if device_class is None or sensor_data is None: - return {} - try: - value = float(self.state.state) - except ValueError: - value = None - if self.state.state == STATE_UNKNOWN: - value = None - current_state: str | None = None - if device_class == sensor.SensorDeviceClass.AQI: - current_state = self._air_quality_description_for_aqi(value) - return create_sensor_state(sensor_data[0], value, current_state) - - binary_sensor_data = self.binary_sensor_types.get(device_class) - if device_class is None or binary_sensor_data is None: + if device_class is None or data is None: return {} - value = { - STATE_ON: 0, - STATE_OFF: 1, - STATE_UNKNOWN: 2, - }[self.state.state] - return create_sensor_state( - binary_sensor_data[0], current_state=binary_sensor_data[1][value] - ) + + try: + value = float(self.state.state) + except ValueError: + value = None + if self.state.state == STATE_UNKNOWN: + value = None + sensor_data = {"name": data[0], "rawValue": value} + + if device_class == sensor.SensorDeviceClass.AQI: + sensor_data["currentSensorState"] = self._air_quality_description_for_aqi( + value + ) + + return {"currentSensorStateData": [sensor_data]} diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index ae98cb13dcb..55322a13e6a 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -25,16 +25,7 @@ UNIT_TASKS = "tasks" ATTR_CONFIG_ENTRY = "config_entry" ATTR_SKILL = "skill" ATTR_TASK = "task" -ATTR_DIRECTION = "direction" SERVICE_CAST_SKILL = "cast_skill" -SERVICE_START_QUEST = "start_quest" -SERVICE_ACCEPT_QUEST = "accept_quest" -SERVICE_CANCEL_QUEST = "cancel_quest" -SERVICE_ABORT_QUEST = "abort_quest" -SERVICE_REJECT_QUEST = "reject_quest" -SERVICE_LEAVE_QUEST = "leave_quest" -SERVICE_SCORE_HABIT = "score_habit" -SERVICE_SCORE_REWARD = "score_reward" WARRIOR = "warrior" ROGUE = "rogue" diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index f9ffb1b53bd..cce2c684ba8 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -51,17 +51,12 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): ), ) self.api = habitipy - self.content: dict[str, Any] = {} async def _async_update_data(self) -> HabiticaData: try: user_response = await self.api.user.get() tasks_response = await self.api.tasks.user.get() tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) - if not self.content: - self.content = await self.api.content.get( - language=user_response["preferences"]["language"] - ) except ClientResponseError as error: if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.debug("Rate limit exceeded, will try again later") diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index d33b9c60c96..0698b85afe1 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -126,18 +126,6 @@ }, "rewards": { "default": "mdi:treasure-chest" - }, - "strength": { - "default": "mdi:arm-flex-outline" - }, - "intelligence": { - "default": "mdi:head-snowflake-outline" - }, - "perception": { - "default": "mdi:eye-outline" - }, - "constitution": { - "default": "mdi:run-fast" } }, "switch": { @@ -163,30 +151,6 @@ }, "cast_skill": { "service": "mdi:creation-outline" - }, - "accept_quest": { - "service": "mdi:script-text" - }, - "reject_quest": { - "service": "mdi:script-text" - }, - "leave_quest": { - "service": "mdi:script-text" - }, - "abort_quest": { - "service": "mdi:script-text-key" - }, - "cancel_quest": { - "service": "mdi:script-text-key" - }, - "start_quest": { - "service": "mdi:script-text-key" - }, - "score_habit": { - "service": "mdi:counter" - }, - "score_reward": { - "service": "mdi:sack" } } } diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 3b2395ecc52..77356f88265 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import StateType from .const import DOMAIN, UNIT_TASKS from .entity import HabiticaBase from .types import HabiticaConfigEntry -from .util import entity_used_in, get_attribute_points, get_attributes_total +from .util import entity_used_in _LOGGER = logging.getLogger(__name__) @@ -36,10 +36,7 @@ _LOGGER = logging.getLogger(__name__) class HabitipySensorEntityDescription(SensorEntityDescription): """Habitipy Sensor Description.""" - value_fn: Callable[[dict[str, Any], dict[str, Any]], StateType] - attributes_fn: ( - Callable[[dict[str, Any], dict[str, Any]], dict[str, Any] | None] | None - ) = None + value_fn: Callable[[dict[str, Any]], StateType] @dataclass(kw_only=True, frozen=True) @@ -68,80 +65,76 @@ class HabitipySensorEntity(StrEnum): REWARDS = "rewards" GEMS = "gems" TRINKETS = "trinkets" - STRENGTH = "strength" - INTELLIGENCE = "intelligence" - CONSTITUTION = "constitution" - PERCEPTION = "perception" SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = ( HabitipySensorEntityDescription( key=HabitipySensorEntity.DISPLAY_NAME, translation_key=HabitipySensorEntity.DISPLAY_NAME, - value_fn=lambda user, _: user.get("profile", {}).get("name"), + value_fn=lambda user: user.get("profile", {}).get("name"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.HEALTH, translation_key=HabitipySensorEntity.HEALTH, native_unit_of_measurement="HP", suggested_display_precision=0, - value_fn=lambda user, _: user.get("stats", {}).get("hp"), + value_fn=lambda user: user.get("stats", {}).get("hp"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.HEALTH_MAX, translation_key=HabitipySensorEntity.HEALTH_MAX, native_unit_of_measurement="HP", entity_registry_enabled_default=False, - value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"), + value_fn=lambda user: user.get("stats", {}).get("maxHealth"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.MANA, translation_key=HabitipySensorEntity.MANA, native_unit_of_measurement="MP", suggested_display_precision=0, - value_fn=lambda user, _: user.get("stats", {}).get("mp"), + value_fn=lambda user: user.get("stats", {}).get("mp"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.MANA_MAX, translation_key=HabitipySensorEntity.MANA_MAX, native_unit_of_measurement="MP", - value_fn=lambda user, _: user.get("stats", {}).get("maxMP"), + value_fn=lambda user: user.get("stats", {}).get("maxMP"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.EXPERIENCE, translation_key=HabitipySensorEntity.EXPERIENCE, native_unit_of_measurement="XP", - value_fn=lambda user, _: user.get("stats", {}).get("exp"), + value_fn=lambda user: user.get("stats", {}).get("exp"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.EXPERIENCE_MAX, translation_key=HabitipySensorEntity.EXPERIENCE_MAX, native_unit_of_measurement="XP", - value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"), + value_fn=lambda user: user.get("stats", {}).get("toNextLevel"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.LEVEL, translation_key=HabitipySensorEntity.LEVEL, - value_fn=lambda user, _: user.get("stats", {}).get("lvl"), + value_fn=lambda user: user.get("stats", {}).get("lvl"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.GOLD, translation_key=HabitipySensorEntity.GOLD, native_unit_of_measurement="GP", suggested_display_precision=2, - value_fn=lambda user, _: user.get("stats", {}).get("gp"), + value_fn=lambda user: user.get("stats", {}).get("gp"), ), HabitipySensorEntityDescription( key=HabitipySensorEntity.CLASS, translation_key=HabitipySensorEntity.CLASS, - value_fn=lambda user, _: user.get("stats", {}).get("class"), + value_fn=lambda user: user.get("stats", {}).get("class"), device_class=SensorDeviceClass.ENUM, options=["warrior", "healer", "wizard", "rogue"], ), HabitipySensorEntityDescription( key=HabitipySensorEntity.GEMS, translation_key=HabitipySensorEntity.GEMS, - value_fn=lambda user, _: user.get("balance", 0) * 4, + value_fn=lambda user: user.get("balance", 0) * 4, suggested_display_precision=0, native_unit_of_measurement="gems", ), @@ -149,7 +142,7 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = ( key=HabitipySensorEntity.TRINKETS, translation_key=HabitipySensorEntity.TRINKETS, value_fn=( - lambda user, _: user.get("purchased", {}) + lambda user: user.get("purchased", {}) .get("plan", {}) .get("consecutive", {}) .get("trinkets", 0) @@ -157,38 +150,6 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = ( suggested_display_precision=0, native_unit_of_measurement="⧖", ), - HabitipySensorEntityDescription( - key=HabitipySensorEntity.STRENGTH, - translation_key=HabitipySensorEntity.STRENGTH, - value_fn=lambda user, content: get_attributes_total(user, content, "str"), - attributes_fn=lambda user, content: get_attribute_points(user, content, "str"), - suggested_display_precision=0, - native_unit_of_measurement="STR", - ), - HabitipySensorEntityDescription( - key=HabitipySensorEntity.INTELLIGENCE, - translation_key=HabitipySensorEntity.INTELLIGENCE, - value_fn=lambda user, content: get_attributes_total(user, content, "int"), - attributes_fn=lambda user, content: get_attribute_points(user, content, "int"), - suggested_display_precision=0, - native_unit_of_measurement="INT", - ), - HabitipySensorEntityDescription( - key=HabitipySensorEntity.PERCEPTION, - translation_key=HabitipySensorEntity.PERCEPTION, - value_fn=lambda user, content: get_attributes_total(user, content, "per"), - attributes_fn=lambda user, content: get_attribute_points(user, content, "per"), - suggested_display_precision=0, - native_unit_of_measurement="PER", - ), - HabitipySensorEntityDescription( - key=HabitipySensorEntity.CONSTITUTION, - translation_key=HabitipySensorEntity.CONSTITUTION, - value_fn=lambda user, content: get_attributes_total(user, content, "con"), - attributes_fn=lambda user, content: get_attribute_points(user, content, "con"), - suggested_display_precision=0, - native_unit_of_measurement="CON", - ), ) @@ -282,16 +243,7 @@ class HabitipySensor(HabiticaBase, SensorEntity): def native_value(self) -> StateType: """Return the state of the device.""" - return self.entity_description.value_fn( - self.coordinator.data.user, self.coordinator.content - ) - - @property - def extra_state_attributes(self) -> dict[str, float | None] | None: - """Return entity specific state attributes.""" - if func := self.entity_description.attributes_fn: - return func(self.coordinator.data.user, self.coordinator.content) - return None + return self.entity_description.value_fn(self.coordinator.data.user) class HabitipyTaskSensor(HabiticaBase, SensorEntity): diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py index a50e5f1e6e3..440e2d4fb23 100644 --- a/homeassistant/components/habitica/services.py +++ b/homeassistant/components/habitica/services.py @@ -19,29 +19,19 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.selector import ConfigEntrySelector from .const import ( ATTR_ARGS, ATTR_CONFIG_ENTRY, ATTR_DATA, - ATTR_DIRECTION, ATTR_PATH, ATTR_SKILL, ATTR_TASK, DOMAIN, EVENT_API_CALL_SUCCESS, - SERVICE_ABORT_QUEST, - SERVICE_ACCEPT_QUEST, SERVICE_API_CALL, - SERVICE_CANCEL_QUEST, SERVICE_CAST_SKILL, - SERVICE_LEAVE_QUEST, - SERVICE_REJECT_QUEST, - SERVICE_SCORE_HABIT, - SERVICE_SCORE_REWARD, - SERVICE_START_QUEST, ) from .types import HabiticaConfigEntry @@ -64,19 +54,6 @@ SERVICE_CAST_SKILL_SCHEMA = vol.Schema( } ) -SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema( - { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), - } -) -SERVICE_SCORE_TASK_SCHEMA = vol.Schema( - { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), - vol.Required(ATTR_TASK): cv.string, - vol.Optional(ATTR_DIRECTION): cv.string, - } -) - def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry: """Return config entry or raise if not found or not loaded.""" @@ -93,23 +70,10 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry: return entry -def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 +def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Habitica integration.""" async def handle_api_call(call: ServiceCall) -> None: - async_create_issue( - hass, - DOMAIN, - "deprecated_api_call", - breaks_in_ha_version="2025.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_api_call", - ) - _LOGGER.warning( - "Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0" - ) - name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] entries = hass.config_entries.async_entries(DOMAIN) @@ -196,104 +160,6 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 await coordinator.async_request_refresh() return response - async def manage_quests(call: ServiceCall) -> ServiceResponse: - """Accept, reject, start, leave or cancel quests.""" - entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY]) - coordinator = entry.runtime_data - - COMMAND_MAP = { - SERVICE_ABORT_QUEST: "abort", - SERVICE_ACCEPT_QUEST: "accept", - SERVICE_CANCEL_QUEST: "cancel", - SERVICE_LEAVE_QUEST: "leave", - SERVICE_REJECT_QUEST: "reject", - SERVICE_START_QUEST: "force-start", - } - try: - return await coordinator.api.groups.party.quests[ - COMMAND_MAP[call.service] - ].post() - except ClientResponseError as e: - if e.status == HTTPStatus.TOO_MANY_REQUESTS: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="setup_rate_limit_exception", - ) from e - if e.status == HTTPStatus.UNAUTHORIZED: - raise ServiceValidationError( - translation_domain=DOMAIN, translation_key="quest_action_unallowed" - ) from e - if e.status == HTTPStatus.NOT_FOUND: - raise ServiceValidationError( - translation_domain=DOMAIN, translation_key="quest_not_found" - ) from e - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="service_call_exception" - ) from e - - for service in ( - SERVICE_ABORT_QUEST, - SERVICE_ACCEPT_QUEST, - SERVICE_CANCEL_QUEST, - SERVICE_LEAVE_QUEST, - SERVICE_REJECT_QUEST, - SERVICE_START_QUEST, - ): - hass.services.async_register( - DOMAIN, - service, - manage_quests, - schema=SERVICE_MANAGE_QUEST_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) - - async def score_task(call: ServiceCall) -> ServiceResponse: - """Score a task action.""" - entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY]) - coordinator = entry.runtime_data - try: - task_id, task_value = next( - (task["id"], task.get("value")) - for task in coordinator.data.tasks - if call.data[ATTR_TASK] in (task["id"], task.get("alias")) - or call.data[ATTR_TASK] == task["text"] - ) - except StopIteration as e: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="task_not_found", - translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"}, - ) from e - - try: - response: dict[str, Any] = ( - await coordinator.api.tasks[task_id] - .score[call.data.get(ATTR_DIRECTION, "up")] - .post() - ) - except ClientResponseError as e: - if e.status == HTTPStatus.TOO_MANY_REQUESTS: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="setup_rate_limit_exception", - ) from e - if e.status == HTTPStatus.UNAUTHORIZED and task_value is not None: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="not_enough_gold", - translation_placeholders={ - "gold": f"{coordinator.data.user["stats"]["gp"]:.2f} GP", - "cost": f"{task_value} GP", - }, - ) from e - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="service_call_exception", - ) from e - else: - await coordinator.async_request_refresh() - return response - hass.services.async_register( DOMAIN, SERVICE_API_CALL, @@ -308,18 +174,3 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 schema=SERVICE_CAST_SKILL_SCHEMA, supports_response=SupportsResponse.ONLY, ) - - hass.services.async_register( - DOMAIN, - SERVICE_SCORE_HABIT, - score_task, - schema=SERVICE_SCORE_TASK_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) - hass.services.async_register( - DOMAIN, - SERVICE_SCORE_REWARD, - score_task, - schema=SERVICE_SCORE_TASK_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index b539f6c65bf..546ac8c1c34 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -17,7 +17,7 @@ api_call: object: cast_skill: fields: - config_entry: &config_entry + config_entry: required: true selector: config_entry: @@ -33,42 +33,7 @@ cast_skill: - "fireball" mode: dropdown translation_key: "skill_select" - task: &task + task: required: true selector: text: -accept_quest: - fields: - config_entry: *config_entry -reject_quest: - fields: - config_entry: *config_entry -start_quest: - fields: - config_entry: *config_entry -cancel_quest: - fields: - config_entry: *config_entry -abort_quest: - fields: - config_entry: *config_entry -leave_quest: - fields: - config_entry: *config_entry -score_habit: - fields: - config_entry: *config_entry - task: *task - direction: - required: true - selector: - select: - options: - - value: up - label: "➕" - - value: down - label: "➖" -score_reward: - fields: - config_entry: *config_entry - task: *task diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index ac1faf5fcef..f7d2f20b8f9 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -1,8 +1,7 @@ { "common": { "todos": "To-Do's", - "dailies": "Dailies", - "config_entry_name": "Select character" + "dailies": "Dailies" }, "config": { "abort": { @@ -165,86 +164,6 @@ }, "rewards": { "name": "Rewards" - }, - "strength": { - "name": "Strength", - "state_attributes": { - "level": { - "name": "[%key:component::habitica::entity::sensor::level::name%]" - }, - "equipment": { - "name": "Battle gear" - }, - "class": { - "name": "Class equip bonus" - }, - "allocated": { - "name": "Allocated attribute points" - }, - "buffs": { - "name": "Buffs" - } - } - }, - "intelligence": { - "name": "Intelligence", - "state_attributes": { - "level": { - "name": "[%key:component::habitica::entity::sensor::level::name%]" - }, - "equipment": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]" - }, - "class": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]" - }, - "allocated": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]" - }, - "buffs": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]" - } - } - }, - "perception": { - "name": "Perception", - "state_attributes": { - "level": { - "name": "[%key:component::habitica::entity::sensor::level::name%]" - }, - "equipment": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]" - }, - "class": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]" - }, - "allocated": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]" - }, - "buffs": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]" - } - } - }, - "constitution": { - "name": "Constitution", - "state_attributes": { - "level": { - "name": "[%key:component::habitica::entity::sensor::level::name%]" - }, - "equipment": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]" - }, - "class": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]" - }, - "allocated": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]" - }, - "buffs": { - "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]" - } - } } }, "switch": { @@ -301,9 +220,6 @@ "not_enough_mana": { "message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}." }, - "not_enough_gold": { - "message": "Unable to buy reward, not enough gold. Your character has {gold}, but the reward costs {cost}." - }, "skill_not_found": { "message": "Unable to cast skill, your character does not have the skill or spell {skill}." }, @@ -314,23 +230,13 @@ "message": "The selected character is currently not loaded or disabled in Home Assistant." }, "task_not_found": { - "message": "Unable to complete action, could not find the task {task}" - }, - "quest_action_unallowed": { - "message": "Action not allowed, only quest leader or group leader can perform this action" - }, - "quest_not_found": { - "message": "Unable to complete action, quest or group not found" + "message": "Unable to cast skill, could not find the task {task}" } }, "issues": { "deprecated_task_entity": { "title": "The Habitica {task_name} sensor is deprecated", "description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts to replace the sensor entity with the newly added todo entity.\nWhen you are done migrating you can disable `{entity}`." - }, - "deprecated_api_call": { - "title": "The Habitica action habitica.api_call is deprecated", - "description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities." } }, "services": { @@ -357,7 +263,7 @@ "description": "Use a skill or spell from your Habitica character on a specific task to affect its progress or status.", "fields": { "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", + "name": "Select character", "description": "Choose the Habitica character to cast the skill." }, "skill": { @@ -369,98 +275,6 @@ "description": "The name (or task ID) of the task you want to target with the skill or spell." } } - }, - "accept_quest": { - "name": "Accept a quest invitation", - "description": "Accept a pending invitation to a quest.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "Choose the Habitica character for which to perform the action." - } - } - }, - "reject_quest": { - "name": "Reject a quest invitation", - "description": "Reject a pending invitation to a quest.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]" - } - } - }, - "leave_quest": { - "name": "Leave a quest", - "description": "Leave the current quest you are participating in.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]" - } - } - }, - "abort_quest": { - "name": "Abort an active quest", - "description": "Terminate your party's ongoing quest. All progress will be lost and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]" - } - } - }, - "cancel_quest": { - "name": "Cancel a pending quest", - "description": "Cancel a quest that has not yet startet. All accepted and pending invitations will be canceled and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]" - } - } - }, - "start_quest": { - "name": "Force-start a pending quest", - "description": "Begin the quest immediately, bypassing any pending invitations that haven't been accepted or rejected. Only quest leader or group leader can perform this action.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]" - } - } - }, - "score_habit": { - "name": "Track a habit", - "description": "Increase the positive or negative streak of a habit to track its progress.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "Select the Habitica character tracking your habit." - }, - "task": { - "name": "Habit name", - "description": "The name (or task ID) of the Habitica habit." - }, - "direction": { - "name": "Reward or loss", - "description": "Is it positive or negative progress you want to track for your habit." - } - } - }, - "score_reward": { - "name": "Buy a reward", - "description": "Reward yourself and buy one of your custom rewards with gold earned by fulfilling tasks.", - "fields": { - "config_entry": { - "name": "[%key:component::habitica::common::config_entry_name%]", - "description": "Select the Habitica character buying the reward." - }, - "task": { - "name": "Reward name", - "description": "The name (or task ID) of the custom reward." - } - } } }, "selector": { diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 03acb08baf9..93a7c234a5d 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -3,7 +3,6 @@ from __future__ import annotations import datetime -from math import floor from typing import TYPE_CHECKING, Any from dateutil.rrule import ( @@ -140,52 +139,3 @@ def get_recurrence_rule(recurrence: rrule) -> str: """ return str(recurrence).split("RRULE:")[1] - - -def get_attribute_points( - user: dict[str, Any], content: dict[str, Any], attribute: str -) -> dict[str, float]: - """Get modifiers contributing to strength attribute.""" - - gear_set = { - "weapon", - "armor", - "head", - "shield", - "back", - "headAccessory", - "eyewear", - "body", - } - - equipment = sum( - stats[attribute] - for gear in gear_set - if (equipped := user["items"]["gear"]["equipped"].get(gear)) - and (stats := content["gear"]["flat"].get(equipped)) - ) - - class_bonus = sum( - stats[attribute] / 2 - for gear in gear_set - if (equipped := user["items"]["gear"]["equipped"].get(gear)) - and (stats := content["gear"]["flat"].get(equipped)) - and stats["klass"] == user["stats"]["class"] - ) - - return { - "level": min(round(user["stats"]["lvl"] / 2), 50), - "equipment": equipment, - "class": class_bonus, - "allocated": user["stats"][attribute], - "buffs": user["stats"]["buffs"][attribute], - } - - -def get_attributes_total( - user: dict[str, Any], content: dict[str, Any], attribute: str -) -> int: - """Get total attribute points.""" - return floor( - sum(value for value in get_attribute_points(user, content, attribute).values()) - ) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 3a3eb0e945c..2a690ca4819 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -284,23 +284,32 @@ def _is_websocket(request: web.Request) -> bool: ) +_CLOSE_TYPES = { + aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.CLOSING, + aiohttp.WSMsgType.CLOSED, +} + + async def _websocket_forward( ws_from: web.WebSocketResponse | ClientWebSocketResponse, ws_to: web.WebSocketResponse | ClientWebSocketResponse, ) -> None: """Handle websocket message directly.""" try: - async for msg in ws_from: - if msg.type is aiohttp.WSMsgType.TEXT: + while msg := await ws_from.receive(): + msg_type = msg.type + if msg_type is aiohttp.WSMsgType.TEXT: await ws_to.send_str(msg.data) - elif msg.type is aiohttp.WSMsgType.BINARY: + elif msg_type is aiohttp.WSMsgType.BINARY: await ws_to.send_bytes(msg.data) - elif msg.type is aiohttp.WSMsgType.PING: + elif msg_type is aiohttp.WSMsgType.PING: await ws_to.ping() - elif msg.type is aiohttp.WSMsgType.PONG: + elif msg_type is aiohttp.WSMsgType.PONG: await ws_to.pong() - elif ws_to.closed: + elif msg_type in _CLOSE_TYPES: await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type] + break except RuntimeError: _LOGGER.debug("Ingress Websocket runtime error") except ConnectionResetError: diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 232b581d58b..f044a3fdfb4 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import ( IssueSeverity, @@ -193,32 +192,11 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor): async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" await super().async_added_to_hass() - automations = automations_with_entity(self.hass, self.entity_id) - scripts = scripts_with_entity(self.hass, self.entity_id) - items = automations + scripts + entity_automations = automations_with_entity(self.hass, self.entity_id) + entity_scripts = scripts_with_entity(self.hass, self.entity_id) + items = entity_automations + entity_scripts if not items: return - - entity_reg: er.EntityRegistry = er.async_get(self.hass) - entity_automations = [ - automation_entity - for automation_id in automations - if (automation_entity := entity_reg.async_get(automation_id)) - ] - entity_scripts = [ - script_entity - for script_id in scripts - if (script_entity := entity_reg.async_get(script_id)) - ] - - items_list = [ - f"- [{item.original_name}](/config/automation/edit/{item.unique_id})" - for item in entity_automations - ] + [ - f"- [{item.original_name}](/config/script/edit/{item.unique_id})" - for item in entity_scripts - ] - async_create_issue( self.hass, DOMAIN, @@ -229,7 +207,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor): translation_key="deprecated_binary_common_door_sensor", translation_placeholders={ "entity": self.entity_id, - "items": "\n".join(items_list), + "items": "\n".join([f"- {item}" for item in items]), }, ) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 2b08031405f..14ae57391ef 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -318,6 +318,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.start_task: asyncio.Task | None = None self.stop_task: asyncio.Task | None = None self._zha_migration_mgr: ZhaMultiPANMigrationHelper | None = None + self.config_entry = config_entry self.original_addon_config: dict[str, Any] | None = None self.revert_reason: str | None = None diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 8634589cb5f..9f3f183f11f 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -18,8 +18,6 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, - STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import State, callback @@ -154,12 +152,12 @@ class SecuritySystem(HomeAccessory): @callback def async_update_state(self, new_state: State) -> None: """Update security state after state changed.""" - hass_state: str | AlarmControlPanelState = new_state.state - if hass_state in {"None", STATE_UNKNOWN, STATE_UNAVAILABLE}: - # Bail out early for no state, unknown or unavailable + hass_state = None + if new_state and new_state.state == "None": + # Bail out early for no state return - if hass_state is not None: - hass_state = AlarmControlPanelState(hass_state) + if new_state and new_state.state is not None: + hass_state = AlarmControlPanelState(new_state.state) if ( hass_state and (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None 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/manifest.json b/homeassistant/components/huum/manifest.json index 38562e1a072..cc393f3785f 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"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index bb8c33ba749..963721a0476 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==11.0.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 11c99a7428f..d589c117edd 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["pyiqvia"], - "requirements": ["numpy==2.1.3", "pyiqvia==2022.04.0"] + "requirements": ["numpy==2.1.2", "pyiqvia==2022.04.0"] } diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 823e9bd59be..4598cf7cd91 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -5,17 +5,25 @@ from __future__ import annotations from functools import partial from hdate import Location +import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ELEVATION, CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, CONF_TIME_ZONE, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType +from .binary_sensor import BINARY_SENSORS from .const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, @@ -24,11 +32,93 @@ from .const import ( DEFAULT_DIASPORA, DEFAULT_HAVDALAH_OFFSET_MINUTES, DEFAULT_LANGUAGE, + DEFAULT_NAME, + DOMAIN, ) from .entity import JewishCalendarConfigEntry, JewishCalendarData +from .sensor import INFO_SENSORS, TIME_SENSORS PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.deprecated(DOMAIN), + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DIASPORA, default=DEFAULT_DIASPORA): cv.boolean, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In( + ["hebrew", "english"] + ), + vol.Optional( + CONF_CANDLE_LIGHT_MINUTES, default=DEFAULT_CANDLE_LIGHT + ): int, + # Default of 0 means use 8.5 degrees / 'three_stars' time. + vol.Optional( + CONF_HAVDALAH_OFFSET_MINUTES, + default=DEFAULT_HAVDALAH_OFFSET_MINUTES, + ): int, + }, + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def get_unique_prefix( + location: Location, + language: str, + candle_lighting_offset: int | None, + havdalah_offset: int | None, +) -> str: + """Create a prefix for unique ids.""" + # location.altitude was unset before 2024.6 when this method + # was used to create the unique id. As such it would always + # use the default altitude of 754. + config_properties = [ + location.latitude, + location.longitude, + location.timezone, + 754, + location.diaspora, + language, + candle_lighting_offset, + havdalah_offset, + ] + prefix = "_".join(map(str, config_properties)) + return f"{prefix}" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Jewish Calendar component.""" + if DOMAIN not in config: + return True + + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + issue_domain=DOMAIN, + breaks_in_ha_version="2024.12.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": DEFAULT_NAME, + }, + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + async def async_setup_entry( hass: HomeAssistant, config_entry: JewishCalendarConfigEntry @@ -63,6 +153,16 @@ async def async_setup_entry( havdalah_offset, ) + # Update unique ID to be unrelated to user defined options + old_prefix = get_unique_prefix( + location, language, candle_lighting_offset, havdalah_offset + ) + + ent_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) + if not entries or any(entry.unique_id.startswith(old_prefix) for entry in entries): + async_update_unique_ids(ent_reg, config_entry.entry_id, old_prefix) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async def update_listener( @@ -80,3 +180,25 @@ async def async_unload_entry( ) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + +@callback +def async_update_unique_ids( + ent_reg: er.EntityRegistry, new_prefix: str, old_prefix: str +) -> None: + """Update unique ID to be unrelated to user defined options. + + Introduced with release 2024.6 + """ + platform_descriptions = { + Platform.BINARY_SENSOR: BINARY_SENSORS, + Platform.SENSOR: (*INFO_SENSORS, *TIME_SENSORS), + } + for platform, descriptions in platform_descriptions.items(): + for description in descriptions: + new_unique_id = f"{new_prefix}-{description.key}" + old_unique_id = f"{old_prefix}_{description.key}" + if entity_id := ent_reg.async_get_entity_id( + platform, DOMAIN, old_unique_id + ): + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py index a2eadbf57bd..9673fc6cf22 100644 --- a/homeassistant/components/jewish_calendar/config_flow.py +++ b/homeassistant/components/jewish_calendar/config_flow.py @@ -101,10 +101,23 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: + _options = {} + if CONF_CANDLE_LIGHT_MINUTES in user_input: + _options[CONF_CANDLE_LIGHT_MINUTES] = user_input[ + CONF_CANDLE_LIGHT_MINUTES + ] + del user_input[CONF_CANDLE_LIGHT_MINUTES] + if CONF_HAVDALAH_OFFSET_MINUTES in user_input: + _options[CONF_HAVDALAH_OFFSET_MINUTES] = user_input[ + CONF_HAVDALAH_OFFSET_MINUTES + ] + del user_input[CONF_HAVDALAH_OFFSET_MINUTES] if CONF_LOCATION in user_input: user_input[CONF_LATITUDE] = user_input[CONF_LOCATION][CONF_LATITUDE] user_input[CONF_LONGITUDE] = user_input[CONF_LOCATION][CONF_LONGITUDE] - return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + return self.async_create_entry( + title=DEFAULT_NAME, data=user_input, options=_options + ) return self.async_show_form( step_id="user", @@ -113,6 +126,10 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): ), ) + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_data) + async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 67de34f2fce..fbbfb03fb3e 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -17,7 +17,6 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, - EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -748,15 +747,6 @@ SENSOR_PROCESS_DATA = [ state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), - PlenticoreSensorEntityDescription( - module_id="scb:event", - key="Event:ActiveErrorCnt", - name="Active Alarms", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - icon="mdi:alert", - formatter="format_round", - ), PlenticoreSensorEntityDescription( module_id="_virt_", key="pv_P", diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index da513bc8cff..82a91c0003f 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -2,12 +2,12 @@ import logging +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient +from lmcloud.const import BT_MODEL_PREFIXES, FirmwareType +from lmcloud.exceptions import AuthFail, RequestNotSuccessful from packaging import version -from pylamarzocco.client_bluetooth import LaMarzoccoBluetoothClient -from pylamarzocco.client_cloud import LaMarzoccoCloudClient -from pylamarzocco.client_local import LaMarzoccoLocalClient -from pylamarzocco.const import BT_MODEL_PREFIXES, FirmwareType -from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 444e4d0723b..c48453214bd 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass -from pylamarzocco.models import LaMarzoccoMachineConfig +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index ae79e21897f..60374a85e1e 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -1,11 +1,11 @@ """Button platform for La Marzocco espresso machines.""" -import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.exceptions import RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant @@ -13,11 +13,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator +from .coordinator import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription -BACKFLUSH_ENABLED_DURATION = 15 - @dataclass(frozen=True, kw_only=True) class LaMarzoccoButtonEntityDescription( @@ -26,25 +24,14 @@ class LaMarzoccoButtonEntityDescription( ): """Description of a La Marzocco button.""" - press_fn: Callable[[LaMarzoccoUpdateCoordinator], Coroutine[Any, Any, None]] - - -async def async_backflush_and_update(coordinator: LaMarzoccoUpdateCoordinator) -> None: - """Press backflush button.""" - await coordinator.device.start_backflush() - # lib will set state optimistically - coordinator.async_set_updated_data(None) - # backflush is enabled for 15 seconds - # then turns off automatically - await asyncio.sleep(BACKFLUSH_ENABLED_DURATION + 1) - await coordinator.async_request_refresh() + press_fn: Callable[[LaMarzoccoMachine], Coroutine[Any, Any, None]] ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = ( LaMarzoccoButtonEntityDescription( key="start_backflush", translation_key="start_backflush", - press_fn=async_backflush_and_update, + press_fn=lambda machine: machine.start_backflush(), ), ) @@ -72,7 +59,7 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): async def async_press(self) -> None: """Press button.""" try: - await self.entity_description.press_fn(self.coordinator) + await self.entity_description.press_fn(self.coordinator.device) except RequestNotSuccessful as exc: raise HomeAssistantError( translation_domain=DOMAIN, @@ -81,3 +68,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): "key": self.entity_description.key, }, ) from exc + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/lamarzocco/calendar.py b/homeassistant/components/lamarzocco/calendar.py index 0ec9b55a9a1..3d8b2474c94 100644 --- a/homeassistant/components/lamarzocco/calendar.py +++ b/homeassistant/components/lamarzocco/calendar.py @@ -3,7 +3,7 @@ from collections.abc import Iterator from datetime import datetime, timedelta -from pylamarzocco.models import LaMarzoccoWakeUpSleepEntry +from lmcloud.models import LaMarzoccoWakeUpSleepEntry from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 04e705edbdc..4fadd3a9a32 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -6,10 +6,10 @@ from collections.abc import Mapping import logging from typing import Any -from pylamarzocco.client_cloud import LaMarzoccoCloudClient -from pylamarzocco.client_local import LaMarzoccoLocalClient -from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful -from pylamarzocco.models import LaMarzoccoDeviceInfo +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient +from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo import voluptuous as vol from homeassistant.components.bluetooth import ( diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index 05fee98c599..e2ff8791a05 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -8,11 +8,11 @@ import logging from time import time from typing import Any -from pylamarzocco.client_bluetooth import LaMarzoccoBluetoothClient -from pylamarzocco.client_cloud import LaMarzoccoCloudClient -from pylamarzocco.client_local import LaMarzoccoLocalClient -from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful -from pylamarzocco.lm_machine import LaMarzoccoMachine +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient +from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 43ae51ee192..edce6a349aa 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import asdict from typing import Any, TypedDict -from pylamarzocco.const import FirmwareType +from lmcloud.const import FirmwareType from homeassistant.components.diagnostics import async_redact_data from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py index 1ea84302a17..f7e6ff9e2b8 100644 --- a/homeassistant/components/lamarzocco/entity.py +++ b/homeassistant/components/lamarzocco/entity.py @@ -3,8 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from pylamarzocco.const import FirmwareType -from pylamarzocco.lm_machine import LaMarzoccoMachine +from lmcloud.const import FirmwareType +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 6b226051118..bfe0d34a9e4 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -32,6 +32,6 @@ "documentation": "https://www.home-assistant.io/integrations/lamarzocco", "integration_type": "device", "iot_class": "cloud_polling", - "loggers": ["pylamarzocco"], - "requirements": ["pylamarzocco==1.2.3"] + "loggers": ["lmcloud"], + "requirements": ["lmcloud==1.2.3"] } diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 825c5d6deb0..df75147e7e1 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -4,16 +4,16 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from pylamarzocco.const import ( +from lmcloud.const import ( KEYS_PER_MODEL, BoilerType, MachineModel, PhysicalKey, PrebrewMode, ) -from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.lm_machine import LaMarzoccoMachine -from pylamarzocco.models import LaMarzoccoMachineConfig +from lmcloud.exceptions import RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.number import ( NumberDeviceClass, diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 1889ba38d6b..1958fa6f210 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -4,10 +4,10 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from pylamarzocco.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel -from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.lm_machine import LaMarzoccoMachine -from pylamarzocco.models import LaMarzoccoMachineConfig +from lmcloud.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel +from lmcloud.exceptions import RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 04b095e798c..ca8a118c1ee 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -3,8 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from pylamarzocco.const import BoilerType, MachineModel, PhysicalKey -from pylamarzocco.lm_machine import LaMarzoccoMachine +from lmcloud.const import BoilerType, MachineModel, PhysicalKey +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index f7690885f05..a611424418f 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -4,10 +4,10 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from pylamarzocco.const import BoilerType -from pylamarzocco.exceptions import RequestNotSuccessful -from pylamarzocco.lm_machine import LaMarzoccoMachine -from pylamarzocco.models import LaMarzoccoMachineConfig +from lmcloud.const import BoilerType +from lmcloud.exceptions import RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index 371ff679bae..61f436a7d7f 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -3,8 +3,8 @@ from dataclasses import dataclass from typing import Any -from pylamarzocco.const import FirmwareType -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.const import FirmwareType +from lmcloud.exceptions import RequestNotSuccessful from homeassistant.components.update import ( UpdateDeviceClass, diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index eb26ef48e4e..5995e06efcc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -8,7 +8,7 @@ import logging import pypck from pypck.connection import PchkConnectionManager -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, @@ -20,7 +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 import device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import ( @@ -39,29 +39,40 @@ from .helpers import ( InputType, async_update_config_entry, generate_unique_id, + import_lcn_config, register_lcn_address_devices, register_lcn_host_device, ) -from .services import register_services +from .schemas import CONFIG_SCHEMA # noqa: F401 +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, {}) + if DOMAIN not in config: + return True - await register_services(hass) - await register_panel_and_ws_api(hass) + # initialize a config_flow for all LCN configurations read from + # configuration.yaml + config_entries_data = import_lcn_config(config[DOMAIN]) + for config_entry_data in config_entries_data: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_entry_data, + ) + ) 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 +132,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 +191,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/config_flow.py b/homeassistant/components/lcn/config_flow.py index 008265e62ae..e78378a61b1 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -9,6 +9,7 @@ import pypck import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import ( CONF_BASE, CONF_DEVICES, @@ -19,12 +20,14 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from . import PchkConnectionManager from .const import CONF_ACKNOWLEDGE, CONF_DIM_MODE, CONF_SK_NUM_TRIES, DIM_MODES, DOMAIN +from .helpers import purge_device_registry, purge_entity_registry _LOGGER = logging.getLogger(__name__) @@ -110,6 +113,55 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 MINOR_VERSION = 1 + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import existing configuration from LCN.""" + # validate the imported connection parameters + if error := await validate_connection(import_data): + async_create_issue( + self.hass, + DOMAIN, + error, + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.ERROR, + translation_key=error, + translation_placeholders={ + "url": "/config/integrations/dashboard/add?domain=lcn" + }, + ) + return self.async_abort(reason=error) + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LCN", + }, + ) + + # check if we already have a host with the same address configured + if entry := get_config_entry(self.hass, import_data): + entry.source = config_entries.SOURCE_IMPORT + # Cleanup entity and device registry, if we imported from configuration.yaml to + # remove orphans when entities were removed from configuration + purge_entity_registry(self.hass, entry.entry_id, import_data) + purge_device_registry(self.hass, entry.entry_id, import_data) + + self.hass.config_entries.async_update_entry(entry, data=import_data) + return self.async_abort(reason="existing_configuration_updated") + + return self.async_create_entry( + title=f"{import_data[CONF_HOST]}", data=import_data + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> config_entries.ConfigFlowResult: diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 6a9c63ea212..7da047682ac 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -9,6 +9,7 @@ import re from typing import cast import pypck +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,12 +19,17 @@ from homeassistant.const import ( CONF_DEVICES, CONF_DOMAIN, CONF_ENTITIES, + CONF_HOST, + CONF_IP_ADDRESS, CONF_LIGHTS, CONF_NAME, + CONF_PASSWORD, + CONF_PORT, CONF_RESOURCE, CONF_SENSORS, CONF_SOURCE, CONF_SWITCHES, + CONF_USERNAME, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -31,13 +37,19 @@ from homeassistant.helpers.typing import ConfigType from .const import ( BINSENSOR_PORTS, + CONF_ACKNOWLEDGE, CONF_CLIMATES, + CONF_CONNECTIONS, + CONF_DIM_MODE, + CONF_DOMAIN_DATA, CONF_HARDWARE_SERIAL, CONF_HARDWARE_TYPE, CONF_OUTPUT, CONF_SCENES, + CONF_SK_NUM_TRIES, CONF_SOFTWARE_SERIAL, CONNECTION, + DEFAULT_NAME, DOMAIN, LED_PORTS, LOGICOP_PORTS, @@ -134,6 +146,110 @@ def generate_unique_id( return unique_id +def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]: + """Convert lcn settings from configuration.yaml to config_entries data. + + Create a list of config_entry data structures like: + + "data": { + "host": "pchk", + "ip_address": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn, + "sk_num_tries: 0, + "dim_mode: "STEPS200", + "acknowledge": False, + "devices": [ + { + "address": (0, 7, False) + "name": "", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + }, ... + ], + "entities": [ + { + "address": (0, 7, False) + "name": "Light_Output1", + "resource": "output1", + "domain": "light", + "domain_data": { + "output": "OUTPUT1", + "dimmable": True, + "transition": 5000.0 + } + }, ... + ] + } + """ + data = {} + for connection in lcn_config[CONF_CONNECTIONS]: + host = { + CONF_HOST: connection[CONF_NAME], + CONF_IP_ADDRESS: connection[CONF_HOST], + CONF_PORT: connection[CONF_PORT], + CONF_USERNAME: connection[CONF_USERNAME], + CONF_PASSWORD: connection[CONF_PASSWORD], + CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES], + CONF_DIM_MODE: connection[CONF_DIM_MODE], + CONF_ACKNOWLEDGE: False, + CONF_DEVICES: [], + CONF_ENTITIES: [], + } + data[connection[CONF_NAME]] = host + + for confkey, domain_config in lcn_config.items(): + if confkey == CONF_CONNECTIONS: + continue + domain = DOMAIN_LOOKUP[confkey] + # loop over entities in configuration.yaml + for domain_data in domain_config: + # remove name and address from domain_data + entity_name = domain_data.pop(CONF_NAME) + address, host_name = domain_data.pop(CONF_ADDRESS) + + if host_name is None: + host_name = DEFAULT_NAME + + # check if we have a new device config + for device_config in data[host_name][CONF_DEVICES]: + if address == device_config[CONF_ADDRESS]: + break + else: # create new device_config + device_config = { + CONF_ADDRESS: address, + CONF_NAME: "", + CONF_HARDWARE_SERIAL: -1, + CONF_SOFTWARE_SERIAL: -1, + CONF_HARDWARE_TYPE: -1, + } + + data[host_name][CONF_DEVICES].append(device_config) + + # insert entity config + resource = get_resource(domain, domain_data).lower() + for entity_config in data[host_name][CONF_ENTITIES]: + if ( + address == entity_config[CONF_ADDRESS] + and resource == entity_config[CONF_RESOURCE] + and domain == entity_config[CONF_DOMAIN] + ): + break + else: # create new entity_config + entity_config = { + CONF_ADDRESS: address, + CONF_NAME: entity_name, + CONF_RESOURCE: resource, + CONF_DOMAIN: domain, + CONF_DOMAIN_DATA: domain_data.copy(), + } + data[host_name][CONF_ENTITIES].append(entity_config) + + return list(data.values()) + + def purge_entity_registry( hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType ) -> None: @@ -320,6 +436,26 @@ def get_device_config( return None +def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]: + """Validate that all connection names are unique. + + Use 'pchk' as default connection_name (or add a numeric suffix if + pchk' is already in use. + """ + suffix = 0 + for host in hosts: + if host.get(CONF_NAME) is None: + if suffix == 0: + host[CONF_NAME] = DEFAULT_NAME + else: + host[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" + suffix += 1 + + schema = vol.Schema(vol.Unique()) + schema([host.get(CONF_NAME) for host in hosts]) + return hosts + + def is_address(value: str) -> tuple[AddressType, str]: """Validate the given address string. diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 695a35df871..6ce41a2d08d 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.7.24", "lcn-frontend==0.2.2"] + "requirements": ["pypck==0.7.24", "lcn-frontend==0.2.1"] } diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index c9c91b9843d..3b4d2333970 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -4,9 +4,20 @@ import voluptuous as vol from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( + CONF_ADDRESS, + CONF_BINARY_SENSORS, + CONF_COVERS, + CONF_HOST, + CONF_LIGHTS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, CONF_SCENE, + CONF_SENSORS, CONF_SOURCE, + CONF_SWITCHES, CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, UnitOfTemperature, ) import homeassistant.helpers.config_validation as cv @@ -14,6 +25,9 @@ from homeassistant.helpers.typing import VolDictType from .const import ( BINSENSOR_PORTS, + CONF_CLIMATES, + CONF_CONNECTIONS, + CONF_DIM_MODE, CONF_DIMMABLE, CONF_LOCKABLE, CONF_MAX_TEMP, @@ -23,8 +37,12 @@ from .const import ( CONF_OUTPUTS, CONF_REGISTER, CONF_REVERSE_TIME, + CONF_SCENES, CONF_SETPOINT, + CONF_SK_NUM_TRIES, CONF_TRANSITION, + DIM_MODES, + DOMAIN, KEYS, LED_PORTS, LOGICOP_PORTS, @@ -38,6 +56,7 @@ from .const import ( VAR_UNITS, VARIABLES, ) +from .helpers import has_unique_host_names, is_address ADDRESS_SCHEMA = vol.Coerce(tuple) @@ -111,3 +130,72 @@ DOMAIN_DATA_SWITCH: VolDictType = { vol.In(OUTPUT_PORTS + RELAY_PORTS + SETPOINTS + KEYS), ), } + + +# +# Configuration +# + +DOMAIN_DATA_BASE: VolDictType = { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, +} + +BINARY_SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_BINARY_SENSOR}) + +CLIMATES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_CLIMATE}) + +COVERS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_COVER}) + +LIGHTS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_LIGHT}) + +SCENES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SCENE}) + +SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SENSOR}) + +SWITCHES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SWITCH}) + +CONNECTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int, + vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All( + vol.Upper, vol.In(DIM_MODES) + ), + vol.Optional(CONF_NAME): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CONNECTIONS): vol.All( + cv.ensure_list, has_unique_host_names, [CONNECTION_SCHEMA] + ), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSORS_SCHEMA] + ), + vol.Optional(CONF_CLIMATES): vol.All( + cv.ensure_list, [CLIMATES_SCHEMA] + ), + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [SENSORS_SCHEMA] + ), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [SWITCHES_SCHEMA] + ), + }, + ) + }, + ), + extra=vol.ALLOW_EXTRA, +) 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/lcn/strings.json b/homeassistant/components/lcn/strings.json index 088a3654500..ae0b1b01f9a 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -63,6 +63,18 @@ } }, "issues": { + "authentication_error": { + "title": "Authentication failed.", + "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure username and password are correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "license_error": { + "title": "Maximum number of connections was reached.", + "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure sufficient PCHK licenses are registered and restart Home Assistant.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "connection_refused": { + "title": "Unable to connect to PCHK.", + "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the connection (IP and port) to the LCN bus coupler is correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, "deprecated_regulatorlock_sensor": { "title": "Deprecated LCN regulator lock binary sensor", "description": "Your LCN regulator lock binary sensor entity `{entity}` is beeing used in automations or scripts. A regulator lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue." diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py index edcadf2598a..187cc74b3eb 100644 --- a/homeassistant/components/lg_thinq/fan.py +++ b/homeassistant/components/lg_thinq/fan.py @@ -72,11 +72,8 @@ class ThinQFanEntity(ThinQEntity, FanEntity): super().__init__(coordinator, entity_description, property_id) self._ordered_named_fan_speeds = [] - self._attr_supported_features = ( - FanEntityFeature.SET_SPEED - | FanEntityFeature.TURN_ON - | FanEntityFeature.TURN_OFF - ) + self._attr_supported_features |= FanEntityFeature.SET_SPEED + if (fan_modes := self.data.fan_modes) is not None: self._attr_speed_count = len(fan_modes) if self.speed_count == 4: @@ -101,7 +98,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): self._attr_percentage = 0 _LOGGER.debug( - "[%s:%s] update status: %s -> %s (percentage=%s)", + "[%s:%s] update status: %s -> %s (percntage=%s)", self.coordinator.device_name, self.property_id, self.data.is_on, @@ -123,7 +120,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): return _LOGGER.debug( - "[%s:%s] async_set_percentage. percentage=%s, value=%s", + "[%s:%s] async_set_percentage. percntage=%s, value=%s", self.coordinator.device_name, self.property_id, percentage, diff --git a/homeassistant/components/linkplay/diagnostics.py b/homeassistant/components/linkplay/diagnostics.py deleted file mode 100644 index cfc1346aff4..00000000000 --- a/homeassistant/components/linkplay/diagnostics.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Diagnostics support for Linkplay.""" - -from __future__ import annotations - -from typing import Any - -from homeassistant.core import HomeAssistant - -from . import LinkPlayConfigEntry - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: LinkPlayConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - data = entry.runtime_data - return {"device_info": data.bridge.to_dict()} diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index e74d22b8207..9ddb6abf093 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.0.20"], + "requirements": ["python-linkplay==0.0.18"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index c29c2978522..36834610c04 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -9,7 +9,7 @@ from typing import Any, Concatenate from linkplay.bridge import LinkPlayBridge from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus from linkplay.controller import LinkPlayController, LinkPlayMultiroom -from linkplay.exceptions import LinkPlayRequestException +from linkplay.exceptions import LinkPlayException, LinkPlayRequestException import voluptuous as vol from homeassistant.components import media_source @@ -69,8 +69,6 @@ SOURCE_MAP: dict[PlayingMode, str] = { PlayingMode.FM: "FM Radio", PlayingMode.RCA: "RCA", PlayingMode.UDISK: "USB", - PlayingMode.SPOTIFY: "Spotify", - PlayingMode.TIDAL: "Tidal", PlayingMode.FOLLOWER: "Follower", } @@ -203,8 +201,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): try: await self._bridge.player.update_status() self._update_properties() - except LinkPlayRequestException: + except LinkPlayException: self._attr_available = False + raise @exception_wrap async def async_select_source(self, source: str) -> None: @@ -293,15 +292,7 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): @exception_wrap async def async_play_preset(self, preset_number: int) -> None: """Play preset number.""" - try: - await self._bridge.player.play_preset(preset_number) - except ValueError as err: - raise HomeAssistantError(err) from err - - @exception_wrap - async def async_media_seek(self, position: float) -> None: - """Seek to a position.""" - await self._bridge.player.seek(round(position)) + await self._bridge.player.play_preset(preset_number) @exception_wrap async def async_join_players(self, group_members: list[str]) -> None: @@ -388,9 +379,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): ) self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other") - self._attr_media_position = self._bridge.player.current_position_in_seconds + self._attr_media_position = self._bridge.player.current_position / 1000 self._attr_media_position_updated_at = utcnow() - self._attr_media_duration = self._bridge.player.total_length_in_seconds + self._attr_media_duration = self._bridge.player.total_length / 1000 self._attr_media_artist = self._bridge.player.artist self._attr_media_title = self._bridge.player.title self._attr_media_album_name = self._bridge.player.album diff --git a/homeassistant/components/linkplay/services.yaml b/homeassistant/components/linkplay/services.yaml index 0d7335a28c8..20bc47be7a7 100644 --- a/homeassistant/components/linkplay/services.yaml +++ b/homeassistant/components/linkplay/services.yaml @@ -11,4 +11,5 @@ play_preset: selector: number: min: 1 + max: 10 mode: box diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 43c151c7c23..520bd0550cc 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.25.2", "Pillow==11.0.0"] + "requirements": ["matrix-nio==0.25.2", "Pillow==10.4.0"] } diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 3ea8f581245..604f9b7cc88 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -18,7 +18,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.frame import report_usage +from homeassistant.helpers.frame import report from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) @@ -156,7 +156,7 @@ async def async_resolve_media( raise Unresolvable("Media Source not loaded") if target_media_player is UNDEFINED: - report_usage( + report( "calls media_source.async_resolve_media without passing an entity_id", exclude_integrations={DOMAIN}, ) 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/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index 33e814efb51..dee08736234 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -9,13 +9,11 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) - class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a ModernForms config flow.""" @@ -57,21 +55,17 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, prepare: bool = False ) -> ConfigFlowResult: """Config flow handler for ModernForms.""" + source = self.context["source"] + # Request user input, unless we are preparing discovery flow if user_input is None: user_input = {} if not prepare: - if self.source == SOURCE_ZEROCONF: - return self.async_show_form( - step_id="zeroconf_confirm", - description_placeholders={"name": self.name}, - ) - return self.async_show_form( - step_id="user", - data_schema=USER_SCHEMA, - ) + if source == SOURCE_ZEROCONF: + return self._show_confirm_dialog() + return self._show_setup_form() - if self.source == SOURCE_ZEROCONF: + if source == SOURCE_ZEROCONF: user_input[CONF_HOST] = self.host user_input[CONF_MAC] = self.mac @@ -81,21 +75,18 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): try: device = await device.update() except ModernFormsConnectionError: - if self.source == SOURCE_ZEROCONF: + if source == SOURCE_ZEROCONF: return self.async_abort(reason="cannot_connect") - return self.async_show_form( - step_id="user", - data_schema=USER_SCHEMA, - errors={"base": "cannot_connect"}, - ) + return self._show_setup_form({"base": "cannot_connect"}) user_input[CONF_MAC] = device.info.mac_address + user_input[CONF_NAME] = device.info.device_name # Check if already configured await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) title = device.info.device_name - if self.source == SOURCE_ZEROCONF: + if source == SOURCE_ZEROCONF: title = self.name if prepare: @@ -105,3 +96,19 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) + + def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) + + def _show_confirm_dialog(self, errors: dict | None = None) -> ConfigFlowResult: + """Show the confirm dialog to the user.""" + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": self.name}, + errors=errors or {}, + ) 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/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index 953859986d0..0ba6ac7b078 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -12,12 +12,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator -from .const import F_SERIES from .entity import MyUplinkEntity, MyUplinkSystemEntity -from .helpers import find_matching_platform, transform_model_series +from .helpers import find_matching_platform CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, BinarySensorEntityDescription]] = { - F_SERIES: { + "F730": { "43161": BinarySensorEntityDescription( key="elect_add", translation_key="elect_add", @@ -51,7 +50,6 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - prefix = transform_model_series(prefix) return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) diff --git a/homeassistant/components/myuplink/const.py b/homeassistant/components/myuplink/const.py index 6fd354a21ec..3541a8078c3 100644 --- a/homeassistant/components/myuplink/const.py +++ b/homeassistant/components/myuplink/const.py @@ -6,5 +6,3 @@ API_ENDPOINT = "https://api.myuplink.com" OAUTH2_AUTHORIZE = "https://api.myuplink.com/oauth/authorize" OAUTH2_TOKEN = "https://api.myuplink.com/oauth/token" OAUTH2_SCOPES = ["WRITESYSTEM", "READSYSTEM", "offline_access"] - -F_SERIES = "f-series" diff --git a/homeassistant/components/myuplink/helpers.py b/homeassistant/components/myuplink/helpers.py index de5486d8dea..eb4881c410e 100644 --- a/homeassistant/components/myuplink/helpers.py +++ b/homeassistant/components/myuplink/helpers.py @@ -6,8 +6,6 @@ from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import Platform -from .const import F_SERIES - def find_matching_platform( device_point: DevicePoint, @@ -88,9 +86,8 @@ PARAMETER_ID_TO_EXCLUDE_F730 = ( "47941", "47975", "48009", + "48042", "48072", - "48442", - "49909", "50113", ) @@ -113,7 +110,7 @@ def skip_entity(model: str, device_point: DevicePoint) -> bool: ): return False return True - if model.lower().startswith("f"): + if "F730" in model: # Entity names containing weekdays are used for advanced scheduling in the # heat pump and should not be exposed in the integration if any(d in device_point.parameter_name.lower() for d in WEEKDAYS): @@ -121,10 +118,3 @@ def skip_entity(model: str, device_point: DevicePoint) -> bool: if device_point.parameter_id in PARAMETER_ID_TO_EXCLUDE_F730: return True return False - - -def transform_model_series(prefix: str) -> str: - """Remap all F-series models.""" - if prefix.lower().startswith("f"): - return F_SERIES - return prefix diff --git a/homeassistant/components/myuplink/number.py b/homeassistant/components/myuplink/number.py index b05ab5d46c9..0c7da0c716f 100644 --- a/homeassistant/components/myuplink/number.py +++ b/homeassistant/components/myuplink/number.py @@ -10,9 +10,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator -from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity, transform_model_series +from .helpers import find_matching_platform, skip_entity DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = { "DM": NumberEntityDescription( @@ -23,7 +22,7 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = { } CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, NumberEntityDescription]] = { - F_SERIES: { + "F730": { "40940": NumberEntityDescription( key="degree_minutes", translation_key="degree_minutes", @@ -49,7 +48,6 @@ def get_description(device_point: DevicePoint) -> NumberEntityDescription | None 3. Default to None """ prefix, _, _ = device_point.category.partition(" ") - prefix = transform_model_series(prefix) description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( device_point.parameter_id ) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index ef827fc1fb1..7feb20bc093 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -25,9 +25,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import MyUplinkConfigEntry, MyUplinkDataCoordinator -from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity, transform_model_series +from .helpers import find_matching_platform, skip_entity DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "°C": SensorEntityDescription( @@ -140,7 +139,7 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { MARKER_FOR_UNKNOWN_VALUE = -32768 CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SensorEntityDescription]] = { - F_SERIES: { + "F730": { "43108": SensorEntityDescription( key="fan_mode", translation_key="fan_mode", @@ -201,7 +200,6 @@ def get_description(device_point: DevicePoint) -> SensorEntityDescription | None """ description = None prefix, _, _ = device_point.category.partition(" ") - prefix = transform_model_series(prefix) description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( device_point.parameter_id ) diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index 75ba6bd7819..5c47c8294fe 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -12,12 +12,11 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator -from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity, transform_model_series +from .helpers import find_matching_platform, skip_entity CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SwitchEntityDescription]] = { - F_SERIES: { + "F730": { "50004": SwitchEntityDescription( key="temporary_lux", translation_key="temporary_lux", @@ -48,7 +47,6 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - prefix = transform_model_series(prefix) return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 0a46d67a3ad..2bee54df3dd 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -2,9 +2,9 @@ from __future__ import annotations -from abc import ABC +from abc import ABC, abstractmethod import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable import datetime import functools import logging @@ -19,7 +19,6 @@ from google_nest_sdm.camera_traits import ( from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.exceptions import ApiException -from webrtc_models import RTCIceCandidate from homeassistant.components.camera import ( Camera, @@ -47,11 +46,6 @@ PLACEHOLDER = Path(__file__).parent / "placeholder.png" # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) -# Refresh streams with a bounded interval and backoff on failure -MIN_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=1) -MAX_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=10) -BACKOFF_MULTIPLIER = 1.5 - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -73,68 +67,6 @@ async def async_setup_entry( async_add_entities(entities) -class StreamRefresh: - """Class that will refresh an expiring stream. - - This class will schedule an alarm for the next expiration time of a stream. - When the alarm fires, it runs the provided `refresh_cb` to extend the - lifetime of the stream and return a new expiration time. - - A simple backoff will be applied when the refresh callback fails. - """ - - def __init__( - self, - hass: HomeAssistant, - expires_at: datetime.datetime, - refresh_cb: Callable[[], Awaitable[datetime.datetime | None]], - ) -> None: - """Initialize StreamRefresh.""" - self._hass = hass - self._unsub: Callable[[], None] | None = None - self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL - self._refresh_cb = refresh_cb - self._schedule_stream_refresh(expires_at - STREAM_EXPIRATION_BUFFER) - - def unsub(self) -> None: - """Invalidates the stream.""" - if self._unsub: - self._unsub() - - async def _handle_refresh(self, _: datetime.datetime) -> None: - """Alarm that fires to check if the stream should be refreshed.""" - self._unsub = None - try: - expires_at = await self._refresh_cb() - except ApiException as err: - _LOGGER.debug("Failed to refresh stream: %s", err) - # Increase backoff until the max backoff interval is reached - self._min_refresh_interval = min( - self._min_refresh_interval * BACKOFF_MULTIPLIER, - MAX_REFRESH_BACKOFF_INTERVAL, - ) - refresh_time = utcnow() + self._min_refresh_interval - else: - if expires_at is None: - return - self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL # Reset backoff - # Defend against invalid stream expiration time in the past - refresh_time = max( - expires_at - STREAM_EXPIRATION_BUFFER, - utcnow() + self._min_refresh_interval, - ) - self._schedule_stream_refresh(refresh_time) - - def _schedule_stream_refresh(self, refresh_time: datetime.datetime) -> None: - """Schedules an alarm to refresh any streams before expiration.""" - _LOGGER.debug("Scheduling stream refresh for %s", refresh_time) - self._unsub = async_track_point_in_utc_time( - self._hass, - self._handle_refresh, - refresh_time, - ) - - class NestCameraBaseEntity(Camera, ABC): """Devices that support cameras.""" @@ -154,6 +86,41 @@ class NestCameraBaseEntity(Camera, ABC): self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 # The API "name" field is a unique device identifier. self._attr_unique_id = f"{self._device.name}-camera" + self._stream_refresh_unsub: Callable[[], None] | None = None + + @abstractmethod + def _stream_expires_at(self) -> datetime.datetime | None: + """Next time when a stream expires.""" + + @abstractmethod + async def _async_refresh_stream(self) -> None: + """Refresh any stream to extend expiration time.""" + + def _schedule_stream_refresh(self) -> None: + """Schedules an alarm to refresh any streams before expiration.""" + if self._stream_refresh_unsub is not None: + self._stream_refresh_unsub() + + expiration_time = self._stream_expires_at() + if not expiration_time: + return + refresh_time = expiration_time - STREAM_EXPIRATION_BUFFER + _LOGGER.debug("Scheduled next stream refresh for %s", refresh_time) + + self._stream_refresh_unsub = async_track_point_in_utc_time( + self.hass, + self._handle_stream_refresh, + refresh_time, + ) + + async def _handle_stream_refresh(self, _: datetime.datetime) -> None: + """Alarm that fires to check if the stream should be refreshed.""" + _LOGGER.debug("Examining streams to refresh") + self._stream_refresh_unsub = None + try: + await self._async_refresh_stream() + finally: + self._schedule_stream_refresh() async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" @@ -161,6 +128,12 @@ class NestCameraBaseEntity(Camera, ABC): self._device.add_update_listener(self.async_write_ha_state) ) + async def async_will_remove_from_hass(self) -> None: + """Invalidates the RTSP token when unloaded.""" + await super().async_will_remove_from_hass() + if self._stream_refresh_unsub: + self._stream_refresh_unsub() + class NestRTSPEntity(NestCameraBaseEntity): """Nest cameras that use RTSP.""" @@ -173,7 +146,6 @@ class NestRTSPEntity(NestCameraBaseEntity): super().__init__(device) self._create_stream_url_lock = asyncio.Lock() self._rtsp_live_stream_trait = device.traits[CameraLiveStreamTrait.NAME] - self._refresh_unsub: Callable[[], None] | None = None @property def use_stream_for_stills(self) -> bool: @@ -201,21 +173,20 @@ class NestRTSPEntity(NestCameraBaseEntity): ) except ApiException as err: raise HomeAssistantError(f"Nest API error: {err}") from err - refresh = StreamRefresh( - self.hass, - self._rtsp_stream.expires_at, - self._async_refresh_stream, - ) - self._refresh_unsub = refresh.unsub + self._schedule_stream_refresh() assert self._rtsp_stream if self._rtsp_stream.expires_at < utcnow(): _LOGGER.warning("Stream already expired") return self._rtsp_stream.rtsp_stream_url - async def _async_refresh_stream(self) -> datetime.datetime | None: + def _stream_expires_at(self) -> datetime.datetime | None: + """Next time when a stream expires.""" + return self._rtsp_stream.expires_at if self._rtsp_stream else None + + async def _async_refresh_stream(self) -> None: """Refresh stream to extend expiration time.""" if not self._rtsp_stream: - return None + return _LOGGER.debug("Extending RTSP stream") try: self._rtsp_stream = await self._rtsp_stream.extend_rtsp_stream() @@ -226,17 +197,14 @@ class NestRTSPEntity(NestCameraBaseEntity): if self.stream: await self.stream.stop() self.stream = None - return None + return # Update the stream worker with the latest valid url if self.stream: self.stream.update_source(self._rtsp_stream.rtsp_stream_url) - return self._rtsp_stream.expires_at async def async_will_remove_from_hass(self) -> None: """Invalidates the RTSP token when unloaded.""" await super().async_will_remove_from_hass() - if self._refresh_unsub is not None: - self._refresh_unsub() if self._rtsp_stream: try: await self._rtsp_stream.stop_stream() @@ -252,23 +220,37 @@ class NestWebRTCEntity(NestCameraBaseEntity): """Initialize the camera.""" super().__init__(device) self._webrtc_sessions: dict[str, WebRtcStream] = {} - self._refresh_unsub: dict[str, Callable[[], None]] = {} @property def frontend_stream_type(self) -> StreamType | None: """Return the type of stream supported by this camera.""" return StreamType.WEB_RTC - async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None: - """Refresh stream to extend expiration time.""" - if not (webrtc_stream := self._webrtc_sessions.get(session_id)): + def _stream_expires_at(self) -> datetime.datetime | None: + """Next time when a stream expires.""" + if not self._webrtc_sessions: return None - _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id) - webrtc_stream = await webrtc_stream.extend_stream() - if session_id in self._webrtc_sessions: - self._webrtc_sessions[session_id] = webrtc_stream - return webrtc_stream.expires_at - return None + return min(stream.expires_at for stream in self._webrtc_sessions.values()) + + async def _async_refresh_stream(self) -> None: + """Refresh stream to extend expiration time.""" + now = utcnow() + for session_id, webrtc_stream in list(self._webrtc_sessions.items()): + if session_id not in self._webrtc_sessions: + continue + if now < (webrtc_stream.expires_at - STREAM_EXPIRATION_BUFFER): + _LOGGER.debug( + "Stream does not yet expire: %s", webrtc_stream.expires_at + ) + continue + _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id) + try: + webrtc_stream = await webrtc_stream.extend_stream() + except ApiException as err: + _LOGGER.debug("Failed to extend stream: %s", err) + else: + if session_id in self._webrtc_sessions: + self._webrtc_sessions[session_id] = webrtc_stream async def async_camera_image( self, width: int | None = None, height: int | None = None @@ -296,18 +278,7 @@ class NestWebRTCEntity(NestCameraBaseEntity): ) self._webrtc_sessions[session_id] = stream send_message(WebRTCAnswer(stream.answer_sdp)) - refresh = StreamRefresh( - self.hass, - stream.expires_at, - functools.partial(self._async_refresh_stream, session_id), - ) - self._refresh_unsub[session_id] = refresh.unsub - - async def async_on_webrtc_candidate( - self, session_id: str, candidate: RTCIceCandidate - ) -> None: - """Ignore WebRTC candidates for Nest cloud based cameras.""" - return + self._schedule_stream_refresh() @callback def close_webrtc_session(self, session_id: str) -> None: @@ -316,8 +287,6 @@ class NestWebRTCEntity(NestCameraBaseEntity): _LOGGER.debug( "Closing WebRTC session %s, %s", session_id, stream.media_session_id ) - unsub = self._refresh_unsub.pop(session_id) - unsub() async def stop_stream() -> None: try: diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 44eaeeaf62d..581113f0c96 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==6.1.5"] + "requirements": ["google-nest-sdm==6.1.4"] } diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 620349ec3c3..92291bdc4f9 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -9,9 +9,9 @@ from typing import Any from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DATE, UnitOfBloodGlucoseConcentration +from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,10 +37,7 @@ async def async_setup_entry( class NightscoutSensor(SensorEntity): """Implementation of a Nightscout sensor.""" - _attr_device_class = SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION - _attr_native_unit_of_measurement = ( - UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER - ) + _attr_native_unit_of_measurement = "mg/dL" _attr_icon = "mdi:cloud-question" def __init__(self, api: NightscoutAPI, name: str, unique_id: str | None) -> None: diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 98ea88d8798..9747feaddb7 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -38,10 +38,12 @@ } } }, + "abort": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, "error": { "no_selection": "[%key:component::nina::config::error::no_selection%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } } diff --git a/homeassistant/components/nordpool/config_flow.py b/homeassistant/components/nordpool/config_flow.py index 1d75d825e47..a9a834d8225 100644 --- a/homeassistant/components/nordpool/config_flow.py +++ b/homeassistant/components/nordpool/config_flow.py @@ -4,12 +4,7 @@ from __future__ import annotations from typing import Any -from pynordpool import ( - Currency, - NordPoolClient, - NordPoolEmptyResponseError, - NordPoolError, -) +from pynordpool import Currency, NordPoolClient, NordPoolError from pynordpool.const import AREAS import voluptuous as vol @@ -58,16 +53,17 @@ async def test_api(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, """Test fetch data from Nord Pool.""" client = NordPoolClient(async_get_clientsession(hass)) try: - await client.async_get_delivery_period( + data = await client.async_get_delivery_period( dt_util.now(), Currency(user_input[CONF_CURRENCY]), user_input[CONF_AREAS], ) - except NordPoolEmptyResponseError: - return {"base": "no_data"} except NordPoolError: return {"base": "cannot_connect"} + if not data.raw: + return {"base": "no_data"} + return {} diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py index fa4e9ca2548..27016ae2b4b 100644 --- a/homeassistant/components/nordpool/coordinator.py +++ b/homeassistant/components/nordpool/coordinator.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING from pynordpool import ( Currency, DeliveryPeriodData, + NordPoolAuthenticationError, NordPoolClient, - NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -19,7 +19,7 @@ from homeassistant.const import CONF_CURRENCY from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import CONF_AREAS, DOMAIN, LOGGER @@ -75,8 +75,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]): Currency(self.config_entry.data[CONF_CURRENCY]), self.config_entry.data[CONF_AREAS], ) - except NordPoolEmptyResponseError as error: - LOGGER.debug("Empty response error: %s", error) + except NordPoolAuthenticationError as error: + LOGGER.error("Authentication error: %s", error) self.async_set_update_error(error) return except NordPoolResponseError as error: @@ -88,4 +88,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]): self.async_set_update_error(error) return + if not data.raw: + self.async_set_update_error(UpdateFailed("No data")) + return + self.async_set_updated_data(data) diff --git a/homeassistant/components/nordpool/diagnostics.py b/homeassistant/components/nordpool/diagnostics.py deleted file mode 100644 index 3160c2bfa6d..00000000000 --- a/homeassistant/components/nordpool/diagnostics.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Diagnostics support for Nord Pool.""" - -from __future__ import annotations - -from typing import Any - -from homeassistant.core import HomeAssistant - -from . import NordPoolConfigEntry - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: NordPoolConfigEntry -) -> dict[str, Any]: - """Return diagnostics for Nord Pool config entry.""" - return {"raw": entry.runtime_data.data.raw} diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json index bf093eb3ee9..ba435c38b5e 100644 --- a/homeassistant/components/nordpool/manifest.json +++ b/homeassistant/components/nordpool/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pynordpool"], - "requirements": ["pynordpool==0.2.2"], + "requirements": ["pynordpool==0.2.1"], "single_config_entry": true } diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 23e3ce0910b..5eea525fb6a 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -17,7 +17,6 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, - UnitOfBloodGlucoseConcentration, UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, @@ -110,12 +109,6 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `%` """ - BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration" - """Blood glucose concentration. - - Unit of measurement: `mg/dL`, `mmol/L` - """ - CO = "carbon_monoxide" """Carbon Monoxide gas concentration. @@ -436,7 +429,6 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.AQI: {None}, NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), NumberDeviceClass.BATTERY: {PERCENTAGE}, - NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration), NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, NumberDeviceClass.CONDUCTIVITY: set(UnitOfConductivity), diff --git a/homeassistant/components/number/icons.json b/homeassistant/components/number/icons.json index 5e0fc6e44d2..a122aaecb09 100644 --- a/homeassistant/components/number/icons.json +++ b/homeassistant/components/number/icons.json @@ -15,9 +15,6 @@ "battery": { "default": "mdi:battery" }, - "blood_glucose_concentration": { - "default": "mdi:spoon-sugar" - }, "carbon_dioxide": { "default": "mdi:molecule-co2" }, diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index b9aec880ecc..580385172e3 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -43,9 +43,6 @@ "battery": { "name": "[%key:component::sensor::entity_component::battery::name%]" }, - "blood_glucose_concentration": { - "name": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]" - }, "carbon_dioxide": { "name": "[%key:component::sensor::entity_component::carbon_dioxide::name%]" }, diff --git a/homeassistant/components/palazzetti/config_flow.py b/homeassistant/components/palazzetti/config_flow.py index fe892b6624d..a58461b9ca7 100644 --- a/homeassistant/components/palazzetti/config_flow.py +++ b/homeassistant/components/palazzetti/config_flow.py @@ -6,7 +6,6 @@ from pypalazzetti.client import PalazzettiClient from pypalazzetti.exceptions import CommunicationError import voluptuous as vol -from homeassistant.components import dhcp from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.helpers import device_registry as dr @@ -17,8 +16,6 @@ from .const import DOMAIN, LOGGER class PalazzettiConfigFlow(ConfigFlow, domain=DOMAIN): """Palazzetti config flow.""" - _discovered_device: PalazzettiClient - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -51,41 +48,3 @@ class PalazzettiConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_HOST): str}), errors=errors, ) - - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> ConfigFlowResult: - """Handle DHCP discovery.""" - - LOGGER.debug( - "DHCP discovery detected Palazzetti: %s", discovery_info.macaddress - ) - - await self.async_set_unique_id(dr.format_mac(discovery_info.macaddress)) - self._abort_if_unique_id_configured() - self._discovered_device = PalazzettiClient(hostname=discovery_info.ip) - try: - await self._discovered_device.connect() - except CommunicationError: - return self.async_abort(reason="cannot_connect") - - return await self.async_step_discovery_confirm() - - async def async_step_discovery_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm discovery.""" - if user_input is not None: - return self.async_create_entry( - title=self._discovered_device.name, - data={CONF_HOST: self._discovered_device.host}, - ) - - self._set_confirm_only() - return self.async_show_form( - step_id="discovery_confirm", - description_placeholders={ - "name": self._discovered_device.name, - "host": self._discovered_device.host, - }, - ) diff --git a/homeassistant/components/palazzetti/manifest.json b/homeassistant/components/palazzetti/manifest.json index aff82275e2e..a1b25f563bf 100644 --- a/homeassistant/components/palazzetti/manifest.json +++ b/homeassistant/components/palazzetti/manifest.json @@ -3,17 +3,8 @@ "name": "Palazzetti", "codeowners": ["@dotvav"], "config_flow": true, - "dhcp": [ - { - "hostname": "connbox*", - "macaddress": "40F3857*" - }, - { - "registered_devices": true - } - ], "documentation": "https://www.home-assistant.io/integrations/palazzetti", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["pypalazzetti==0.1.11"] + "requirements": ["pypalazzetti==0.1.10"] } diff --git a/homeassistant/components/palazzetti/strings.json b/homeassistant/components/palazzetti/strings.json index cc10c8ed5c6..fdf50f29f0d 100644 --- a/homeassistant/components/palazzetti/strings.json +++ b/homeassistant/components/palazzetti/strings.json @@ -8,9 +8,6 @@ "data_description": { "host": "The host name or the IP address of the Palazzetti CBox" } - }, - "discovery_confirm": { - "description": "Do you want to add {name} ({host}) to Home Assistant?" } }, "abort": { diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 0c39392ca19..bacbff63211 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -251,8 +251,8 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN): """Handle reauth confirmation.""" errors: dict[str, str] | None = {} description_placeholders: dict[str, str] = {} - reauth_entry = self._get_reauth_entry() if user_input is not None: + reauth_entry = self._get_reauth_entry() errors, _, description_placeholders = await self._async_try_connect( {CONF_IP_ADDRESS: reauth_entry.data[CONF_IP_ADDRESS], **user_input} ) @@ -261,10 +261,6 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry, data_updates=user_input ) - self.context["title_placeholders"] = { - "name": reauth_entry.title, - "ip_address": reauth_entry.data[CONF_IP_ADDRESS], - } return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}), diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 389e3384ad9..9b2b9736574 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -436,10 +436,6 @@ async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall) # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - if sys.version_info >= (3, 13): - raise HomeAssistantError( - "Memory profiling is not supported on Python 3.13. Please use Python 3.12." - ) from guppy import hpy # pylint: disable=import-outside-toplevel start_time = int(time.time() * 1000000) diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json index 8d2814c8c7f..9f27ee7f7d0 100644 --- a/homeassistant/components/profiler/manifest.json +++ b/homeassistant/components/profiler/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "pyprof2calltree==1.4.5", - "guppy3==3.1.4.post1;python_version<'3.13'", + "guppy3==3.1.4.post1", "objgraph==3.5.0" ], "single_config_entry": true diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index f13799422df..1e70c4d3e10 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==11.0.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 383a4e5f572..526516bfcdd 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( + ATTR_NAME, PERCENTAGE, EntityCategory, UnitOfDataRate, @@ -374,6 +375,17 @@ class QNAPMemorySensor(QNAPSensor): return None + # Deprecated since Home Assistant 2024.6.0 + # Can be removed completely in 2024.12.0 + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + if self.coordinator.data: + data = self.coordinator.data["system_stats"]["memory"] + size = round(float(data["total"]) / 1024, 2) + return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} + return None + class QNAPNetworkSensor(QNAPSensor): """A QNAP sensor that monitors network stats.""" @@ -402,6 +414,22 @@ class QNAPNetworkSensor(QNAPSensor): return None + # Deprecated since Home Assistant 2024.6.0 + # Can be removed completely in 2024.12.0 + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + if self.coordinator.data: + data = self.coordinator.data["system_stats"]["nics"][self.monitor_device] + return { + ATTR_IP: data["ip"], + ATTR_MASK: data["mask"], + ATTR_MAC: data["mac"], + ATTR_MAX_SPEED: data["max_speed"], + ATTR_PACKETS_ERR: data["err_packets"], + } + return None + class QNAPSystemSensor(QNAPSensor): """A QNAP sensor that monitors overall system health.""" @@ -427,6 +455,25 @@ class QNAPSystemSensor(QNAPSensor): return None + # Deprecated since Home Assistant 2024.6.0 + # Can be removed completely in 2024.12.0 + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + if self.coordinator.data: + data = self.coordinator.data["system_stats"] + days = int(data["uptime"]["days"]) + hours = int(data["uptime"]["hours"]) + minutes = int(data["uptime"]["minutes"]) + + return { + ATTR_NAME: data["system"]["name"], + ATTR_MODEL: data["system"]["model"], + ATTR_SERIAL: data["system"]["serial_number"], + ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m", + } + return None + class QNAPDriveSensor(QNAPSensor): """A QNAP sensor that monitors HDD/SSD drive stats.""" @@ -486,3 +533,17 @@ class QNAPVolumeSensor(QNAPSensor): return used_gb / total_gb * 100 return None + + # Deprecated since Home Assistant 2024.6.0 + # Can be removed completely in 2024.12.0 + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + if self.coordinator.data: + data = self.coordinator.data["volumes"][self.monitor_device] + total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 + + return { + ATTR_VOLUME_SIZE: f"{round(total_gb, 1)} {UnitOfInformation.GIBIBYTES}" + } + return None diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 3fcc895c2b9..14f2d093f37 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==11.0.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.4.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index fc2a8ccb1cc..30f8fa8d07a 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -16,7 +16,7 @@ from sqlalchemy.pool import ( StaticPool, ) -from homeassistant.helpers.frame import ReportBehavior, report_usage +from homeassistant.helpers.frame import report from homeassistant.util.loop import raise_for_blocking_call _LOGGER = logging.getLogger(__name__) @@ -108,14 +108,14 @@ class RecorderPool(SingletonThreadPool, NullPool): # raise_for_blocking_call will raise an exception def _do_get_db_connection_protected(self) -> ConnectionPoolEntry: - report_usage( + report( ( "accesses the database without the database executor; " f"{ADVISE_MSG} " "for faster database operations" ), exclude_integrations={"recorder"}, - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) return NullPool._create_connection(self) # noqa: SLF001 diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7243af9d4d5..4ffe7c72971 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -28,7 +28,6 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -129,10 +128,6 @@ QUERY_STATISTICS_SUMMARY_SUM = ( STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { - **{ - unit: BloodGlucoseConcentrationConverter - for unit in BloodGlucoseConcentrationConverter.VALID_UNITS - }, **{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS}, **{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS}, **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f4dce73fa47..ac917e903df 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -16,7 +16,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( - BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -55,9 +54,6 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10 UNIT_SCHEMA = vol.Schema( { - vol.Optional("blood_glucose_concentration"): vol.In( - BloodGlucoseConcentrationConverter.VALID_UNITS - ), vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS), vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json index d333a8a0201..7f4a15ffe21 100644 --- a/homeassistant/components/reolink/icons.json +++ b/homeassistant/components/reolink/icons.json @@ -246,12 +246,6 @@ "off": "mdi:music-note-off" } }, - "vehicle_tone": { - "default": "mdi:music-note", - "state": { - "off": "mdi:music-note-off" - } - }, "visitor_tone": { "default": "mdi:music-note", "state": { 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/reolink/select.py b/homeassistant/components/reolink/select.py index a444997a907..1306c881059 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -197,16 +197,6 @@ CHIME_SELECT_ENTITIES = ( value=lambda chime: ChimeToneEnum(chime.tone("people")).name, method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value), ), - ReolinkChimeSelectEntityDescription( - key="vehicle_tone", - cmd_key="GetDingDongCfg", - translation_key="vehicle_tone", - entity_category=EntityCategory.CONFIG, - get_options=[method.name for method in ChimeToneEnum], - supported=lambda chime: "vehicle" in chime.chime_event_types, - value=lambda chime: ChimeToneEnum(chime.tone("vehicle")).name, - method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value), - ), ReolinkChimeSelectEntityDescription( key="visitor_tone", cmd_key="GetDingDongCfg", diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 1d699b7b658..fbc88ed1b50 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -606,22 +606,6 @@ "waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]" } }, - "vehicle_tone": { - "name": "Vehicle ringtone", - "state": { - "off": "[%key:common::state::off%]", - "citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]", - "originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]", - "pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]", - "loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]", - "attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]", - "hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]", - "goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]", - "operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]", - "moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]", - "waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]" - } - }, "visitor_tone": { "name": "Visitor ringtone", "state": { 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/russound_rio/const.py b/homeassistant/components/russound_rio/const.py index af52e89d399..1b38dc8ce5c 100644 --- a/homeassistant/components/russound_rio/const.py +++ b/homeassistant/components/russound_rio/const.py @@ -17,7 +17,7 @@ RUSSOUND_RIO_EXCEPTIONS = ( ) -CONNECT_TIMEOUT = 15 +CONNECT_TIMEOUT = 5 MP_FEATURES_BY_FLAG = { FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON: MediaPlayerEntityFeature.VOLUME_MUTE diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index 9790ff43e68..0233305bb1f 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -96,4 +96,6 @@ class RussoundBaseEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Remove callbacks.""" - self._client.unregister_state_update_callbacks(self._state_update_callback) + await self._client.unregister_state_update_callbacks( + self._state_update_callback + ) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index ab77ca3ab6a..96fc0fb53db 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.1.0"] + "requirements": ["aiorussound==4.0.5"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 45818d3e25b..316e4d2be7c 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from aiorussound import Controller -from aiorussound.models import PlayStatus, Source +from aiorussound.models import Source from aiorussound.rio import ZoneControlSurface from homeassistant.components.media_player import ( @@ -132,18 +132,11 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): def state(self) -> MediaPlayerState | None: """Return the state of the device.""" status = self._zone.status - play_status = self._source.play_status - if not status: + if status == "ON": + return MediaPlayerState.ON + if status == "OFF": return MediaPlayerState.OFF - if play_status == PlayStatus.PLAYING: - return MediaPlayerState.PLAYING - if play_status == PlayStatus.PAUSED: - return MediaPlayerState.PAUSED - if play_status == PlayStatus.TRANSITIONING: - return MediaPlayerState.BUFFERING - if play_status == PlayStatus.STOPPED: - return MediaPlayerState.IDLE - return MediaPlayerState.ON + return None @property def source(self): @@ -182,7 +175,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): Value is returned based on a range (0..50). Therefore float divide by 50 to get to the required range. """ - return self._zone.volume / 50.0 + return float(self._zone.volume or "0") / 50.0 @command async def async_turn_off(self) -> None: diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index f4573f873a2..aa3d1906b21 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -17,7 +17,6 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, - UnitOfBloodGlucoseConcentration, UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, @@ -48,7 +47,6 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -129,12 +127,6 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `%` """ - BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration" - """Blood glucose concentration. - - Unit of measurement: `mg/dL`, `mmol/L` - """ - CO = "carbon_monoxide" """Carbon Monoxide gas concentration. @@ -501,7 +493,6 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter, SensorDeviceClass.CONDUCTIVITY: ConductivityConverter, SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.DATA_RATE: DataRateConverter, @@ -533,7 +524,6 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.AQI: {None}, SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), SensorDeviceClass.BATTERY: {PERCENTAGE}, - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration), SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, SensorDeviceClass.CONDUCTIVITY: set(UnitOfConductivity), @@ -609,7 +599,6 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT}, - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CO: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CO2: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CONDUCTIVITY: {SensorStateClass.MEASUREMENT}, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 56ecb36adb3..f2b51899312 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -37,7 +37,6 @@ CONF_IS_APPARENT_POWER = "is_apparent_power" CONF_IS_AQI = "is_aqi" CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure" CONF_IS_BATTERY_LEVEL = "is_battery_level" -CONF_IS_BLOOD_GLUCOSE_CONCENTRATION = "is_blood_glucose_concentration" CONF_IS_CO = "is_carbon_monoxide" CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CONDUCTIVITY = "is_conductivity" @@ -88,9 +87,6 @@ ENTITY_CONDITIONS = { SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}], SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [ - {CONF_TYPE: CONF_IS_BLOOD_GLUCOSE_CONCENTRATION} - ], SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}], SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_IS_CONDUCTIVITY}], @@ -155,7 +151,6 @@ CONDITION_SCHEMA = vol.All( CONF_IS_AQI, CONF_IS_ATMOSPHERIC_PRESSURE, CONF_IS_BATTERY_LEVEL, - CONF_IS_BLOOD_GLUCOSE_CONCENTRATION, CONF_IS_CO, CONF_IS_CO2, CONF_IS_CONDUCTIVITY, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index ffee10d9f40..b07b3fac11e 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -36,7 +36,6 @@ CONF_APPARENT_POWER = "apparent_power" CONF_AQI = "aqi" CONF_ATMOSPHERIC_PRESSURE = "atmospheric_pressure" CONF_BATTERY_LEVEL = "battery_level" -CONF_BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration" CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" CONF_CONDUCTIVITY = "conductivity" @@ -87,9 +86,6 @@ ENTITY_TRIGGERS = { SensorDeviceClass.AQI: [{CONF_TYPE: CONF_AQI}], SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_ATMOSPHERIC_PRESSURE}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], - SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [ - {CONF_TYPE: CONF_BLOOD_GLUCOSE_CONCENTRATION} - ], SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}], SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_CONDUCTIVITY}], @@ -155,7 +151,6 @@ TRIGGER_SCHEMA = vol.All( CONF_AQI, CONF_ATMOSPHERIC_PRESSURE, CONF_BATTERY_LEVEL, - CONF_BLOOD_GLUCOSE_CONCENTRATION, CONF_CO, CONF_CO2, CONF_CONDUCTIVITY, diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index ea4c902e665..6132fcbc1e9 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -12,9 +12,6 @@ "atmospheric_pressure": { "default": "mdi:thermometer-lines" }, - "blood_glucose_concentration": { - "default": "mdi:spoon-sugar" - }, "carbon_dioxide": { "default": "mdi:molecule-co2" }, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 6d529e72c3b..71bead342c4 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -6,7 +6,6 @@ "is_aqi": "Current {entity_name} air quality index", "is_atmospheric_pressure": "Current {entity_name} atmospheric pressure", "is_battery_level": "Current {entity_name} battery level", - "is_blood_glucose_concentration": "Current {entity_name} blood glucose concentration", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", "is_conductivity": "Current {entity_name} conductivity", @@ -57,7 +56,6 @@ "aqi": "{entity_name} air quality index changes", "atmospheric_pressure": "{entity_name} atmospheric pressure changes", "battery_level": "{entity_name} battery level changes", - "blood_glucose_concentration": "{entity_name} blood glucose concentration changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "conductivity": "{entity_name} conductivity changes", @@ -151,9 +149,6 @@ "battery": { "name": "Battery" }, - "blood_glucose_concentration": { - "name": "Blood glucose concentration" - }, "carbon_monoxide": { "name": "Carbon monoxide" }, diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index af00a1fdfed..2f39644d6d3 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==11.0.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a66fbb20f48..6332e139244 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -603,7 +603,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): async def _async_update_data(self) -> None: """Fetch data.""" - if self.update_sleep_period() or self.hass.is_stopping: + if self.update_sleep_period(): return if self.sleep_period: diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 7d08367cf7d..875c98acb6d 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==11.0.0", "simplehound==0.3"] + "requirements": ["Pillow==10.4.0", "simplehound==0.3"] } 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/__init__.py b/homeassistant/components/smarty/__init__.py index 0d043804c3d..0e5ca216621 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -30,13 +30,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.FAN, - Platform.SENSOR, - Platform.SWITCH, -] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR, Platform.SWITCH] async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: diff --git a/homeassistant/components/smarty/button.py b/homeassistant/components/smarty/button.py deleted file mode 100644 index b8e31cf6fc8..00000000000 --- a/homeassistant/components/smarty/button.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Platform to control a Salda Smarty XP/XV ventilation unit.""" - -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -import logging -from typing import Any - -from pysmarty2 import Smarty - -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .coordinator import SmartyConfigEntry, SmartyCoordinator -from .entity import SmartyEntity - -_LOGGER = logging.getLogger(__name__) - - -@dataclass(frozen=True, kw_only=True) -class SmartyButtonDescription(ButtonEntityDescription): - """Class describing Smarty button.""" - - press_fn: Callable[[Smarty], bool | None] - - -ENTITIES: tuple[SmartyButtonDescription, ...] = ( - SmartyButtonDescription( - key="reset_filters_timer", - translation_key="reset_filters_timer", - press_fn=lambda smarty: smarty.reset_filters_timer(), - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: SmartyConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Smarty Button Platform.""" - - coordinator = entry.runtime_data - - async_add_entities( - SmartyButton(coordinator, description) for description in ENTITIES - ) - - -class SmartyButton(SmartyEntity, ButtonEntity): - """Representation of a Smarty Button.""" - - entity_description: SmartyButtonDescription - - def __init__( - self, - coordinator: SmartyCoordinator, - entity_description: SmartyButtonDescription, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self.entity_description = entity_description - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - - async def async_press(self, **kwargs: Any) -> None: - """Press the button.""" - await self.hass.async_add_executor_job( - self.entity_description.press_fn, self.coordinator.client - ) - await self.coordinator.async_refresh() diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json index 341a300a26e..5553a1c0135 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": { @@ -46,11 +42,6 @@ "name": "Boost state" } }, - "button": { - "reset_filters_timer": { - "name": "Reset filters timer" - } - }, "sensor": { "supply_air_temperature": { "name": "Supply air temperature" diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index e1cedba10e7..c868c04f7d0 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -93,13 +93,6 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - # aiopyarr defaults to the service port if one isn't given - # this is counter to standard practice where http = 80 - # and https = 443. - if CONF_URL in user_input: - url = yarl.URL(user_input[CONF_URL]) - user_input[CONF_URL] = f"{url.scheme}://{url.host}:{url.port}{url.path}" - if self.source == SOURCE_REAUTH: user_input = {**self._get_reauth_entry().data, **user_input} diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 76a7d0bfa91..d6c5eb298d8 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.30.6", "sonos-websocket==0.1.3"], + "requirements": ["soco==0.30.4", "sonos-websocket==0.1.3"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 8f8f7e0d588..afe352904ce 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "loggers": ["spotipy"], "quality_scale": "silver", - "requirements": ["spotifyaio==0.8.8"], + "requirements": ["spotifyaio==0.8.7"], "zeroconf": ["_spotify-connect._tcp.local."] } 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/statistics/strings.json b/homeassistant/components/statistics/strings.json index 3e6fec9d986..a060c88da24 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -23,10 +23,10 @@ "state_characteristic": { "description": "Read the documention for further details on available options and how to use them.", "data": { - "state_characteristic": "Statistic characteristic" + "state_characteristic": "State_characteristic" }, "data_description": { - "state_characteristic": "The statistic characteristic that should be used as the state of the sensor." + "state_characteristic": "The characteristic that should be used as the state of the statistics sensor." } }, "options": { diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index fdf81d99e65..304ef5bbf62 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.7.5", "av==13.1.0", "numpy==2.1.3"] + "requirements": ["PyTurboJPEG==1.7.5", "av==13.1.0", "numpy==2.1.2"] } 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/suez_water/coordinator.py b/homeassistant/components/suez_water/coordinator.py index 224929c606e..55f3ba348d4 100644 --- a/homeassistant/components/suez_water/coordinator.py +++ b/homeassistant/components/suez_water/coordinator.py @@ -1,11 +1,6 @@ """Suez water update coordinator.""" -from collections.abc import Mapping -from dataclasses import dataclass -from datetime import date -from typing import Any - -from pysuez import PySuezError, SuezClient +from pysuez import AggregatedData, PySuezError, SuezClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -16,28 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_COUNTER_ID, DATA_REFRESH_INTERVAL, DOMAIN -@dataclass -class SuezWaterAggregatedAttributes: - """Class containing aggregated sensor extra attributes.""" - - this_month_consumption: dict[date, float] - previous_month_consumption: dict[date, float] - last_year_overall: dict[str, float] - this_year_overall: dict[str, float] - history: dict[date, float] - highest_monthly_consumption: float - - -@dataclass -class SuezWaterData: - """Class used to hold all fetch data from suez api.""" - - aggregated_value: float - aggregated_attr: Mapping[str, Any] - price: float - - -class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]): +class SuezWaterCoordinator(DataUpdateCoordinator[AggregatedData]): """Suez water coordinator.""" _suez_client: SuezClient @@ -63,22 +37,10 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]): if not await self._suez_client.check_credentials(): raise ConfigEntryError("Invalid credentials for suez water") - async def _async_update_data(self) -> SuezWaterData: + async def _async_update_data(self) -> AggregatedData: """Fetch data from API endpoint.""" try: - aggregated = await self._suez_client.fetch_aggregated_data() - data = SuezWaterData( - aggregated_value=aggregated.value, - aggregated_attr={ - "this_month_consumption": aggregated.current_month, - "previous_month_consumption": aggregated.previous_month, - "highest_monthly_consumption": aggregated.highest_monthly_consumption, - "last_year_overall": aggregated.previous_year, - "this_year_overall": aggregated.current_year, - "history": aggregated.history, - }, - price=(await self._suez_client.get_price()).price, - ) + data = await self._suez_client.fetch_aggregated_data() except PySuezError as err: _LOGGER.exception(err) raise UpdateFailed( diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 2ba699a9af1..22a61c835e1 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -2,53 +2,19 @@ from __future__ import annotations -from collections.abc import Callable, Mapping -from dataclasses import dataclass +from collections.abc import Mapping from typing import Any -from pysuez.const import ATTRIBUTION - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CURRENCY_EURO, UnitOfVolume +from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_COUNTER_ID, DOMAIN -from .coordinator import SuezWaterCoordinator, SuezWaterData - - -@dataclass(frozen=True, kw_only=True) -class SuezWaterSensorEntityDescription(SensorEntityDescription): - """Describes Suez water sensor entity.""" - - value_fn: Callable[[SuezWaterData], float | str | None] - attr_fn: Callable[[SuezWaterData], Mapping[str, Any] | None] = lambda _: None - - -SENSORS: tuple[SuezWaterSensorEntityDescription, ...] = ( - SuezWaterSensorEntityDescription( - key="water_usage_yesterday", - translation_key="water_usage_yesterday", - native_unit_of_measurement=UnitOfVolume.LITERS, - device_class=SensorDeviceClass.WATER, - value_fn=lambda suez_data: suez_data.aggregated_value, - attr_fn=lambda suez_data: suez_data.aggregated_attr, - ), - SuezWaterSensorEntityDescription( - key="water_price", - translation_key="water_price", - native_unit_of_measurement=CURRENCY_EURO, - device_class=SensorDeviceClass.MONETARY, - value_fn=lambda suez_data: suez_data.price, - ), -) +from .coordinator import SuezWaterCoordinator async def async_setup_entry( @@ -58,42 +24,46 @@ async def async_setup_entry( ) -> None: """Set up Suez Water sensor from a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - counter_id = entry.data[CONF_COUNTER_ID] - - async_add_entities( - SuezWaterSensor(coordinator, counter_id, description) for description in SENSORS - ) + async_add_entities([SuezAggregatedSensor(coordinator, entry.data[CONF_COUNTER_ID])]) -class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity): - """Representation of a Suez water sensor.""" +class SuezAggregatedSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity): + """Representation of a Sensor.""" _attr_has_entity_name = True - _attr_attribution = ATTRIBUTION - entity_description: SuezWaterSensorEntityDescription + _attr_translation_key = "water_usage_yesterday" + _attr_native_unit_of_measurement = UnitOfVolume.LITERS + _attr_device_class = SensorDeviceClass.WATER - def __init__( - self, - coordinator: SuezWaterCoordinator, - counter_id: int, - entity_description: SuezWaterSensorEntityDescription, - ) -> None: - """Initialize the suez water sensor entity.""" + def __init__(self, coordinator: SuezWaterCoordinator, counter_id: int) -> None: + """Initialize the data object.""" super().__init__(coordinator) - self._attr_unique_id = f"{counter_id}_{entity_description.key}" + self._attr_extra_state_attributes = {} + self._attr_unique_id = f"{counter_id}_water_usage_yesterday" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, str(counter_id))}, entry_type=DeviceEntryType.SERVICE, manufacturer="Suez", ) - self.entity_description = entity_description @property - def native_value(self) -> float | str | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + def native_value(self) -> float: + """Return the current daily usage.""" + return self.coordinator.data.value @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return extra state of the sensor.""" - return self.entity_description.attr_fn(self.coordinator.data) + def attribution(self) -> str: + """Return data attribution message.""" + return self.coordinator.data.attribution + + @property + def extra_state_attributes(self) -> Mapping[str, Any]: + """Return aggregated data.""" + return { + "this_month_consumption": self.coordinator.data.current_month, + "previous_month_consumption": self.coordinator.data.previous_month, + "highest_monthly_consumption": self.coordinator.data.highest_monthly_consumption, + "last_year_overall": self.coordinator.data.previous_year, + "this_year_overall": self.coordinator.data.current_year, + "history": self.coordinator.data.history, + } diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json index 6be2affab97..a1af12abd55 100644 --- a/homeassistant/components/suez_water/strings.json +++ b/homeassistant/components/suez_water/strings.json @@ -23,9 +23,6 @@ "sensor": { "water_usage_yesterday": { "name": "Water usage yesterday" - }, - "water_price": { - "name": "Water price" } } } diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 625b4698301..a2738ed446f 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -85,9 +85,6 @@ def make_device_data( "Meter", "MeterPlus", "WoIOSensor", - "Hub 2", - "MeterPro", - "MeterPro(CO2)", ]: devices_data.sensors.append( prepare_device(hass, api, device, coordinators_by_id) diff --git a/homeassistant/components/switchbot_cloud/sensor.py b/homeassistant/components/switchbot_cloud/sensor.py index 90135ad96b3..ac612aea119 100644 --- a/homeassistant/components/switchbot_cloud/sensor.py +++ b/homeassistant/components/switchbot_cloud/sensor.py @@ -9,11 +9,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - UnitOfTemperature, -) +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,7 +21,6 @@ from .entity import SwitchBotCloudEntity SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_BATTERY = "battery" -SENSOR_TYPE_CO2 = "CO2" METER_PLUS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( @@ -48,16 +43,6 @@ METER_PLUS_SENSOR_DESCRIPTIONS = ( ), ) -METER_PRO_CO2_SENSOR_DESCRIPTIONS = ( - *METER_PLUS_SENSOR_DESCRIPTIONS, - SensorEntityDescription( - key=SENSOR_TYPE_CO2, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.CO2, - ), -) - async def async_setup_entry( hass: HomeAssistant, @@ -70,11 +55,7 @@ async def async_setup_entry( async_add_entities( SwitchBotCloudSensor(data.api, device, coordinator, description) for device, coordinator in data.devices.sensors - for description in ( - METER_PRO_CO2_SENSOR_DESCRIPTIONS - if device.device_type == "MeterPro(CO2)" - else METER_PLUS_SENSOR_DESCRIPTIONS - ) + for description in METER_PLUS_SENSOR_DESCRIPTIONS ) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 4c6ae0653d3..236f25bb1ed 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "iot_class": "local_push", "loggers": ["psutil"], - "requirements": ["psutil-home-assistant==0.0.1", "psutil==6.1.0"] + "requirements": ["psutil-home-assistant==0.0.1", "psutil==6.0.0"] } diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py index 528a5052678..cd593f68e3a 100644 --- a/homeassistant/components/tedee/__init__.py +++ b/homeassistant/components/tedee/__init__.py @@ -7,7 +7,7 @@ from typing import Any from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response -from aiotedee.exception import TedeeDataUpdateException, TedeeWebhookException +from pytedee_async.exception import TedeeDataUpdateException, TedeeWebhookException from homeassistant.components.http import HomeAssistantView from homeassistant.components.webhook import ( diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py index b586db7c2a7..5eab7bfa254 100644 --- a/homeassistant/components/tedee/binary_sensor.py +++ b/homeassistant/components/tedee/binary_sensor.py @@ -3,8 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from aiotedee import TedeeLock -from aiotedee.lock import TedeeLockState +from pytedee_async import TedeeLock +from pytedee_async.lock import TedeeLockState from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py index 422d818d1b5..65d4ec12e80 100644 --- a/homeassistant/components/tedee/config_flow.py +++ b/homeassistant/components/tedee/config_flow.py @@ -4,7 +4,7 @@ from collections.abc import Mapping import logging from typing import Any -from aiotedee import ( +from pytedee_async import ( TedeeAuthException, TedeeClient, TedeeClientException, diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 445585a1a2c..de3090a3f78 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -8,7 +8,7 @@ import logging import time from typing import Any -from aiotedee import ( +from pytedee_async import ( TedeeClient, TedeeClientException, TedeeDataUpdateException, @@ -16,7 +16,7 @@ from aiotedee import ( TedeeLock, TedeeWebhookException, ) -from aiotedee.bridge import TedeeBridge +from pytedee_async.bridge import TedeeBridge from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST diff --git a/homeassistant/components/tedee/entity.py b/homeassistant/components/tedee/entity.py index 96cc6f2b3f5..c72e293a292 100644 --- a/homeassistant/components/tedee/entity.py +++ b/homeassistant/components/tedee/entity.py @@ -1,6 +1,6 @@ """Bases for Tedee entities.""" -from aiotedee.lock import TedeeLock +from pytedee_async.lock import TedeeLock from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index 6e89a48f2a0..34d313f3e48 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -2,7 +2,7 @@ from typing import Any -from aiotedee import TedeeClientException, TedeeLock, TedeeLockState +from pytedee_async import TedeeClientException, TedeeLock, TedeeLockState from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index bca51f08f93..4f071267a25 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["http", "webhook"], "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", - "loggers": ["aiotedee"], + "loggers": ["pytedee_async"], "quality_scale": "platinum", - "requirements": ["aiotedee==0.2.20"] + "requirements": ["pytedee-async==0.2.20"] } diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py index 90f76317fff..33894a5eb52 100644 --- a/homeassistant/components/tedee/sensor.py +++ b/homeassistant/components/tedee/sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass -from aiotedee import TedeeLock +from pytedee_async import TedeeLock from homeassistant.components.sensor import ( SensorDeviceClass, 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/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 86fd83ad088..906ce02f5b1 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -9,7 +9,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.6", - "numpy==2.1.3", - "Pillow==11.0.0" + "numpy==2.1.2", + "Pillow==10.4.0" ] } diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index e7030b568b3..70db4a183aa 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -134,7 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - signing = product["command_signing"] == "required" if signing: if not tesla.private_key: - await tesla.get_private_key(hass.config.path("tesla_fleet.key")) + await tesla.get_private_key("config/tesla_fleet.key") api = VehicleSigned(tesla.vehicle, vin) else: api = VehicleSpecific(tesla.vehicle, vin) 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/thethingsnetwork/__init__.py b/homeassistant/components/thethingsnetwork/__init__.py index d3c6c8356cb..253ce7a052e 100644 --- a/homeassistant/components/thethingsnetwork/__init__.py +++ b/homeassistant/components/thethingsnetwork/__init__.py @@ -2,15 +2,55 @@ import logging +import voluptuous as vol + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, PLATFORMS, TTN_API_HOST +from .const import CONF_APP_ID, DOMAIN, PLATFORMS, TTN_API_HOST from .coordinator import TTNCoordinator _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema( + { + # Configuration via yaml not longer supported - keeping to warn about migration + DOMAIN: vol.Schema( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required("access_key"): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Initialize of The Things Network component.""" + + if DOMAIN in config: + ir.async_create_issue( + hass, + DOMAIN, + "manual_migration", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="manual_migration", + translation_placeholders={ + "domain": DOMAIN, + "v2_v3_migration_url": "https://www.thethingsnetwork.org/forum/c/v2-to-v3-upgrade/102", + "v2_deprecation_url": "https://www.thethingsnetwork.org/forum/t/the-things-network-v2-is-permanently-shutting-down-completed/50710", + }, + ) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with The Things Network.""" diff --git a/homeassistant/components/thethingsnetwork/strings.json b/homeassistant/components/thethingsnetwork/strings.json index f5a4fcef8fd..98572cb318c 100644 --- a/homeassistant/components/thethingsnetwork/strings.json +++ b/homeassistant/components/thethingsnetwork/strings.json @@ -22,5 +22,11 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "issues": { + "manual_migration": { + "description": "Configuring {domain} using YAML was removed as part of migrating to [The Things Network v3]({v2_v3_migration_url}). [The Things Network v2 has shutted down]({v2_deprecation_url}).\n\nPlease remove the {domain} entry from the configuration.yaml and add re-add the integration using the config_flow", + "title": "The {domain} YAML configuration is not supported" + } } } diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 3d52d2225be..da7d92f7051 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -61,29 +61,15 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME: Final = "Threshold" - -def no_missing_threshold(value: dict) -> dict: - """Validate data point list is greater than polynomial degrees.""" - if value.get(CONF_LOWER) is None and value.get(CONF_UPPER) is None: - raise vol.Invalid("Lower or Upper thresholds are not provided") - - return value - - -PLATFORM_SCHEMA = vol.All( - BINARY_SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce( - float - ), - vol.Optional(CONF_LOWER): vol.Coerce(float), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UPPER): vol.Coerce(float), - } - ), - no_missing_threshold, +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float), + vol.Optional(CONF_LOWER): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UPPER): vol.Coerce(float), + } ) @@ -140,6 +126,9 @@ async def async_setup_platform( hysteresis: float = config[CONF_HYSTERESIS] device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) + if lower is None and upper is None: + raise ValueError("Lower or Upper thresholds not provided") + async_add_entities( [ ThresholdSensor( diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 9b5c7ee1168..ce05b8070f6 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -6,9 +6,15 @@ import aiohttp import tibber from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -67,6 +73,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Use discovery to load platform legacy notify platform + # The use of the legacy notify service was deprecated with HA Core 2024.6 + # Support will be removed with HA Core 2024.12 + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: DOMAIN}, + hass.data[DATA_HASS_CONFIG], + ) + ) + return True diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index bc9304ab59d..205bc1352eb 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.30.8"] + "requirements": ["pyTibber==0.30.4"] } diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py index fdeeeba68ef..1c9f86ed502 100644 --- a/homeassistant/components/tibber/notify.py +++ b/homeassistant/components/tibber/notify.py @@ -2,21 +2,38 @@ from __future__ import annotations +from collections.abc import Callable +from typing import Any + from tibber import Tibber from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, + BaseNotificationService, NotifyEntity, NotifyEntityFeature, + migrate_notify_issue, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN as TIBBER_DOMAIN +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> TibberNotificationService: + """Get the Tibber notification service.""" + tibber_connection: Tibber = hass.data[TIBBER_DOMAIN] + return TibberNotificationService(tibber_connection.send_notification) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -24,6 +41,31 @@ async def async_setup_entry( async_add_entities([TibberNotificationEntity(entry.entry_id)]) +class TibberNotificationService(BaseNotificationService): + """Implement the notification service for Tibber.""" + + def __init__(self, notify: Callable) -> None: + """Initialize the service.""" + self._notify = notify + + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message to Tibber devices.""" + migrate_notify_issue( + self.hass, + TIBBER_DOMAIN, + "Tibber", + "2024.12.0", + service_name=self._service_name, + ) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + try: + await self._notify(title=title, message=message) + except TimeoutError as exc: + raise HomeAssistantError( + translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout" + ) from exc + + class TibberNotificationEntity(NotifyEntity): """Implement the notification entity service for Tibber.""" diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 721b491bbf5..5b083ac58bf 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -78,7 +78,7 @@ "description": "When should user be reminded of this task, in natural language." }, "reminder_date_lang": { - "name": "Reminder date language", + "name": "Reminder data language", "description": "The language of reminder_date_string." }, "reminder_date": { diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index af20b54675b..6bde656dc30 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_omada", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["tplink-omada-client==1.4.3"] + "requirements": ["tplink-omada-client==1.4.2"] } diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index d7981105fd2..b2f47738d4a 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -7,5 +7,5 @@ "integration_type": "helper", "iot_class": "calculated", "quality_scale": "internal", - "requirements": ["numpy==2.1.3"] + "requirements": ["numpy==2.1.2"] } diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index c8a639cd239..47143f3595c 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -146,21 +146,14 @@ class DeviceListener(SharingDeviceListener): self.hass = hass self.manager = manager - def update_device( - self, device: CustomerDevice, updated_status_properties: list[str] | None - ) -> None: + def update_device(self, device: CustomerDevice) -> None: """Update device status.""" LOGGER.debug( - "Received update for device %s: %s (updated properties: %s)", + "Received update for device %s: %s", device.id, self.manager.device_map[device.id].status, - updated_status_properties, - ) - dispatcher_send( - self.hass, - f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}", - updated_status_properties, ) + dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}") def add_device(self, device: CustomerDevice) -> None: """Add device added listener.""" diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index cc258560067..4d3710f7570 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -283,15 +283,10 @@ class TuyaEntity(Entity): async_dispatcher_connect( self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}", - self._handle_state_update, + self.async_write_ha_state, ) ) - async def _handle_state_update( - self, updated_status_properties: list[str] | None - ) -> None: - self.async_write_ha_state() - def _send_command(self, commands: list[dict[str, Any]]) -> None: """Send command to the device.""" LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index b53e6fa27d8..305a74160de 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -43,5 +43,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["tuya_iot"], - "requirements": ["tuya-device-sharing-sdk==0.2.1"] + "requirements": ["tuya-device-sharing-sdk==0.1.9"] } diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 098a17e90f0..58a262e769f 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -94,7 +94,6 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): | FanEntityFeature.TURN_ON ) _attr_name = None - _attr_translation_key = "vesync" _enable_turn_on_off_backwards_compatibility = False def __init__(self, fan) -> None: diff --git a/homeassistant/components/vesync/icons.json b/homeassistant/components/vesync/icons.json index e4769acc9a5..cfdefb2ed09 100644 --- a/homeassistant/components/vesync/icons.json +++ b/homeassistant/components/vesync/icons.json @@ -1,20 +1,4 @@ { - "entity": { - "fan": { - "vesync": { - "state_attributes": { - "preset_mode": { - "state": { - "auto": "mdi:fan-auto", - "sleep": "mdi:sleep", - "pet": "mdi:paw", - "turbo": "mdi:weather-tornado" - } - } - } - } - } - }, "services": { "update_devices": { "service": "mdi:update" diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index b6e4e2fd957..5ff0aa58722 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -42,20 +42,6 @@ "current_voltage": { "name": "Current voltage" } - }, - "fan": { - "vesync": { - "state_attributes": { - "preset_mode": { - "state": { - "auto": "Auto", - "sleep": "Sleep", - "pet": "Pet", - "turbo": "Turbo" - } - } - } - } } }, "services": { diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 307fcaf0ea8..136aa94b43a 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) @@ -43,10 +43,12 @@ def _calculate_uptime( ) -> datetime: """Calculate device uptime.""" + assert isinstance(last_value, datetime) + delta_uptime = coordinator.api.convert_uptime(coordinator.data.sensors[key]) if ( - not isinstance(last_value, datetime) + not last_value or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION ): return delta_uptime 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/weheat/manifest.json b/homeassistant/components/weheat/manifest.json index ef89a2f1acb..d32e0ce4047 100644 --- a/homeassistant/components/weheat/manifest.json +++ b/homeassistant/components/weheat/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/weheat", "iot_class": "cloud_polling", - "requirements": ["weheat==2024.11.02"] + "requirements": ["weheat==2024.09.23"] } 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/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index 8ddfdbd592d..b9d51cd3c36 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -4,7 +4,7 @@ from typing import Any import zeroconf -from homeassistant.helpers.frame import ReportBehavior, report_usage +from homeassistant.helpers.frame import report from .models import HaZeroconf @@ -16,14 +16,14 @@ def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: """ def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: - report_usage( + report( ( "attempted to create another Zeroconf instance. Please use the shared" " Zeroconf via await" " homeassistant.components.zeroconf.async_get_instance(hass)" ), exclude_integrations={"zeroconf"}, - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) return hass_zc 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/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 3631bf1163b..e3f643486a0 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -10,7 +10,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.59.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.59.0"], "usb": [ { "vid": "0658", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f1748c6b7fb..64eadeb0d7e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3158,12 +3158,11 @@ class OptionsFlow(ConfigEntryBaseFlow): @config_entry.setter def config_entry(self, value: ConfigEntry) -> None: """Set the config entry value.""" - report_usage( + report( "sets option flow config_entry explicitly, which is deprecated " "and will stop working in 2025.12", - core_behavior=ReportBehavior.ERROR, - core_integration_behavior=ReportBehavior.ERROR, - custom_integration_behavior=ReportBehavior.LOG, + error_if_integration=False, + error_if_core=True, ) self._config_entry = value diff --git a/homeassistant/const.py b/homeassistant/const.py index 4082a076b94..0bdd625e417 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}" @@ -1358,13 +1358,6 @@ CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" -class UnitOfBloodGlucoseConcentration(StrEnum): - """Blood glucose concentration units.""" - - MILLIGRAMS_PER_DECILITER = "mg/dL" - MILLIMOLE_PER_LITER = "mmol/L" - - # Speed units class UnitOfSpeed(StrEnum): """Speed units.""" 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/dhcp.py b/homeassistant/generated/dhcp.py index 7dacf9a0bca..cd20b88b285 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -379,15 +379,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "gateway*", "macaddress": "F8811A*", }, - { - "domain": "palazzetti", - "hostname": "connbox*", - "macaddress": "40F3857*", - }, - { - "domain": "palazzetti", - "registered_devices": True, - }, { "domain": "powerwall", "hostname": "1118431-*", 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/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2b35ebade76..81ac10f86cc 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -719,14 +719,14 @@ def template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value should be a string") if not (hass := _async_get_hass_or_none()): # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import report - report_usage( + report( ( "validates schema outside the event loop, " "which will stop working in HA Core 2025.10" ), - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) template_value = template_helper.Template(str(value), hass) @@ -748,14 +748,14 @@ def dynamic_template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value does not contain a dynamic template") if not (hass := _async_get_hass_or_none()): # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import report - report_usage( + report( ( "validates schema outside the event loop, " "which will stop working in HA Core 2025.10" ), - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) template_value = template_helper.Template(str(value), hass) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 61a798dbd75..02ea8103192 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -997,14 +997,14 @@ class TrackTemplateResultInfo: continue # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import report - report_usage( + report( ( "calls async_track_template_result with template without hass, " "which will stop working in HA Core 2025.10" ), - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) track_template_.template.hass = hass diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e3da52604cb..33e8f3d3d6e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1277,14 +1277,14 @@ def async_register_entity_service( schema = cv.make_entity_service_schema(schema) elif not cv.is_entity_service_schema(schema): # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import report - report_usage( + report( ( "registers an entity service with a non entity service schema " "which will stop working in HA Core 2025.9" ), - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) service_func: str | HassJob[..., Any] diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2eab666bbd4..753464c35d5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -515,18 +515,18 @@ class Template: will be non optional in Home Assistant Core 2025.10. """ # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import report if not isinstance(template, str): raise TypeError("Expected template to be a string") if not hass: - report_usage( + report( ( "creates a template object without passing hass, " "which will stop working in HA Core 2025.10" ), - core_behavior=ReportBehavior.LOG, + error_if_core=False, ) self.template: str = template.strip() diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 87d55891e90..f5c2a2a1288 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -29,7 +29,7 @@ from homeassistant.util.dt import utcnow from . import entity, event from .debounce import Debouncer -from .frame import report_usage +from .frame import report from .typing import UNDEFINED, UndefinedType REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 @@ -286,20 +286,24 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): to ensure that multiple retries do not cause log spam. """ if self.config_entry is None: - report_usage( + report( "uses `async_config_entry_first_refresh`, which is only supported " "for coordinators with a config entry and will stop working in " - "Home Assistant 2025.11" + "Home Assistant 2025.11", + error_if_core=True, + error_if_integration=False, ) elif ( self.config_entry.state is not config_entries.ConfigEntryState.SETUP_IN_PROGRESS ): - report_usage( + report( "uses `async_config_entry_first_refresh`, which is only supported " f"when entry state is {config_entries.ConfigEntryState.SETUP_IN_PROGRESS}, " f"but it is in state {self.config_entry.state}, " "This will stop working in Home Assistant 2025.11", + error_if_core=True, + error_if_integration=False, ) if await self.__wrap_async_setup(): await self._async_refresh( diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5bc539beb86..99811a11bab 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.0b4 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 @@ -13,7 +13,6 @@ async-interrupt==1.2.0 async-upnp-client==0.41.0 atomicwrites-homeassistant==1.4.1 attrs==24.2.0 -audioop-lts==0.2.1;python_version>='3.13' av==13.1.0 awesomeversion==24.6.0 bcrypt==4.2.0 @@ -28,14 +27,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 @@ -44,7 +43,7 @@ mutagen==1.47.0 orjson==3.10.11 packaging>=23.1 paho-mqtt==1.6.1 -Pillow==11.0.0 +Pillow==10.4.0 propcache==0.2.0 psutil-home-assistant==0.0.1 PyJWT==2.9.0 @@ -60,8 +59,6 @@ PyYAML==6.0.2 requests==2.32.3 securetar==2024.2.1 SQLAlchemy==2.0.31 -standard-aifc==3.13.0;python_version>='3.13' -standard-telnetlib==3.13.0;python_version>='3.13' typing-extensions>=4.12.2,<5.0 ulid-transform==1.0.2 urllib3>=1.26.5,<2 @@ -84,9 +81,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.67.1 -grpcio-status==1.67.1 -grpcio-reflection==1.67.1 +grpcio==1.66.2 +grpcio-status==1.66.2 +grpcio-reflection==1.66.2 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 @@ -115,7 +112,7 @@ httpcore==1.0.5 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==2.1.3 +numpy==2.1.2 pandas~=2.2.3 # Constrain multidict to avoid typing issues @@ -127,7 +124,7 @@ backoff>=2.0 # Required to avoid breaking (#101042). # v2 has breaking changes (#99218). -pydantic==1.10.19 +pydantic==1.10.18 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 @@ -181,8 +178,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/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 1bf3561e66a..289df28738a 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -10,7 +10,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, UNIT_NOT_RECOGNIZED_TEMPLATE, - UnitOfBloodGlucoseConcentration, UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, @@ -174,17 +173,6 @@ class DistanceConverter(BaseUnitConverter): } -class BloodGlucoseConcentrationConverter(BaseUnitConverter): - """Utility to convert blood glucose concentration values.""" - - UNIT_CLASS = "blood_glucose_concentration" - _UNIT_CONVERSION: dict[str | None, float] = { - UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER: 18, - UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER: 1, - } - VALID_UNITS = set(UnitOfBloodGlucoseConcentration) - - class ConductivityConverter(BaseUnitConverter): """Utility to convert electric current values.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 39d38a8f47d..39ac17d94f9 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -25,6 +25,7 @@ except ImportError: from propcache import cached_property from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.frame import report from .const import SECRET_YAML from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass @@ -143,6 +144,37 @@ class FastSafeLoader(FastestAvailableSafeLoader, _LoaderMixin): self.secrets = secrets +class SafeLoader(FastSafeLoader): + """Provided for backwards compatibility. Logs when instantiated.""" + + def __init__(*args: Any, **kwargs: Any) -> None: + """Log a warning and call super.""" + SafeLoader.__report_deprecated() + FastSafeLoader.__init__(*args, **kwargs) + + @classmethod + def add_constructor(cls, tag: str, constructor: Callable) -> None: + """Log a warning and call super.""" + SafeLoader.__report_deprecated() + FastSafeLoader.add_constructor(tag, constructor) + + @classmethod + def add_multi_constructor( + cls, tag_prefix: str, multi_constructor: Callable + ) -> None: + """Log a warning and call super.""" + SafeLoader.__report_deprecated() + FastSafeLoader.add_multi_constructor(tag_prefix, multi_constructor) + + @staticmethod + def __report_deprecated() -> None: + """Log deprecation warning.""" + report( + "uses deprecated 'SafeLoader' instead of 'FastSafeLoader', " + "which will stop working in HA Core 2024.6," + ) + + class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin): """Python safe loader.""" @@ -152,6 +184,37 @@ class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin): self.secrets = secrets +class SafeLineLoader(PythonSafeLoader): + """Provided for backwards compatibility. Logs when instantiated.""" + + def __init__(*args: Any, **kwargs: Any) -> None: + """Log a warning and call super.""" + SafeLineLoader.__report_deprecated() + PythonSafeLoader.__init__(*args, **kwargs) + + @classmethod + def add_constructor(cls, tag: str, constructor: Callable) -> None: + """Log a warning and call super.""" + SafeLineLoader.__report_deprecated() + PythonSafeLoader.add_constructor(tag, constructor) + + @classmethod + def add_multi_constructor( + cls, tag_prefix: str, multi_constructor: Callable + ) -> None: + """Log a warning and call super.""" + SafeLineLoader.__report_deprecated() + PythonSafeLoader.add_multi_constructor(tag_prefix, multi_constructor) + + @staticmethod + def __report_deprecated() -> None: + """Log deprecation warning.""" + report( + "uses deprecated 'SafeLineLoader' instead of 'PythonSafeLoader', " + "which will stop working in HA Core 2024.6," + ) + + type LoaderType = FastSafeLoader | PythonSafeLoader diff --git a/pyproject.toml b/pyproject.toml index ebf22a93d7d..7855a6671cc 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.0b4", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", @@ -37,7 +36,6 @@ dependencies = [ "async-interrupt==1.2.0", "attrs==24.2.0", "atomicwrites-homeassistant==1.4.1", - "audioop-lts==0.2.1;python_version>='3.13'", "awesomeversion==24.6.0", "bcrypt==4.2.0", "certifi>=2021.5.30", @@ -56,7 +54,7 @@ dependencies = [ "PyJWT==2.9.0", # PyJWT has loose dependency. We want the latest one. "cryptography==43.0.1", - "Pillow==11.0.0", + "Pillow==10.4.0", "propcache==0.2.0", "pyOpenSSL==24.2.1", "orjson==3.10.11", @@ -67,8 +65,6 @@ dependencies = [ "requests==2.32.3", "securetar==2024.2.1", "SQLAlchemy==2.0.31", - "standard-aifc==3.13.0;python_version>='3.13'", - "standard-telnetlib==3.13.0;python_version>='3.13'", "typing-extensions>=4.12.2,<5.0", "ulid-transform==1.0.2", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 @@ -621,17 +617,6 @@ filterwarnings = [ # https://github.com/ssaenger/pyws66i/blob/v1.1/pyws66i/__init__.py#L2 "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pyws66i", - # -- New in Python 3.13 - # https://github.com/kurtmckee/feedparser/pull/389 - >6.0.11 - # https://github.com/kurtmckee/feedparser/issues/481 - "ignore:'count' is passed as positional argument:DeprecationWarning:feedparser.html", - # https://github.com/youknowone/python-deadlib - Backports for aifc, telnetlib - "ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition", - "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor", - "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection", - "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:plumlightpad.lightpad", - "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i", - # -- unmaintained projects, last release about 2+ years # https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a", diff --git a/requirements.txt b/requirements.txt index b97c8dc57a0..c7436cab5b8 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.0b4 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 @@ -13,7 +13,6 @@ astral==2.2 async-interrupt==1.2.0 attrs==24.2.0 atomicwrites-homeassistant==1.4.1 -audioop-lts==0.2.1;python_version>='3.13' awesomeversion==24.6.0 bcrypt==4.2.0 certifi>=2021.5.30 @@ -27,7 +26,7 @@ Jinja2==3.1.4 lru-dict==1.3.0 PyJWT==2.9.0 cryptography==43.0.1 -Pillow==11.0.0 +Pillow==10.4.0 propcache==0.2.0 pyOpenSSL==24.2.1 orjson==3.10.11 @@ -38,8 +37,6 @@ PyYAML==6.0.2 requests==2.32.3 securetar==2024.2.1 SQLAlchemy==2.0.31 -standard-aifc==3.13.0;python_version>='3.13' -standard-telnetlib==3.13.0;python_version>='3.13' typing-extensions>=4.12.2,<5.0 ulid-transform==1.0.2 urllib3>=1.26.5,<2 diff --git a/requirements_all.txt b/requirements_all.txt index 65ef5f1ebf2..f883405070c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -33,7 +33,7 @@ Mastodon.py==1.8.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==11.0.0 +Pillow==10.4.0 # homeassistant.components.plex PlexAPI==4.15.16 @@ -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,10 +354,10 @@ 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 +aiorussound==4.0.5 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.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 @@ -395,9 +392,6 @@ aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig aiotankerkoenig==0.4.2 -# homeassistant.components.tedee -aiotedee==0.2.20 - # homeassistant.components.tractive aiotractive==0.6.0 @@ -420,7 +414,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 @@ -738,7 +732,7 @@ debugpy==1.8.6 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.4.1 +deebot-client==8.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -863,7 +857,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.4.1 +eq3btsmart==1.2.0 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -996,7 +990,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 @@ -1021,7 +1015,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.5 +google-nest-sdm==6.1.4 # homeassistant.components.google_photos google-photos-library-api==0.12.1 @@ -1069,7 +1063,7 @@ gspread==5.5.0 gstreamer-player==1.1.2 # homeassistant.components.profiler -guppy3==3.1.4.post1;python_version<'3.13' +guppy3==3.1.4.post1 # homeassistant.components.iaqualink h2==4.1.0 @@ -1096,7 +1090,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 +1127,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 +1145,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.12 +huum==0.7.11 # homeassistant.components.hyperion hyperion-py==0.7.5 @@ -1271,7 +1265,7 @@ lakeside==0.13 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.2 +lcn-frontend==0.2.1 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 @@ -1312,6 +1306,9 @@ linear-garage-door==0.2.9 # homeassistant.components.linode linode-api==4.1.9b1 +# homeassistant.components.lamarzocco +lmcloud==1.2.3 + # homeassistant.components.google_maps locationsharinglib==5.0.1 @@ -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 @@ -1494,7 +1491,7 @@ numato-gpio==0.13.0 # homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==2.1.3 +numpy==2.1.2 # homeassistant.components.nyt_games nyt_games==0.4.4 @@ -1654,7 +1651,7 @@ proxmoxer==2.0.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==6.1.0 +psutil==6.0.0 # homeassistant.components.pulseaudio_loopback pulsectl==23.5.2 @@ -1741,7 +1738,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.30.4 # homeassistant.components.dlink pyW215==0.7.0 @@ -2026,9 +2023,6 @@ pykwb==0.0.8 # homeassistant.components.lacrosse pylacrosse==0.4 -# homeassistant.components.lamarzocco -pylamarzocco==1.2.3 - # homeassistant.components.lastfm pylast==5.1.0 @@ -2102,7 +2096,7 @@ pynetio==0.1.9.1 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.2 +pynordpool==0.2.1 # homeassistant.components.nuki pynuki==1.6.3 @@ -2155,7 +2149,7 @@ pyoverkiz==1.14.1 pyownet==0.10.0.post1 # homeassistant.components.palazzetti -pypalazzetti==0.1.11 +pypalazzetti==0.1.10 # homeassistant.components.elv pypca==0.0.7 @@ -2301,6 +2295,9 @@ pyswitchbee==1.8.3 # homeassistant.components.tautulli pytautulli==23.1.1 +# homeassistant.components.tedee +pytedee-async==0.2.20 + # homeassistant.components.thinkingcleaner pythinkingcleaner==0.0.3 @@ -2365,7 +2362,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.20 +python-linkplay==0.0.18 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -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 @@ -2695,7 +2689,7 @@ smhi-pkg==1.0.18 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.6 +soco==0.30.4 # homeassistant.components.solaredge_local solaredge-local==0.2.3 @@ -2719,7 +2713,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.7 # homeassistant.components.sql sqlparse==0.5.0 @@ -2864,7 +2858,7 @@ total-connect-client==2024.5 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink-omada-client==1.4.3 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2879,7 +2873,7 @@ ttls==1.8.3 ttn_client==1.2.0 # homeassistant.components.tuya -tuya-device-sharing-sdk==0.2.1 +tuya-device-sharing-sdk==0.1.9 # homeassistant.components.twentemilieu twentemilieu==2.0.1 @@ -2993,7 +2987,7 @@ webio-api==0.1.8 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2024.11.02 +weheat==2024.09.23 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.8 @@ -3093,7 +3087,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.59.1 +zwave-js-server-python==0.59.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test.txt b/requirements_test.txt index 166fd965e2c..241fff89ac3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ license-expression==30.4.0 mock-open==1.4.0 mypy-dev==1.14.0a2 pre-commit==4.0.0 -pydantic==1.10.19 +pydantic==1.10.18 pylint==3.3.1 pylint-per-file-ignores==1.3.2 pipdeptree==2.23.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b61e65f3c68..a4d7dd7f85b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ Mastodon.py==1.8.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==11.0.0 +Pillow==10.4.0 # homeassistant.components.plex PlexAPI==4.15.16 @@ -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,10 +336,10 @@ 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 +aiorussound==4.0.5 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.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 @@ -377,9 +374,6 @@ aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig aiotankerkoenig==0.4.2 -# homeassistant.components.tedee -aiotedee==0.2.20 - # homeassistant.components.tractive aiotractive==0.6.0 @@ -402,7 +396,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 @@ -628,7 +622,7 @@ dbus-fast==2.24.3 debugpy==1.8.6 # homeassistant.components.ecovacs -deebot-client==8.4.1 +deebot-client==8.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -732,7 +726,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.4.1 +eq3btsmart==1.2.0 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 @@ -846,7 +840,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 @@ -871,7 +865,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.5 +google-nest-sdm==6.1.4 # homeassistant.components.google_photos google-photos-library-api==0.12.1 @@ -907,7 +901,7 @@ growattServer==1.5.0 gspread==5.5.0 # homeassistant.components.profiler -guppy3==3.1.4.post1;python_version<'3.13' +guppy3==3.1.4.post1 # homeassistant.components.iaqualink h2==4.1.0 @@ -931,7 +925,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 +953,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 +968,7 @@ httplib2==0.20.4 huawei-lte-api==1.10.0 # homeassistant.components.huum -huum==0.7.12 +huum==0.7.11 # homeassistant.components.hyperion hyperion-py==0.7.5 @@ -1067,7 +1061,7 @@ lacrosse-view==1.0.3 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.2 +lcn-frontend==0.2.1 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 @@ -1093,6 +1087,9 @@ libsoundtouch==0.8 # homeassistant.components.linear_garage_door linear-garage-door==0.2.9 +# homeassistant.components.lamarzocco +lmcloud==1.2.3 + # homeassistant.components.london_underground london-tube-status==0.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 @@ -1242,7 +1239,7 @@ numato-gpio==0.13.0 # homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==2.1.3 +numpy==2.1.2 # homeassistant.components.nyt_games nyt_games==0.4.4 @@ -1352,7 +1349,7 @@ prometheus-client==0.21.0 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==6.1.0 +psutil==6.0.0 # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 @@ -1418,7 +1415,7 @@ pyElectra==1.2.4 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.30.4 # homeassistant.components.dlink pyW215==0.7.0 @@ -1631,9 +1628,6 @@ pykrakenapi==0.1.8 # homeassistant.components.kulersky pykulersky==0.5.2 -# homeassistant.components.lamarzocco -pylamarzocco==1.2.3 - # homeassistant.components.lastfm pylast==5.1.0 @@ -1695,7 +1689,7 @@ pynetgear==0.10.10 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.2 +pynordpool==0.2.1 # homeassistant.components.nuki pynuki==1.6.3 @@ -1742,7 +1736,7 @@ pyoverkiz==1.14.1 pyownet==0.10.0.post1 # homeassistant.components.palazzetti -pypalazzetti==0.1.11 +pypalazzetti==0.1.10 # homeassistant.components.lcn pypck==0.7.24 @@ -1858,6 +1852,9 @@ pyswitchbee==1.8.3 # homeassistant.components.tautulli pytautulli==23.1.1 +# homeassistant.components.tedee +pytedee-async==0.2.20 + # homeassistant.components.motionmount python-MotionMount==2.2.0 @@ -1892,7 +1889,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.20 +python-linkplay==0.0.18 # homeassistant.components.matter python-matter-server==6.6.0 @@ -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 @@ -2150,7 +2144,7 @@ smhi-pkg==1.0.18 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.6 +soco==0.30.4 # homeassistant.components.solarlog solarlog_cli==0.3.2 @@ -2171,7 +2165,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.7 # homeassistant.components.sql sqlparse==0.5.0 @@ -2277,7 +2271,7 @@ toonapi==0.3.0 total-connect-client==2024.5 # homeassistant.components.tplink_omada -tplink-omada-client==1.4.3 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2292,7 +2286,7 @@ ttls==1.8.3 ttn_client==1.2.0 # homeassistant.components.tuya -tuya-device-sharing-sdk==0.2.1 +tuya-device-sharing-sdk==0.1.9 # homeassistant.components.twentemilieu twentemilieu==2.0.1 @@ -2388,7 +2382,7 @@ webio-api==0.1.8 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2024.11.02 +weheat==2024.09.23 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.8 @@ -2467,7 +2461,7 @@ zeversolar==0.3.2 zha==0.0.37 # homeassistant.components.zwave_js -zwave-js-server-python==0.59.1 +zwave-js-server-python==0.59.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 23f584dd0de..bab89d20584 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.7.3 +ruff==0.7.2 yamllint==1.35.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7d53741c661..02dad3aef3f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -117,9 +117,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.67.1 -grpcio-status==1.67.1 -grpcio-reflection==1.67.1 +grpcio==1.66.2 +grpcio-status==1.66.2 +grpcio-reflection==1.66.2 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 @@ -148,7 +148,7 @@ httpcore==1.0.5 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==2.1.3 +numpy==2.1.2 pandas~=2.2.3 # Constrain multidict to avoid typing issues @@ -160,7 +160,7 @@ backoff>=2.0 # Required to avoid breaking (#101042). # v2 has breaking changes (#99218). -pydantic==1.10.19 +pydantic==1.10.18 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 @@ -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..083cdaba1a9 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.6" + 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..745159d61d3 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 \ @@ -22,8 +22,8 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \ --no-cache \ -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 + stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.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/script/hassfest/services.py b/script/hassfest/services.py index 8c9ab5c0c0b..92fca14d373 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -75,14 +75,6 @@ CUSTOM_INTEGRATION_FIELD_SCHEMA = CORE_INTEGRATION_FIELD_SCHEMA.extend( } ) -CUSTOM_INTEGRATION_SECTION_SCHEMA = vol.Schema( - { - vol.Optional("collapsed"): bool, - vol.Required("fields"): vol.Schema({str: CUSTOM_INTEGRATION_FIELD_SCHEMA}), - } -) - - CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any( vol.Schema( { @@ -113,17 +105,7 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any( vol.Optional("target"): vol.Any( selector.TargetSelector.CONFIG_SCHEMA, None ), - vol.Optional("fields"): vol.All( - vol.Schema( - { - str: vol.Any( - CUSTOM_INTEGRATION_FIELD_SCHEMA, - CUSTOM_INTEGRATION_SECTION_SCHEMA, - ) - } - ), - unique_field_validator, - ), + vol.Optional("fields"): vol.Schema({str: CUSTOM_INTEGRATION_FIELD_SCHEMA}), } ), None, diff --git a/script/licenses.py b/script/licenses.py index 464a2fc456b..f4d534365bc 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -84,7 +84,6 @@ OSI_APPROVED_LICENSES_SPDX = { "LGPL-3.0-only", "LGPL-3.0-or-later", "MIT", - "MIT-CMU", "MPL-1.1", "MPL-2.0", "PSF-2.0", 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_http.py b/tests/components/backup/test_http.py index 76b1f76b55b..93ecb27bc97 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -1,11 +1,8 @@ """Tests for the Backup integration.""" -import asyncio -from io import StringIO from unittest.mock import patch from aiohttp import web -import pytest from homeassistant.core import HomeAssistant @@ -52,12 +49,12 @@ async def test_downloading_backup_not_found( assert resp.status == 404 -async def test_downloading_as_non_admin( +async def test_non_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, ) -> None: - """Test downloading a backup file when you are not an admin.""" + """Test downloading a backup file that does not exist.""" hass_admin_user.groups = [] await setup_backup_integration(hass) @@ -65,53 +62,3 @@ async def test_downloading_as_non_admin( resp = await client.get("/api/backup/download/abc123") assert resp.status == 401 - - -async def test_uploading_a_backup_file( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, -) -> None: - """Test uploading a backup file.""" - await setup_backup_integration(hass) - - client = await hass_client() - - with patch( - "homeassistant.components.backup.manager.BackupManager.async_receive_backup", - ) as async_receive_backup_mock: - resp = await client.post( - "/api/backup/upload", - data={"file": StringIO("test")}, - ) - assert resp.status == 201 - assert async_receive_backup_mock.called - - -@pytest.mark.parametrize( - ("error", "message"), - [ - (OSError("Boom!"), "Can't write backup file Boom!"), - (asyncio.CancelledError("Boom!"), ""), - ], -) -async def test_error_handling_uploading_a_backup_file( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - error: Exception, - message: str, -) -> None: - """Test error handling when uploading a backup file.""" - await setup_backup_integration(hass) - - client = await hass_client() - - with patch( - "homeassistant.components.backup.manager.BackupManager.async_receive_backup", - side_effect=error, - ): - resp = await client.post( - "/api/backup/upload", - data={"file": StringIO("test")}, - ) - assert resp.status == 500 - assert await resp.text() == message diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 9d24964aedf..a4dba5c6936 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -2,18 +2,13 @@ from __future__ import annotations -import asyncio -from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, Mock, patch -import aiohttp -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 +18,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 +174,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 +245,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 +262,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 +282,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( @@ -320,40 +335,6 @@ async def test_loading_platforms_when_running_async_post_backup_actions( assert "Loaded 1 platforms" in caplog.text -async def test_async_receive_backup( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test receiving a backup file.""" - manager = BackupManager(hass) - - size = 2 * 2**16 - protocol = Mock(_reading_paused=False) - stream = aiohttp.StreamReader(protocol, 2**16) - stream.feed_data(b"0" * size + b"\r\n--:--") - stream.feed_eof() - - open_mock = mock_open() - - with patch("pathlib.Path.open", open_mock), patch("shutil.move") as mover_mock: - await manager.async_receive_backup( - contents=aiohttp.BodyPartReader( - b"--:", - CIMultiDictProxy( - CIMultiDict( - { - aiohttp.hdrs.CONTENT_DISPOSITION: "attachment; filename=abc123.tar" - } - ) - ), - stream, - ) - ) - assert open_mock.call_count == 1 - assert mover_mock.call_count == 1 - assert mover_mock.mock_calls[0].args[1].name == "abc123.tar" - - async def test_async_trigger_restore( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, 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/bang_olufsen/const.py b/tests/components/bang_olufsen/const.py index 6602a898eb6..3769aef5cd3 100644 --- a/tests/components/bang_olufsen/const.py +++ b/tests/components/bang_olufsen/const.py @@ -16,7 +16,6 @@ from mozart_api.models import ( PlayQueueItemType, RenderingState, SceneProperties, - Source, UserFlow, VolumeLevel, VolumeMute, @@ -126,10 +125,7 @@ TEST_DATA_ZEROCONF_IPV6 = ZeroconfServiceInfo( }, ) -TEST_SOURCE = Source( - name="Tidal", id="tidal", is_seekable=True, is_enabled=True, is_playable=True -) -TEST_AUDIO_SOURCES = [TEST_SOURCE.name, BangOlufsenSource.LINE_IN.name] +TEST_AUDIO_SOURCES = [BangOlufsenSource.TIDAL.name, BangOlufsenSource.LINE_IN.name] TEST_VIDEO_SOURCES = ["HDMI A"] TEST_SOURCES = TEST_AUDIO_SOURCES + TEST_VIDEO_SOURCES TEST_FALLBACK_SOURCES = [ diff --git a/tests/components/bang_olufsen/snapshots/test_media_player.ambr b/tests/components/bang_olufsen/snapshots/test_media_player.ambr index ea96e286821..e48dc39198b 100644 --- a/tests/components/bang_olufsen/snapshots/test_media_player.ambr +++ b/tests/components/bang_olufsen/snapshots/test_media_player.ambr @@ -573,7 +573,7 @@ 'Test Listening Mode (234)', 'Test Listening Mode 2 (345)', ]), - 'source': 'Line-In', + 'source': 'Chromecast built-in', 'source_list': list([ 'Tidal', 'Line-In', diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index aa35b0265dc..e991ab3d1bc 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -105,7 +105,6 @@ from .const import ( TEST_SEEK_POSITION_HOME_ASSISTANT_FORMAT, TEST_SOUND_MODE_2, TEST_SOUND_MODES, - TEST_SOURCE, TEST_SOURCES, TEST_VIDEO_SOURCES, TEST_VOLUME, @@ -232,7 +231,7 @@ async def test_async_update_sources_availability( # Add a source that is available and playable mock_mozart_client.get_available_sources.return_value = SourceArray( - items=[TEST_SOURCE] + items=[BangOlufsenSource.TIDAL] ) # Send playback_source. The source is not actually used, so its attributes don't matter @@ -240,7 +239,7 @@ async def test_async_update_sources_availability( assert mock_mozart_client.get_available_sources.call_count == 2 assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) - assert states.attributes[ATTR_INPUT_SOURCE_LIST] == [TEST_SOURCE.name] + assert states.attributes[ATTR_INPUT_SOURCE_LIST] == [BangOlufsenSource.TIDAL.name] async def test_async_update_playback_metadata( @@ -358,17 +357,19 @@ async def test_async_update_playback_state( @pytest.mark.parametrize( - ("source", "content_type", "progress", "metadata"), + ("reported_source", "real_source", "content_type", "progress", "metadata"), [ - # Normal source, music mediatype expected + # Normal source, music mediatype expected, no progress expected ( - TEST_SOURCE, + BangOlufsenSource.TIDAL, + BangOlufsenSource.TIDAL, MediaType.MUSIC, TEST_PLAYBACK_PROGRESS.progress, PlaybackContentMetadata(), ), - # URI source, url media type expected + # URI source, url media type expected, no progress expected ( + BangOlufsenSource.URI_STREAMER, BangOlufsenSource.URI_STREAMER, MediaType.URL, TEST_PLAYBACK_PROGRESS.progress, @@ -377,17 +378,44 @@ async def test_async_update_playback_state( # Line-In source,media type expected, progress 0 expected ( BangOlufsenSource.LINE_IN, + BangOlufsenSource.CHROMECAST, MediaType.MUSIC, 0, PlaybackContentMetadata(), ), + # Chromecast as source, but metadata says Line-In. + # Progress is not set to 0 as the source is Chromecast first + ( + BangOlufsenSource.CHROMECAST, + BangOlufsenSource.LINE_IN, + MediaType.MUSIC, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(title=BangOlufsenSource.LINE_IN.name), + ), + # Chromecast as source, but metadata says Bluetooth + ( + BangOlufsenSource.CHROMECAST, + BangOlufsenSource.BLUETOOTH, + MediaType.MUSIC, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(title=BangOlufsenSource.BLUETOOTH.name), + ), + # Chromecast as source, but metadata says Bluetooth in another way + ( + BangOlufsenSource.CHROMECAST, + BangOlufsenSource.BLUETOOTH, + MediaType.MUSIC, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(art=[]), + ), ], ) async def test_async_update_source_change( hass: HomeAssistant, mock_mozart_client: AsyncMock, mock_config_entry: MockConfigEntry, - source: Source, + reported_source: Source, + real_source: Source, content_type: MediaType, progress: int, metadata: PlaybackContentMetadata, @@ -416,10 +444,10 @@ async def test_async_update_source_change( # Simulate metadata playback_metadata_callback(metadata) - source_change_callback(source) + source_change_callback(reported_source) assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) - assert states.attributes[ATTR_INPUT_SOURCE] == source.name + assert states.attributes[ATTR_INPUT_SOURCE] == real_source.name assert states.attributes[ATTR_MEDIA_CONTENT_TYPE] == content_type assert states.attributes[ATTR_MEDIA_POSITION] == progress @@ -746,7 +774,7 @@ async def test_async_media_next_track( ("source", "expected_result", "seek_called_times"), [ # Seekable source, seek expected - (TEST_SOURCE, does_not_raise(), 1), + (BangOlufsenSource.DEEZER, does_not_raise(), 1), # Non seekable source, seek shouldn't work (BangOlufsenSource.LINE_IN, pytest.raises(HomeAssistantError), 0), # Malformed source, seek shouldn't work @@ -834,7 +862,7 @@ async def test_async_clear_playlist( # Invalid source ("Test source", pytest.raises(ServiceValidationError), 0, 0), # Valid audio source - (TEST_SOURCE.name, does_not_raise(), 1, 0), + (BangOlufsenSource.TIDAL.name, does_not_raise(), 1, 0), # Valid video source (TEST_VIDEO_SOURCES[0], does_not_raise(), 0, 1), ], @@ -1404,7 +1432,7 @@ async def test_async_join_players( await hass.config_entries.async_setup(mock_config_entry_2.entry_id) # Set the source to a beolink expandable source - source_change_callback(TEST_SOURCE) + source_change_callback(BangOlufsenSource.TIDAL) await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -1440,7 +1468,7 @@ async def test_async_join_players( ), # Invalid media_player entity ( - TEST_SOURCE, + BangOlufsenSource.TIDAL, [TEST_MEDIA_PLAYER_ENTITY_ID_3], pytest.raises(ServiceValidationError), "invalid_grouping_entity", @@ -1609,7 +1637,7 @@ async def test_async_beolink_expand( ) # Set the source to a beolink expandable source - source_change_callback(TEST_SOURCE) + source_change_callback(BangOlufsenSource.TIDAL) await hass.services.async_call( DOMAIN, 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/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index f8f94d44126..92d9450b670 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -44,7 +44,7 @@ async def test_form_home(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Electricity Maps" + assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", } @@ -185,7 +185,7 @@ async def test_form_error_handling( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Electricity Maps" + assert result["title"] == "CO2 Signal" assert result["data"] == { "api_key": "api_key", } 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..14a9b0ca88c 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -418,44 +418,6 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None: assert len(callback.mock_calls) == 0 -@pytest.mark.parametrize( - ("language", "expected"), - [("en", "English done"), ("de", "German done"), ("not_translated", "Done")], -) -@pytest.mark.usefixtures("init_components") -async def test_trigger_sentence_response_translation( - hass: HomeAssistant, language: str, expected: str -) -> None: - """Test translation of default response 'done'.""" - hass.config.language = language - - agent = hass.data[DATA_DEFAULT_ENTITY] - assert isinstance(agent, default_agent.DefaultAgent) - - translations = { - "en": {"component.conversation.conversation.agent.done": "English done"}, - "de": {"component.conversation.conversation.agent.done": "German done"}, - "not_translated": {}, - } - - with patch( - "homeassistant.components.conversation.default_agent.translation.async_get_translations", - return_value=translations.get(language), - ): - unregister = agent.register_trigger( - ["test sentence"], AsyncMock(return_value=None) - ) - result = await conversation.async_converse( - hass, "test sentence", None, Context() - ) - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert result.response.speech == { - "plain": {"speech": expected, "extra_data": None} - } - - unregister() - - @pytest.mark.usefixtures("init_components", "sl_setup") async def test_shopping_list_add_item(hass: HomeAssistant) -> None: """Test adding an item to the shopping list through the default agent.""" @@ -770,8 +732,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 +800,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 +835,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 +1009,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 +1058,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 +1169,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 +1191,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/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index e0e82d68863..1573484795f 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -160,7 +160,6 @@ "hasHumidifier": true, "humidifierMode": "manual", "hasHeatPump": true, - "compressorProtectionMinTemp": 100, "humidity": "30" }, "equipmentStatus": "fan", diff --git a/tests/components/ecobee/test_number.py b/tests/components/ecobee/test_number.py index be65b6dbb30..5b01fe8c5ba 100644 --- a/tests/components/ecobee/test_number.py +++ b/tests/components/ecobee/test_number.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from .common import setup_platform -VENTILATOR_MIN_HOME_ID = "number.ecobee_ventilator_minimum_time_home" -VENTILATOR_MIN_AWAY_ID = "number.ecobee_ventilator_minimum_time_away" +VENTILATOR_MIN_HOME_ID = "number.ecobee_ventilator_min_time_home" +VENTILATOR_MIN_AWAY_ID = "number.ecobee_ventilator_min_time_away" THERMOSTAT_ID = 0 @@ -26,9 +26,7 @@ async def test_ventilator_min_on_home_attributes(hass: HomeAssistant) -> None: assert state.attributes.get("min") == 0 assert state.attributes.get("max") == 60 assert state.attributes.get("step") == 5 - assert ( - state.attributes.get("friendly_name") == "ecobee Ventilator minimum time home" - ) + assert state.attributes.get("friendly_name") == "ecobee Ventilator min time home" assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES @@ -41,9 +39,7 @@ async def test_ventilator_min_on_away_attributes(hass: HomeAssistant) -> None: assert state.attributes.get("min") == 0 assert state.attributes.get("max") == 60 assert state.attributes.get("step") == 5 - assert ( - state.attributes.get("friendly_name") == "ecobee Ventilator minimum time away" - ) + assert state.attributes.get("friendly_name") == "ecobee Ventilator min time away" assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES @@ -81,42 +77,3 @@ async def test_set_min_time_away(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() mock_set_min_away_time.assert_called_once_with(THERMOSTAT_ID, target_value) - - -COMPRESSOR_MIN_TEMP_ID = "number.ecobee2_compressor_minimum_temperature" - - -async def test_compressor_protection_min_temp_attributes(hass: HomeAssistant) -> None: - """Test the compressor min temp value is correct. - - Ecobee runs in Fahrenheit; the test rig runs in Celsius. Conversions are necessary. - """ - await setup_platform(hass, NUMBER_DOMAIN) - - state = hass.states.get(COMPRESSOR_MIN_TEMP_ID) - assert state.state == "-12.2" - assert ( - state.attributes.get("friendly_name") - == "ecobee2 Compressor minimum temperature" - ) - - -async def test_set_compressor_protection_min_temp(hass: HomeAssistant) -> None: - """Test the number can set minimum compressor operating temp. - - Ecobee runs in Fahrenheit; the test rig runs in Celsius. Conversions are necessary - """ - target_value = 0 - with patch( - "homeassistant.components.ecobee.Ecobee.set_aux_cutover_threshold" - ) as mock_set_compressor_min_temp: - await setup_platform(hass, NUMBER_DOMAIN) - - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: COMPRESSOR_MIN_TEMP_ID, ATTR_VALUE: target_value}, - blocking=True, - ) - await hass.async_block_till_done() - mock_set_compressor_min_temp.assert_called_once_with(1, 32) diff --git a/tests/components/ecobee/test_switch.py b/tests/components/ecobee/test_switch.py index b3c4c4f8296..31c8ce8f72d 100644 --- a/tests/components/ecobee/test_switch.py +++ b/tests/components/ecobee/test_switch.py @@ -118,7 +118,7 @@ async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None: mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False) -DEVICE_ID = "switch.ecobee2_auxiliary_heat_only" +DEVICE_ID = "switch.ecobee2_aux_heat_only" async def test_aux_heat_only_turn_on(hass: HomeAssistant) -> None: diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index e7cb85a9cfc..33e4739a488 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -12,46 +12,222 @@ from homeassistant.components.file import DOMAIN from homeassistant.components.notify import ATTR_TITLE_DEFAULT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, assert_setup_component + + +async def test_bad_config(hass: HomeAssistant) -> None: + """Test set up the platform with bad/missing config.""" + config = {notify.DOMAIN: {"name": "test", "platform": "file"}} + with assert_setup_component(0, domain="notify") as handle_config: + assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert not handle_config[notify.DOMAIN] @pytest.mark.parametrize( ("domain", "service", "params"), [ + (notify.DOMAIN, "test", {"message": "one, two, testing, testing"}), ( notify.DOMAIN, "send_message", {"entity_id": "notify.test", "message": "one, two, testing, testing"}, ), ], + ids=["legacy", "entity"], +) +@pytest.mark.parametrize( + ("timestamp", "config"), + [ + ( + False, + { + "notify": [ + { + "name": "test", + "platform": "file", + "filename": "mock_file", + "timestamp": False, + } + ] + }, + ), + ( + True, + { + "notify": [ + { + "name": "test", + "platform": "file", + "filename": "mock_file", + "timestamp": True, + } + ] + }, + ), + ], + ids=["no_timestamp", "timestamp"], ) -@pytest.mark.parametrize("timestamp", [False, True], ids=["no_timestamp", "timestamp"]) async def test_notify_file( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_is_allowed_path: MagicMock, timestamp: bool, + mock_is_allowed_path: MagicMock, + config: ConfigType, domain: str, service: str, params: dict[str, str], ) -> None: """Test the notify file output.""" filename = "mock_file" - full_filename = os.path.join(hass.config.path(), filename) + message = params["message"] + assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + freezer.move_to(dt_util.utcnow()) + + m_open = mock_open() + with ( + patch("homeassistant.components.file.notify.open", m_open, create=True), + patch("homeassistant.components.file.notify.os.stat") as mock_st, + ): + mock_st.return_value.st_size = 0 + title = ( + f"{ATTR_TITLE_DEFAULT} notifications " + f"(Log started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n" + ) + + await hass.services.async_call(domain, service, params, blocking=True) + + full_filename = os.path.join(hass.config.path(), filename) + assert m_open.call_count == 1 + assert m_open.call_args == call(full_filename, "a", encoding="utf8") + + assert m_open.return_value.write.call_count == 2 + if not timestamp: + assert m_open.return_value.write.call_args_list == [ + call(title), + call(f"{message}\n"), + ] + else: + assert m_open.return_value.write.call_args_list == [ + call(title), + call(f"{dt_util.utcnow().isoformat()} {message}\n"), + ] + + +@pytest.mark.parametrize( + ("domain", "service", "params"), + [(notify.DOMAIN, "test", {"message": "one, two, testing, testing"})], + ids=["legacy"], +) +@pytest.mark.parametrize( + ("is_allowed", "config"), + [ + ( + True, + { + "notify": [ + { + "name": "test", + "platform": "file", + "filename": "mock_file", + } + ] + }, + ), + ], + ids=["allowed_but_access_failed"], +) +async def test_legacy_notify_file_exception( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_is_allowed_path: MagicMock, + config: ConfigType, + domain: str, + service: str, + params: dict[str, str], +) -> None: + """Test legacy notify file output has exception.""" + assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + + freezer.move_to(dt_util.utcnow()) + + m_open = mock_open() + with ( + patch("homeassistant.components.file.notify.open", m_open, create=True), + patch("homeassistant.components.file.notify.os.stat") as mock_st, + ): + mock_st.side_effect = OSError("Access Failed") + with pytest.raises(ServiceValidationError) as exc: + await hass.services.async_call(domain, service, params, blocking=True) + assert f"{exc.value!r}" == "ServiceValidationError('write_access_failed')" + + +@pytest.mark.parametrize( + ("timestamp", "data", "options"), + [ + ( + False, + { + "name": "test", + "platform": "notify", + "file_path": "mock_file", + }, + { + "timestamp": False, + }, + ), + ( + True, + { + "name": "test", + "platform": "notify", + "file_path": "mock_file", + }, + { + "timestamp": True, + }, + ), + ], + ids=["no_timestamp", "timestamp"], +) +async def test_legacy_notify_file_entry_only_setup( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + timestamp: bool, + mock_is_allowed_path: MagicMock, + data: dict[str, Any], + options: dict[str, Any], +) -> None: + """Test the legacy notify file output in entry only setup.""" + filename = "mock_file" + + domain = notify.DOMAIN + service = "test" + params = {"message": "one, two, testing, testing"} message = params["message"] entry = MockConfigEntry( domain=DOMAIN, - data={"name": "test", "platform": "notify", "file_path": full_filename}, - options={"timestamp": timestamp}, + data=data, version=2, - title=f"test [{filename}]", + options=options, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) freezer.move_to(dt_util.utcnow()) @@ -69,7 +245,7 @@ async def test_notify_file( await hass.services.async_call(domain, service, params, blocking=True) assert m_open.call_count == 1 - assert m_open.call_args == call(full_filename, "a", encoding="utf8") + assert m_open.call_args == call(filename, "a", encoding="utf8") assert m_open.return_value.write.call_count == 2 if not timestamp: @@ -101,14 +277,14 @@ async def test_notify_file( ], ids=["not_allowed"], ) -async def test_notify_file_not_allowed( +async def test_legacy_notify_file_not_allowed( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_is_allowed_path: MagicMock, config: dict[str, Any], options: dict[str, Any], ) -> None: - """Test notify file output not allowed.""" + """Test legacy notify file output not allowed.""" entry = MockConfigEntry( domain=DOMAIN, data=config, @@ -125,10 +301,11 @@ async def test_notify_file_not_allowed( @pytest.mark.parametrize( ("service", "params"), [ + ("test", {"message": "one, two, testing, testing"}), ( "send_message", {"entity_id": "notify.test", "message": "one, two, testing, testing"}, - ) + ), ], ) @pytest.mark.parametrize( diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 9e6a16e3e27..634ae9d626c 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -7,10 +7,33 @@ import pytest from homeassistant.components.file import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, get_fixture_path +@patch("os.path.isfile", Mock(return_value=True)) +@patch("os.access", Mock(return_value=True)) +async def test_file_value_yaml_setup( + hass: HomeAssistant, mock_is_allowed_path: MagicMock +) -> None: + """Test the File sensor from YAML setup.""" + config = { + "sensor": { + "platform": "file", + "scan_interval": 30, + "name": "file1", + "file_path": get_fixture_path("file_value.txt", "file"), + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.file1") + assert state.state == "21" + + @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) async def test_file_value_entry_setup( 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/geniushub/test_config_flow.py b/tests/components/geniushub/test_config_flow.py index 7d1d33a2245..9234e03e35a 100644 --- a/tests/components/geniushub/test_config_flow.py +++ b/tests/components/geniushub/test_config_flow.py @@ -2,14 +2,21 @@ from http import HTTPStatus import socket +from typing import Any from unittest.mock import AsyncMock from aiohttp import ClientConnectionError, ClientResponseError import pytest from homeassistant.components.geniushub import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -302,3 +309,174 @@ async def test_cloud_duplicate( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +async def test_import_local_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], +) -> None: + """Test full local import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "10.0.0.130" + assert result["data"] == data + assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_TOKEN: "abcdef", + }, + { + CONF_TOKEN: "abcdef", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +async def test_import_cloud_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], +) -> None: + """Test full cloud import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Genius hub" + assert result["data"] == data + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + { + CONF_TOKEN: "abcdef", + }, + { + CONF_TOKEN: "abcdef", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (socket.gaierror, "invalid_host"), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.UNAUTHORIZED), + "invalid_auth", + ), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.NOT_FOUND), + "invalid_host", + ), + (TimeoutError, "cannot_connect"), + (ClientConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_import_flow_exceptions( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], + exception: Exception, + reason: str, +) -> None: + """Test import flow exceptions.""" + mock_geniushub_client.request.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.131", + CONF_USERNAME: "test-username1", + CONF_PASSWORD: "test-password", + }, + ], +) +async def test_import_flow_local_duplicate( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_local_config_entry: MockConfigEntry, + data: dict[str, Any], +) -> None: + """Test import flow aborts on local duplicate data.""" + mock_local_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow_cloud_duplicate( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_cloud_config_entry: MockConfigEntry, +) -> None: + """Test import flow aborts on cloud duplicate data.""" + mock_cloud_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" 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..ec586776142 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -3,10 +3,9 @@ from collections.abc import Callable, Generator import logging from typing import NamedTuple -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, call, 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 @@ -243,11 +238,7 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, - [ - "rtsp://stream", - f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", - ], + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] ) # Stream exists but the source is different @@ -261,11 +252,7 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, - [ - "rtsp://stream", - f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", - ], + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] ) # If the stream is already added, the stream should not be added again. @@ -309,9 +296,8 @@ async def _test_setup_and_signaling( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_go_binary( +async def test_setup_managed( hass: HomeAssistant, - issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: AsyncMock, @@ -322,21 +308,131 @@ async def test_setup_go_binary( config: ConfigType, ui_enabled: bool, ) -> None: - """Test the go2rtc config entry with binary.""" + """Test the go2rtc setup with managed go2rtc instance.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry + camera = init_test_integration - def after_setup() -> None: - server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) - server_start.assert_called_once() + entity_id = camera.entity_id + stream_name_original = f"{camera.entity_id}_original" + assert camera.frontend_stream_type == StreamType.HLS - await _test_setup_and_signaling( - hass, - issue_registry, - rest_client, - ws_client, - config, - after_setup, - init_test_integration, + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == ConfigEntryState.LOADED + server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) + server_start.assert_called_once() + + receive_message_callback = Mock(spec_set=WebRTCSendMessage) + + async def test() -> None: + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + ws_client.send.assert_called_once_with( + WebRTCOffer( + OFFER_SDP, + camera.async_get_webrtc_client_configuration().configuration.ice_servers, + ) + ) + ws_client.subscribe.assert_called_once() + + # Simulate the answer from the go2rtc server + callback = ws_client.subscribe.call_args[0][0] + callback(WebRTCAnswer(ANSWER_SDP)) + receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) + + await test() + + stream_added_calls = [ + call(stream_name_original, "rtsp://stream"), + call( + entity_id, + [ + f"rtsp://127.0.0.1:18554/{stream_name_original}", + f"ffmpeg:{stream_name_original}#audio=opus", + ], + ), + ] + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream original missing + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), + ] + ) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream original source different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_original: Stream([Producer("rtsp://different")]), + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), + ] + ), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream source different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_original: Stream([Producer("rtsp://stream")]), + entity_id: Stream([Producer("rtsp://different")]), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # If the stream is already added, the stream should not be added again. + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_original: Stream([Producer("rtsp://stream")]), + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), + ] + ), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_not_called() + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + # Set stream source to None and provider should be skipped + rest_client.streams.list.return_value = {} + receive_message_callback.reset_mock() + camera.set_stream_source(None) + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + receive_message_callback.assert_called_once_with( + WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") ) await hass.async_stop() @@ -352,9 +448,8 @@ async def test_setup_go_binary( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup( +async def test_setup_self_hosted( hass: HomeAssistant, - issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: Mock, @@ -363,22 +458,83 @@ async def test_setup( mock_is_docker_env: Mock, has_go2rtc_entry: bool, ) -> None: - """Test the go2rtc config entry without binary.""" + """Test the go2rtc with selfhosted go2rtc instance.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} + camera = init_test_integration - def after_setup() -> None: - server.assert_not_called() + entity_id = camera.entity_id + assert camera.frontend_stream_type == StreamType.HLS - await _test_setup_and_signaling( - hass, - issue_registry, - rest_client, - ws_client, - config, - after_setup, - init_test_integration, + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == ConfigEntryState.LOADED + server.assert_not_called() + + receive_message_callback = Mock(spec_set=WebRTCSendMessage) + + async def test() -> None: + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + ws_client.send.assert_called_once_with( + WebRTCOffer( + OFFER_SDP, + camera.async_get_webrtc_client_configuration().configuration.ice_servers, + ) + ) + ws_client.subscribe.assert_called_once() + + # Simulate the answer from the go2rtc server + callback = ws_client.subscribe.call_args[0][0] + callback(WebRTCAnswer(ANSWER_SDP)) + receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) + + await test() + + rest_client.streams.add.assert_called_once_with( + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + ) + + # Stream exists but the source is different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream([Producer("rtsp://different")]) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_called_once_with( + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + ) + + # If the stream is already added, the stream should not be added again. + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream([Producer("rtsp://stream")]) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_not_called() + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + # Set stream source to None and provider should be skipped + rest_client.streams.list.return_value = {} + receive_message_callback.reset_mock() + camera.set_stream_source(None) + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + receive_message_callback.assert_called_once_with( + WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") ) mock_get_binary.assert_not_called() @@ -730,30 +886,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/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 1e42edf8e7b..f5dedc357c1 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -4069,90 +4069,3 @@ async def test_sensorstate( ) is False ) - - -@pytest.mark.parametrize( - ("state", "identifier"), - [ - (STATE_ON, 0), - (STATE_OFF, 1), - (STATE_UNKNOWN, 2), - ], -) -@pytest.mark.parametrize( - ("device_class", "name", "states"), - [ - ( - binary_sensor.BinarySensorDeviceClass.CO, - "CarbonMonoxideLevel", - ["carbon monoxide detected", "no carbon monoxide detected", "unknown"], - ), - ( - binary_sensor.BinarySensorDeviceClass.SMOKE, - "SmokeLevel", - ["smoke detected", "no smoke detected", "unknown"], - ), - ( - binary_sensor.BinarySensorDeviceClass.MOISTURE, - "WaterLeak", - ["leak", "no leak", "unknown"], - ), - ], -) -async def test_binary_sensorstate( - hass: HomeAssistant, - state: str, - identifier: int, - device_class: binary_sensor.BinarySensorDeviceClass, - name: str, - states: list[str], -) -> None: - """Test SensorState trait support for binary sensor domain.""" - - assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None - assert trait.SensorStateTrait.supported( - binary_sensor.DOMAIN, None, device_class, None - ) - - trt = trait.SensorStateTrait( - hass, - State( - "binary_sensor.test", - state, - { - "device_class": device_class, - }, - ), - BASIC_CONFIG, - ) - - assert trt.sync_attributes() == { - "sensorStatesSupported": [ - { - "name": name, - "descriptiveCapabilities": { - "availableStates": states, - }, - } - ] - } - assert trt.query_attributes() == { - "currentSensorStateData": [ - { - "name": name, - "currentSensorState": states[identifier], - "rawValue": None, - }, - ] - } - - assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None - assert ( - trait.SensorStateTrait.supported( - binary_sensor.DOMAIN, - None, - binary_sensor.BinarySensorDeviceClass.TAMPER, - None, - ) - is False - ) diff --git a/tests/components/habitica/conftest.py b/tests/components/habitica/conftest.py index 8d729f4358f..b5ceadd2762 100644 --- a/tests/components/habitica/conftest.py +++ b/tests/components/habitica/conftest.py @@ -34,7 +34,7 @@ def mock_called_with( ( call for call in mock_client.mock_calls - if call[0].upper() == method.upper() and call[1] == URL(url) + if call[0] == method.upper() and call[1] == URL(url) ), None, ) @@ -56,11 +56,6 @@ def mock_habitica(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: f"{DEFAULT_URL}/api/v3/tasks/user", json=load_json_object_fixture("tasks.json", DOMAIN), ) - aioclient_mock.get( - f"{DEFAULT_URL}/api/v3/content", - params={"language": "en"}, - json=load_json_object_fixture("content.json", DOMAIN), - ) return aioclient_mock diff --git a/tests/components/habitica/fixtures/common_buttons_unavailable.json b/tests/components/habitica/fixtures/common_buttons_unavailable.json index efee5364e02..08039ae1762 100644 --- a/tests/components/habitica/fixtures/common_buttons_unavailable.json +++ b/tests/components/habitica/fixtures/common_buttons_unavailable.json @@ -29,26 +29,11 @@ "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_warrior_5", - "armor": "armor_warrior_5", - "head": "head_warrior_5", - "shield": "shield_warrior_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/content.json b/tests/components/habitica/fixtures/content.json deleted file mode 100644 index e8e14dead73..00000000000 --- a/tests/components/habitica/fixtures/content.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "success": true, - "data": { - "gear": { - "flat": { - "weapon_warrior_5": { - "text": "Ruby Sword", - "notes": "Weapon whose forge-glow never fades. Increases Strength by 15. ", - "str": 15, - "value": 90, - "type": "weapon", - "key": "weapon_warrior_5", - "set": "warrior-5", - "klass": "warrior", - "index": "5", - "int": 0, - "per": 0, - "con": 0 - }, - "armor_warrior_5": { - "text": "Golden Armor", - "notes": "Looks ceremonial, but no known blade can pierce it. Increases Constitution by 11.", - "con": 11, - "value": 120, - "last": true, - "type": "armor", - "key": "armor_warrior_5", - "set": "warrior-5", - "klass": "warrior", - "index": "5", - "str": 0, - "int": 0, - "per": 0 - }, - "head_warrior_5": { - "text": "Golden Helm", - "notes": "Regal crown bound to shining armor. Increases Strength by 12.", - "str": 12, - "value": 80, - "last": true, - "type": "head", - "key": "head_warrior_5", - "set": "warrior-5", - "klass": "warrior", - "index": "5", - "int": 0, - "per": 0, - "con": 0 - }, - "shield_warrior_5": { - "text": "Golden Shield", - "notes": "Shining badge of the vanguard. Increases Constitution by 9.", - "con": 9, - "value": 90, - "last": true, - "type": "shield", - "key": "shield_warrior_5", - "set": "warrior-5", - "klass": "warrior", - "index": "5", - "str": 0, - "int": 0, - "per": 0 - }, - "weapon_wizard_5": { - "twoHanded": true, - "text": "Archmage Staff", - "notes": "Assists in weaving the most complex of spells. Increases Intelligence by 15 and Perception by 7. Two-handed item.", - "int": 15, - "per": 7, - "value": 160, - "type": "weapon", - "key": "weapon_wizard_5", - "set": "wizard-5", - "klass": "wizard", - "index": "5", - "str": 0, - "con": 0 - }, - "armor_wizard_5": { - "text": "Royal Magus Robe", - "notes": "Symbol of the power behind the throne. Increases Intelligence by 12.", - "int": 12, - "value": 120, - "last": true, - "type": "armor", - "key": "armor_wizard_5", - "set": "wizard-5", - "klass": "wizard", - "index": "5", - "str": 0, - "per": 0, - "con": 0 - }, - "head_wizard_5": { - "text": "Royal Magus Hat", - "notes": "Shows authority over fortune, weather, and lesser mages. Increases Perception by 10.", - "per": 10, - "value": 80, - "last": true, - "type": "head", - "key": "head_wizard_5", - "set": "wizard-5", - "klass": "wizard", - "index": "5", - "str": 0, - "int": 0, - "con": 0 - }, - "weapon_healer_5": { - "text": "Royal Scepter", - "notes": "Fit to grace the hand of a monarch, or of one who stands at a monarch's right hand. Increases Intelligence by 9. ", - "int": 9, - "value": 90, - "type": "weapon", - "key": "weapon_healer_5", - "set": "healer-5", - "klass": "healer", - "index": "5", - "str": 0, - "per": 0, - "con": 0 - }, - "armor_healer_5": { - "text": "Royal Mantle", - "notes": "Attire of those who have saved the lives of kings. Increases Constitution by 18.", - "con": 18, - "value": 120, - "last": true, - "type": "armor", - "key": "armor_healer_5", - "set": "healer-5", - "klass": "healer", - "index": "5", - "str": 0, - "int": 0, - "per": 0 - }, - "head_healer_5": { - "text": "Royal Diadem", - "notes": "For king, queen, or miracle-worker. Increases Intelligence by 9.", - "int": 9, - "value": 80, - "last": true, - "type": "head", - "key": "head_healer_5", - "set": "healer-5", - "klass": "healer", - "index": "5", - "str": 0, - "per": 0, - "con": 0 - }, - "shield_healer_5": { - "text": "Royal Shield", - "notes": "Bestowed upon those most dedicated to the kingdom's defense. Increases Constitution by 12.", - "con": 12, - "value": 90, - "last": true, - "type": "shield", - "key": "shield_healer_5", - "set": "healer-5", - "klass": "healer", - "index": "5", - "str": 0, - "int": 0, - "per": 0 - }, - "weapon_rogue_5": { - "text": "Ninja-to", - "notes": "Sleek and deadly as the ninja themselves. Increases Strength by 8. ", - "str": 8, - "value": 90, - "type": "weapon", - "key": "weapon_rogue_5", - "set": "rogue-5", - "klass": "rogue", - "index": "5", - "int": 0, - "per": 0, - "con": 0 - }, - "armor_rogue_5": { - "text": "Umbral Armor", - "notes": "Allows stealth in the open in broad daylight. Increases Perception by 18.", - "per": 18, - "value": 120, - "last": true, - "type": "armor", - "key": "armor_rogue_5", - "set": "rogue-5", - "klass": "rogue", - "index": "5", - "str": 0, - "int": 0, - "con": 0 - }, - "head_rogue_5": { - "text": "Umbral Hood", - "notes": "Conceals even thoughts from those who would probe them. Increases Perception by 12.", - "per": 12, - "value": 80, - "last": true, - "type": "head", - "key": "head_rogue_5", - "set": "rogue-5", - "klass": "rogue", - "index": "5", - "str": 0, - "int": 0, - "con": 0 - }, - "shield_rogue_5": { - "text": "Ninja-to", - "notes": "Sleek and deadly as the ninja themselves. Increases Strength by 8. ", - "str": 8, - "value": 90, - "type": "shield", - "key": "shield_rogue_5", - "set": "rogue-5", - "klass": "rogue", - "index": "5", - "int": 0, - "per": 0, - "con": 0 - }, - "back_special_heroicAureole": { - "text": "Heroic Aureole", - "notes": "The gems on this aureole glimmer when you tell your tales of glory. Increases all stats by 7.", - "con": 7, - "str": 7, - "per": 7, - "int": 7, - "value": 175, - "type": "back", - "key": "back_special_heroicAureole", - "set": "special-heroicAureole", - "klass": "special", - "index": "heroicAureole" - }, - "headAccessory_armoire_gogglesOfBookbinding": { - "per": 8, - "set": "bookbinder", - "notes": "These goggles will help you zero in on any task, large or small! Increases Perception by 8. Enchanted Armoire: Bookbinder Set (Item 1 of 4).", - "text": "Goggles of Bookbinding", - "value": 100, - "type": "headAccessory", - "key": "headAccessory_armoire_gogglesOfBookbinding", - "klass": "armoire", - "index": "gogglesOfBookbinding", - "str": 0, - "int": 0, - "con": 0 - }, - "eyewear_armoire_plagueDoctorMask": { - "con": 5, - "int": 5, - "set": "plagueDoctor", - "notes": "An authentic mask worn by the doctors who battle the Plague of Procrastination. Increases Constitution and Intelligence by 5 each. Enchanted Armoire: Plague Doctor Set (Item 2 of 3).", - "text": "Plague Doctor Mask", - "value": 100, - "type": "eyewear", - "key": "eyewear_armoire_plagueDoctorMask", - "klass": "armoire", - "index": "plagueDoctorMask", - "str": 0, - "per": 0 - }, - "body_special_aetherAmulet": { - "text": "Aether Amulet", - "notes": "This amulet has a mysterious history. Increases Constitution and Strength by 10 each.", - "value": 175, - "str": 10, - "con": 10, - "type": "body", - "key": "body_special_aetherAmulet", - "set": "special-aetherAmulet", - "klass": "special", - "index": "aetherAmulet", - "int": 0, - "per": 0 - } - } - } - }, - "appVersion": "5.29.2" -} diff --git a/tests/components/habitica/fixtures/healer_fixture.json b/tests/components/habitica/fixtures/healer_fixture.json index 85f719f4ca7..04cbabcfa2d 100644 --- a/tests/components/habitica/fixtures/healer_fixture.json +++ b/tests/components/habitica/fixtures/healer_fixture.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,36 +24,17 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 5, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 5 }, "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, "needsCron": true, - "lastCron": "2024-09-21T22:01:55.586Z", - "items": { - "gear": { - "equipped": { - "weapon": "weapon_healer_5", - "armor": "armor_healer_5", - "head": "head_healer_5", - "shield": "shield_healer_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "lastCron": "2024-09-21T22:01:55.586Z" } } diff --git a/tests/components/habitica/fixtures/healer_skills_unavailable.json b/tests/components/habitica/fixtures/healer_skills_unavailable.json index a6bff246b2a..305a5f8cda1 100644 --- a/tests/components/habitica/fixtures/healer_skills_unavailable.json +++ b/tests/components/habitica/fixtures/healer_skills_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_healer_5", - "armor": "armor_healer_5", - "head": "head_healer_5", - "shield": "shield_healer_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/quest_invitation_off.json b/tests/components/habitica/fixtures/quest_invitation_off.json index b5eccd99e10..f862a85c7c4 100644 --- a/tests/components/habitica/fixtures/quest_invitation_off.json +++ b/tests/components/habitica/fixtures/quest_invitation_off.json @@ -29,8 +29,7 @@ "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true diff --git a/tests/components/habitica/fixtures/rogue_fixture.json b/tests/components/habitica/fixtures/rogue_fixture.json index 1e5e996c034..f0ea42a7182 100644 --- a/tests/components/habitica/fixtures/rogue_fixture.json +++ b/tests/components/habitica/fixtures/rogue_fixture.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,36 +24,17 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 5, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 5 }, "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, "needsCron": true, - "lastCron": "2024-09-21T22:01:55.586Z", - "items": { - "gear": { - "equipped": { - "weapon": "weapon_rogue_5", - "armor": "armor_rogue_5", - "head": "head_rogue_5", - "shield": "shield_rogue_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "lastCron": "2024-09-21T22:01:55.586Z" } } diff --git a/tests/components/habitica/fixtures/rogue_skills_unavailable.json b/tests/components/habitica/fixtures/rogue_skills_unavailable.json index c7c5ff32245..2709731ba55 100644 --- a/tests/components/habitica/fixtures/rogue_skills_unavailable.json +++ b/tests/components/habitica/fixtures/rogue_skills_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": true, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_rogue_5", - "armor": "armor_rogue_5", - "head": "head_rogue_5", - "shield": "shield_rogue_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/rogue_stealth_unavailable.json b/tests/components/habitica/fixtures/rogue_stealth_unavailable.json index 9fd7adcca42..a4e86abbb91 100644 --- a/tests/components/habitica/fixtures/rogue_stealth_unavailable.json +++ b/tests/components/habitica/fixtures/rogue_stealth_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 4, "streaks": false, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_rogue_5", - "armor": "armor_rogue_5", - "head": "head_rogue_5", - "shield": "shield_rogue_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/tasks.json b/tests/components/habitica/fixtures/tasks.json index 2e8305283d0..0d6ffba0732 100644 --- a/tests/components/habitica/fixtures/tasks.json +++ b/tests/components/habitica/fixtures/tasks.json @@ -121,8 +121,7 @@ "createdAt": "2024-07-07T17:51:53.264Z", "updatedAt": "2024-07-12T09:58:45.438Z", "userId": "5f359083-ef78-4af0-985a-0b2c6d05797c", - "id": "e97659e0-2c42-4599-a7bb-00282adc410d", - "alias": "create_a_task" + "id": "e97659e0-2c42-4599-a7bb-00282adc410d" }, { "_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa", @@ -455,8 +454,7 @@ "createdAt": "2024-09-21T22:17:19.513Z", "updatedAt": "2024-09-21T22:19:35.576Z", "userId": "5f359083-ef78-4af0-985a-0b2c6d05797c", - "id": "2f6fcabc-f670-4ec3-ba65-817e8deea490", - "alias": "pay_bills" + "id": "2f6fcabc-f670-4ec3-ba65-817e8deea490" }, { "_id": "1aa3137e-ef72-4d1f-91ee-41933602f438", diff --git a/tests/components/habitica/fixtures/user.json b/tests/components/habitica/fixtures/user.json index 569c5b81a02..818f4ed4eda 100644 --- a/tests/components/habitica/fixtures/user.json +++ b/tests/components/habitica/fixtures/user.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,17 +24,12 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 5, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 5 }, "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true @@ -64,20 +59,6 @@ } }, "needsCron": true, - "lastCron": "2024-09-21T22:01:55.586Z", - "items": { - "gear": { - "equipped": { - "weapon": "weapon_warrior_5", - "armor": "armor_warrior_5", - "head": "head_warrior_5", - "shield": "shield_warrior_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "lastCron": "2024-09-21T22:01:55.586Z" } } diff --git a/tests/components/habitica/fixtures/warrior_fixture.json b/tests/components/habitica/fixtures/warrior_fixture.json index 3517e8a908a..53d18206f9a 100644 --- a/tests/components/habitica/fixtures/warrior_fixture.json +++ b/tests/components/habitica/fixtures/warrior_fixture.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,36 +24,17 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 5, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 5 }, "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, "needsCron": true, - "lastCron": "2024-09-21T22:01:55.586Z", - "items": { - "gear": { - "equipped": { - "weapon": "weapon_warrior_5", - "armor": "armor_warrior_5", - "head": "head_warrior_5", - "shield": "shield_warrior_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "lastCron": "2024-09-21T22:01:55.586Z" } } diff --git a/tests/components/habitica/fixtures/warrior_skills_unavailable.json b/tests/components/habitica/fixtures/warrior_skills_unavailable.json index b3d33c85d5c..53160646569 100644 --- a/tests/components/habitica/fixtures/warrior_skills_unavailable.json +++ b/tests/components/habitica/fixtures/warrior_skills_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_warrior_5", - "armor": "armor_warrior_5", - "head": "head_warrior_5", - "shield": "shield_warrior_5", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/wizard_fixture.json b/tests/components/habitica/fixtures/wizard_fixture.json index de596e231de..0f9f2a49639 100644 --- a/tests/components/habitica/fixtures/wizard_fixture.json +++ b/tests/components/habitica/fixtures/wizard_fixture.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,36 +24,17 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 5, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 5 }, "preferences": { "sleep": false, "automaticAllocation": true, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, "needsCron": true, - "lastCron": "2024-09-21T22:01:55.586Z", - "items": { - "gear": { - "equipped": { - "weapon": "weapon_wizard_5", - "armor": "armor_wizard_5", - "head": "head_wizard_5", - "shield": "shield_base_0", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "lastCron": "2024-09-21T22:01:55.586Z" } } diff --git a/tests/components/habitica/fixtures/wizard_frost_unavailable.json b/tests/components/habitica/fixtures/wizard_frost_unavailable.json index 31d10fde4b9..ba57568e99e 100644 --- a/tests/components/habitica/fixtures/wizard_frost_unavailable.json +++ b/tests/components/habitica/fixtures/wizard_frost_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": true, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_wizard_5", - "armor": "armor_wizard_5", - "head": "head_wizard_5", - "shield": "shield_base_0", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/fixtures/wizard_skills_unavailable.json b/tests/components/habitica/fixtures/wizard_skills_unavailable.json index f3bdee9dd74..11bf0a19193 100644 --- a/tests/components/habitica/fixtures/wizard_skills_unavailable.json +++ b/tests/components/habitica/fixtures/wizard_skills_unavailable.json @@ -4,10 +4,10 @@ "profile": { "name": "test-user" }, "stats": { "buffs": { - "str": 26, - "int": 26, - "per": 26, - "con": 26, + "str": 0, + "int": 0, + "per": 0, + "con": 0, "stealth": 0, "streaks": false, "seafoam": false, @@ -24,35 +24,16 @@ "maxHealth": 50, "maxMP": 166, "toNextLevel": 880, - "points": 0, - "str": 15, - "con": 15, - "int": 15, - "per": 15 + "points": 0 }, "preferences": { "sleep": false, "automaticAllocation": false, - "disableClasses": false, - "language": "en" + "disableClasses": false }, "flags": { "classSelected": true }, - "needsCron": false, - "items": { - "gear": { - "equipped": { - "weapon": "weapon_wizard_5", - "armor": "armor_wizard_5", - "head": "head_wizard_5", - "shield": "shield_base_0", - "back": "heroicAureole", - "headAccessory": "headAccessory_armoire_gogglesOfBookbinding", - "eyewear": "plagueDoctorMask", - "body": "aetherAmulet" - } - } - } + "needsCron": false } } diff --git a/tests/components/habitica/snapshots/test_sensor.ambr b/tests/components/habitica/snapshots/test_sensor.ambr index 3a43069bfc4..ee75b424a93 100644 --- a/tests/components/habitica/snapshots/test_sensor.ambr +++ b/tests/components/habitica/snapshots/test_sensor.ambr @@ -59,61 +59,6 @@ 'state': 'wizard', }) # --- -# name: test_sensors[sensor.test_user_constitution-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_user_constitution', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Constitution', - 'platform': 'habitica', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': '00000000-0000-0000-0000-000000000000_constitution', - 'unit_of_measurement': 'CON', - }) -# --- -# name: test_sensors[sensor.test_user_constitution-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'allocated': 15, - 'buffs': 26, - 'class': 0, - 'equipment': 20, - 'friendly_name': 'test-user Constitution', - 'level': 19, - 'unit_of_measurement': 'CON', - }), - 'context': , - 'entity_id': 'sensor.test_user_constitution', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }) -# --- # name: test_sensors[sensor.test_user_dailies-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -622,61 +567,6 @@ 'state': '0', }) # --- -# name: test_sensors[sensor.test_user_intelligence-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_user_intelligence', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Intelligence', - 'platform': 'habitica', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': '00000000-0000-0000-0000-000000000000_intelligence', - 'unit_of_measurement': 'INT', - }) -# --- -# name: test_sensors[sensor.test_user_intelligence-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'allocated': 15, - 'buffs': 26, - 'class': 0, - 'equipment': 0, - 'friendly_name': 'test-user Intelligence', - 'level': 19, - 'unit_of_measurement': 'INT', - }), - 'context': , - 'entity_id': 'sensor.test_user_intelligence', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '60', - }) -# --- # name: test_sensors[sensor.test_user_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -964,61 +854,6 @@ 'state': '880', }) # --- -# name: test_sensors[sensor.test_user_perception-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_user_perception', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Perception', - 'platform': 'habitica', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': '00000000-0000-0000-0000-000000000000_perception', - 'unit_of_measurement': 'PER', - }) -# --- -# name: test_sensors[sensor.test_user_perception-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'allocated': 15, - 'buffs': 26, - 'class': 0, - 'equipment': 8, - 'friendly_name': 'test-user Perception', - 'level': 19, - 'unit_of_measurement': 'PER', - }), - 'context': , - 'entity_id': 'sensor.test_user_perception', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '68', - }) -# --- # name: test_sensors[sensor.test_user_rewards-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1080,61 +915,6 @@ 'state': '1', }) # --- -# name: test_sensors[sensor.test_user_strength-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_user_strength', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Strength', - 'platform': 'habitica', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': '00000000-0000-0000-0000-000000000000_strength', - 'unit_of_measurement': 'STR', - }) -# --- -# name: test_sensors[sensor.test_user_strength-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'allocated': 15, - 'buffs': 26, - 'class': 0, - 'equipment': 27, - 'friendly_name': 'test-user Strength', - 'level': 19, - 'unit_of_measurement': 'STR', - }), - 'context': , - 'entity_id': 'sensor.test_user_strength', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '87', - }) -# --- # name: test_sensors[sensor.test_user_to_do_s-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/habitica/test_binary_sensor.py b/tests/components/habitica/test_binary_sensor.py index 1710f8f217e..5b19cd008bf 100644 --- a/tests/components/habitica/test_binary_sensor.py +++ b/tests/components/habitica/test_binary_sensor.py @@ -66,11 +66,7 @@ async def test_pending_quest_states( json=load_json_object_fixture(f"{fixture}.json", DOMAIN), ) aioclient_mock.get(f"{DEFAULT_URL}/api/v3/tasks/user", json={"data": []}) - aioclient_mock.get( - f"{DEFAULT_URL}/api/v3/content", - params={"language": "en"}, - json=load_json_object_fixture("content.json", DOMAIN), - ) + config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/habitica/test_button.py b/tests/components/habitica/test_button.py index 979cefef923..6bd62f3a58e 100644 --- a/tests/components/habitica/test_button.py +++ b/tests/components/habitica/test_button.py @@ -63,11 +63,6 @@ async def test_buttons( f"{DEFAULT_URL}/api/v3/tasks/user", json=load_json_object_fixture("tasks.json", DOMAIN), ) - aioclient_mock.get( - f"{DEFAULT_URL}/api/v3/content", - params={"language": "en"}, - json=load_json_object_fixture("content.json", DOMAIN), - ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -168,11 +163,6 @@ async def test_button_press( f"{DEFAULT_URL}/api/v3/tasks/user", json=load_json_object_fixture("tasks.json", DOMAIN), ) - aioclient_mock.get( - f"{DEFAULT_URL}/api/v3/content", - params={"language": "en"}, - json=load_json_object_fixture("content.json", DOMAIN), - ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/habitica/test_services.py b/tests/components/habitica/test_services.py deleted file mode 100644 index 403779bcbfb..00000000000 --- a/tests/components/habitica/test_services.py +++ /dev/null @@ -1,548 +0,0 @@ -"""Test Habitica actions.""" - -from collections.abc import Generator -from http import HTTPStatus -from typing import Any -from unittest.mock import patch - -import pytest - -from homeassistant.components.habitica.const import ( - ATTR_CONFIG_ENTRY, - ATTR_DIRECTION, - ATTR_SKILL, - ATTR_TASK, - DEFAULT_URL, - DOMAIN, - SERVICE_ABORT_QUEST, - SERVICE_ACCEPT_QUEST, - SERVICE_CANCEL_QUEST, - SERVICE_CAST_SKILL, - SERVICE_LEAVE_QUEST, - SERVICE_REJECT_QUEST, - SERVICE_SCORE_HABIT, - SERVICE_SCORE_REWARD, - SERVICE_START_QUEST, -) -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError - -from .conftest import mock_called_with - -from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker - -REQUEST_EXCEPTION_MSG = "Unable to connect to Habitica, try again later" -RATE_LIMIT_EXCEPTION_MSG = "Rate limit exceeded, try again later" - - -@pytest.fixture(autouse=True) -def services_only() -> Generator[None]: - """Enable only services.""" - with patch( - "homeassistant.components.habitica.PLATFORMS", - [], - ): - yield - - -@pytest.fixture(autouse=True) -async def load_entry( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - services_only: Generator, -) -> None: - """Load config entry.""" - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - - -@pytest.mark.parametrize( - ("service_data", "item", "target_id"), - [ - ( - { - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "pickpocket", - }, - "pickPocket", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ( - { - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "backstab", - }, - "backStab", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ( - { - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "fireball", - }, - "fireball", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ( - { - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "smash", - }, - "smash", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ( - { - ATTR_TASK: "Rechnungen bezahlen", - ATTR_SKILL: "smash", - }, - "smash", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ( - { - ATTR_TASK: "pay_bills", - ATTR_SKILL: "smash", - }, - "smash", - "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ), - ], - ids=[ - "cast pickpocket", - "cast backstab", - "cast fireball", - "cast smash", - "select task by name", - "select task_by_alias", - ], -) -async def test_cast_skill( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - service_data: dict[str, Any], - item: str, - target_id: str, -) -> None: - """Test Habitica cast skill action.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}", - json={"success": True, "data": {}}, - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_CAST_SKILL, - service_data={ - ATTR_CONFIG_ENTRY: config_entry.entry_id, - **service_data, - }, - return_response=True, - blocking=True, - ) - - assert mock_called_with( - mock_habitica, - "post", - f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}", - ) - - -@pytest.mark.parametrize( - ( - "service_data", - "http_status", - "expected_exception", - "expected_exception_msg", - ), - [ - ( - { - ATTR_TASK: "task-not-found", - ATTR_SKILL: "smash", - }, - HTTPStatus.OK, - ServiceValidationError, - "Unable to complete action, could not find the task 'task-not-found'", - ), - ( - { - ATTR_TASK: "Rechnungen bezahlen", - ATTR_SKILL: "smash", - }, - HTTPStatus.TOO_MANY_REQUESTS, - ServiceValidationError, - RATE_LIMIT_EXCEPTION_MSG, - ), - ( - { - ATTR_TASK: "Rechnungen bezahlen", - ATTR_SKILL: "smash", - }, - HTTPStatus.NOT_FOUND, - ServiceValidationError, - "Unable to cast skill, your character does not have the skill or spell smash", - ), - ( - { - ATTR_TASK: "Rechnungen bezahlen", - ATTR_SKILL: "smash", - }, - HTTPStatus.UNAUTHORIZED, - ServiceValidationError, - "Unable to cast skill, not enough mana. Your character has 50 MP, but the skill costs 10 MP", - ), - ( - { - ATTR_TASK: "Rechnungen bezahlen", - ATTR_SKILL: "smash", - }, - HTTPStatus.BAD_REQUEST, - HomeAssistantError, - REQUEST_EXCEPTION_MSG, - ), - ], -) -@pytest.mark.usefixtures("mock_habitica") -async def test_cast_skill_exceptions( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - service_data: dict[str, Any], - http_status: HTTPStatus, - expected_exception: Exception, - expected_exception_msg: str, -) -> None: - """Test Habitica cast skill action exceptions.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/user/class/cast/smash?targetId=2f6fcabc-f670-4ec3-ba65-817e8deea490", - json={"success": True, "data": {}}, - status=http_status, - ) - - with pytest.raises(expected_exception, match=expected_exception_msg): - await hass.services.async_call( - DOMAIN, - SERVICE_CAST_SKILL, - service_data={ - ATTR_CONFIG_ENTRY: config_entry.entry_id, - **service_data, - }, - return_response=True, - blocking=True, - ) - - -@pytest.mark.usefixtures("mock_habitica") -async def test_get_config_entry( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, -) -> None: - """Test Habitica config entry exceptions.""" - - with pytest.raises( - ServiceValidationError, - match="The selected character is not configured in Home Assistant", - ): - await hass.services.async_call( - DOMAIN, - SERVICE_CAST_SKILL, - service_data={ - ATTR_CONFIG_ENTRY: "0000000000000000", - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "smash", - }, - return_response=True, - blocking=True, - ) - - assert await hass.config_entries.async_unload(config_entry.entry_id) - - with pytest.raises( - ServiceValidationError, - match="The selected character is currently not loaded or disabled in Home Assistant", - ): - await hass.services.async_call( - DOMAIN, - SERVICE_CAST_SKILL, - service_data={ - ATTR_CONFIG_ENTRY: config_entry.entry_id, - ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490", - ATTR_SKILL: "smash", - }, - return_response=True, - blocking=True, - ) - - -@pytest.mark.parametrize( - ("service", "command"), - [ - (SERVICE_ABORT_QUEST, "abort"), - (SERVICE_ACCEPT_QUEST, "accept"), - (SERVICE_CANCEL_QUEST, "cancel"), - (SERVICE_LEAVE_QUEST, "leave"), - (SERVICE_REJECT_QUEST, "reject"), - (SERVICE_START_QUEST, "force-start"), - ], - ids=[], -) -async def test_handle_quests( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - service: str, - command: str, -) -> None: - """Test Habitica actions for quest handling.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}", - json={"success": True, "data": {}}, - ) - - await hass.services.async_call( - DOMAIN, - service, - service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id}, - return_response=True, - blocking=True, - ) - - assert mock_called_with( - mock_habitica, - "post", - f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}", - ) - - -@pytest.mark.parametrize( - ( - "http_status", - "expected_exception", - "expected_exception_msg", - ), - [ - ( - HTTPStatus.TOO_MANY_REQUESTS, - ServiceValidationError, - RATE_LIMIT_EXCEPTION_MSG, - ), - ( - HTTPStatus.NOT_FOUND, - ServiceValidationError, - "Unable to complete action, quest or group not found", - ), - ( - HTTPStatus.UNAUTHORIZED, - ServiceValidationError, - "Action not allowed, only quest leader or group leader can perform this action", - ), - ( - HTTPStatus.BAD_REQUEST, - HomeAssistantError, - REQUEST_EXCEPTION_MSG, - ), - ], -) -@pytest.mark.usefixtures("mock_habitica") -async def test_handle_quests_exceptions( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - http_status: HTTPStatus, - expected_exception: Exception, - expected_exception_msg: str, -) -> None: - """Test Habitica handle quests action exceptions.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/groups/party/quests/accept", - json={"success": True, "data": {}}, - status=http_status, - ) - - with pytest.raises(expected_exception, match=expected_exception_msg): - await hass.services.async_call( - DOMAIN, - SERVICE_ACCEPT_QUEST, - service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id}, - return_response=True, - blocking=True, - ) - - -@pytest.mark.parametrize( - ("service", "service_data", "task_id"), - [ - ( - SERVICE_SCORE_HABIT, - { - ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d", - ATTR_DIRECTION: "up", - }, - "e97659e0-2c42-4599-a7bb-00282adc410d", - ), - ( - SERVICE_SCORE_HABIT, - { - ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d", - ATTR_DIRECTION: "down", - }, - "e97659e0-2c42-4599-a7bb-00282adc410d", - ), - ( - SERVICE_SCORE_REWARD, - { - ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b", - }, - "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b", - ), - ( - SERVICE_SCORE_HABIT, - { - ATTR_TASK: "Füge eine Aufgabe zu Habitica hinzu", - ATTR_DIRECTION: "up", - }, - "e97659e0-2c42-4599-a7bb-00282adc410d", - ), - ( - SERVICE_SCORE_HABIT, - { - ATTR_TASK: "create_a_task", - ATTR_DIRECTION: "up", - }, - "e97659e0-2c42-4599-a7bb-00282adc410d", - ), - ], - ids=[ - "habit score up", - "habit score down", - "buy reward", - "match task by name", - "match task by alias", - ], -) -async def test_score_task( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - service: str, - service_data: dict[str, Any], - task_id: str, -) -> None: - """Test Habitica score task action.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}", - json={"success": True, "data": {}}, - ) - - await hass.services.async_call( - DOMAIN, - service, - service_data={ - ATTR_CONFIG_ENTRY: config_entry.entry_id, - **service_data, - }, - return_response=True, - blocking=True, - ) - - assert mock_called_with( - mock_habitica, - "post", - f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}", - ) - - -@pytest.mark.parametrize( - ( - "service_data", - "http_status", - "expected_exception", - "expected_exception_msg", - ), - [ - ( - { - ATTR_TASK: "task does not exist", - ATTR_DIRECTION: "up", - }, - HTTPStatus.OK, - ServiceValidationError, - "Unable to complete action, could not find the task 'task does not exist'", - ), - ( - { - ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d", - ATTR_DIRECTION: "up", - }, - HTTPStatus.TOO_MANY_REQUESTS, - ServiceValidationError, - RATE_LIMIT_EXCEPTION_MSG, - ), - ( - { - ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d", - ATTR_DIRECTION: "up", - }, - HTTPStatus.BAD_REQUEST, - HomeAssistantError, - REQUEST_EXCEPTION_MSG, - ), - ( - { - ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b", - ATTR_DIRECTION: "up", - }, - HTTPStatus.UNAUTHORIZED, - HomeAssistantError, - "Unable to buy reward, not enough gold. Your character has 137.63 GP, but the reward costs 10 GP", - ), - ], -) -@pytest.mark.usefixtures("mock_habitica") -async def test_score_task_exceptions( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_habitica: AiohttpClientMocker, - service_data: dict[str, Any], - http_status: HTTPStatus, - expected_exception: Exception, - expected_exception_msg: str, -) -> None: - """Test Habitica score task action exceptions.""" - - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/tasks/e97659e0-2c42-4599-a7bb-00282adc410d/score/up", - json={"success": True, "data": {}}, - status=http_status, - ) - mock_habitica.post( - f"{DEFAULT_URL}/api/v3/tasks/5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b/score/up", - json={"success": True, "data": {}}, - status=http_status, - ) - - with pytest.raises(expected_exception, match=expected_exception_msg): - await hass.services.async_call( - DOMAIN, - SERVICE_SCORE_HABIT, - service_data={ - ATTR_CONFIG_ENTRY: config_entry.entry_id, - **service_data, - }, - return_response=True, - blocking=True, - ) diff --git a/tests/components/habitica/test_todo.py b/tests/components/habitica/test_todo.py index c9a4b3dd37a..88947caba2d 100644 --- a/tests/components/habitica/test_todo.py +++ b/tests/components/habitica/test_todo.py @@ -672,11 +672,6 @@ async def test_next_due_date( f"{DEFAULT_URL}/api/v3/tasks/user", json=load_json_object_fixture(fixture, DOMAIN), ) - aioclient_mock.get( - f"{DEFAULT_URL}/api/v3/content", - params={"language": "en"}, - json=load_json_object_fixture("content.json", DOMAIN), - ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 94b0e68e76d..8377d847a7a 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -10,12 +10,7 @@ from homeassistant.components.alarm_control_panel import ( ) from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import SecuritySystem -from homeassistant.const import ( - ATTR_CODE, - ATTR_ENTITY_ID, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service @@ -312,33 +307,3 @@ async def test_supported_states(hass: HomeAssistant, hk_driver) -> None: for val in valid_target_values.values(): assert val in test_config.get("target_values") - - -@pytest.mark.parametrize( - ("state"), - [ - (None), - ("None"), - (STATE_UNKNOWN), - (STATE_UNAVAILABLE), - ], -) -async def test_handle_non_alarm_states( - hass: HomeAssistant, hk_driver, events: list[Event], state: str -) -> None: - """Test we can handle states that should not raise.""" - code = "1234" - config = {ATTR_CODE: code} - entity_id = "alarm_control_panel.test" - - hass.states.async_set(entity_id, state) - await hass.async_block_till_done() - acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) - acc.run() - await hass.async_block_till_done() - - assert acc.aid == 2 - assert acc.category == 11 # AlarmSystem - - assert acc.char_current_state.value == 3 - assert acc.char_target_state.value == 3 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/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py index e00fe41749f..dbd4ecd802d 100644 --- a/tests/components/jewish_calendar/test_config_flow.py +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import AsyncMock +import pytest + from homeassistant import config_entries, setup from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, @@ -18,10 +20,12 @@ from homeassistant.const import ( CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, CONF_TIME_ZONE, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -55,6 +59,51 @@ async def test_step_user(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No assert entries[0].data[CONF_TIME_ZONE] == hass.config.time_zone +@pytest.mark.parametrize("diaspora", [True, False]) +@pytest.mark.parametrize("language", ["hebrew", "english"]) +async def test_import_no_options(hass: HomeAssistant, language, diaspora) -> None: + """Test that the import step works.""" + conf = { + DOMAIN: {CONF_NAME: "test", CONF_LANGUAGE: language, CONF_DIASPORA: diaspora} + } + + assert await async_setup_component(hass, DOMAIN, conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert CONF_LANGUAGE in entries[0].data + assert CONF_DIASPORA in entries[0].data + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == conf[DOMAIN][entry_key] + + +async def test_import_with_options(hass: HomeAssistant) -> None: + """Test that the import step works.""" + conf = { + DOMAIN: { + CONF_NAME: "test", + CONF_DIASPORA: DEFAULT_DIASPORA, + CONF_LANGUAGE: DEFAULT_LANGUAGE, + CONF_CANDLE_LIGHT_MINUTES: 20, + CONF_HAVDALAH_OFFSET_MINUTES: 50, + CONF_LATITUDE: 31.76, + CONF_LONGITUDE: 35.235, + } + } + + # Simulate HomeAssistant setting up the component + assert await async_setup_component(hass, DOMAIN, conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == conf[DOMAIN][entry_key] + for entry_key, entry_val in entries[0].options.items(): + assert entry_val == conf[DOMAIN][entry_key] + + async def test_single_instance_allowed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/jewish_calendar/test_init.py b/tests/components/jewish_calendar/test_init.py index cb982afec0f..b8454b41a60 100644 --- a/tests/components/jewish_calendar/test_init.py +++ b/tests/components/jewish_calendar/test_init.py @@ -1 +1,76 @@ """Tests for the Jewish Calendar component's init.""" + +from hdate import Location + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSORS +from homeassistant.components.jewish_calendar import get_unique_prefix +from homeassistant.components.jewish_calendar.const import ( + CONF_CANDLE_LIGHT_MINUTES, + CONF_DIASPORA, + CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_DIASPORA, + DEFAULT_LANGUAGE, + DOMAIN, +) +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er +from homeassistant.setup import async_setup_component + + +async def test_import_unique_id_migration(hass: HomeAssistant) -> None: + """Test unique_id migration.""" + yaml_conf = { + DOMAIN: { + CONF_NAME: "test", + CONF_DIASPORA: DEFAULT_DIASPORA, + CONF_LANGUAGE: DEFAULT_LANGUAGE, + CONF_CANDLE_LIGHT_MINUTES: 20, + CONF_HAVDALAH_OFFSET_MINUTES: 50, + CONF_LATITUDE: 31.76, + CONF_LONGITUDE: 35.235, + } + } + + # Create an entry in the entity registry with the data from conf + ent_reg = er.async_get(hass) + location = Location( + latitude=yaml_conf[DOMAIN][CONF_LATITUDE], + longitude=yaml_conf[DOMAIN][CONF_LONGITUDE], + timezone=hass.config.time_zone, + diaspora=DEFAULT_DIASPORA, + ) + old_prefix = get_unique_prefix(location, DEFAULT_LANGUAGE, 20, 50) + sample_entity = ent_reg.async_get_or_create( + BINARY_SENSORS, + DOMAIN, + unique_id=f"{old_prefix}_erev_shabbat_hag", + suggested_object_id=f"{DOMAIN}_erev_shabbat_hag", + ) + # Save the existing unique_id, DEFAULT_LANGUAGE should be part of it + old_unique_id = sample_entity.unique_id + assert DEFAULT_LANGUAGE in old_unique_id + + # Simulate HomeAssistant setting up the component + assert await async_setup_component(hass, DOMAIN, yaml_conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == yaml_conf[DOMAIN][entry_key] + for entry_key, entry_val in entries[0].options.items(): + assert entry_val == yaml_conf[DOMAIN][entry_key] + + # Assert that the unique_id was updated + new_unique_id = ent_reg.async_get(sample_entity.entity_id).unique_id + assert new_unique_id != old_unique_id + assert DEFAULT_LANGUAGE not in new_unique_id + + # Confirm that when the component is reloaded, the unique_id is not changed + assert ent_reg.async_get(sample_entity.entity_id).unique_id == new_unique_id + + # Confirm that all the unique_ids are prefixed correctly + await hass.config_entries.async_reload(entries[0].entry_id) + er_entries = er.async_entries_for_config_entry(ent_reg, entries[0].entry_id) + assert all(entry.unique_id.startswith(entries[0].entry_id) for entry in er_entries) diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index f6ca0fe40df..f88fa474f8b 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -1,6 +1,6 @@ """Mock inputs for tests.""" -from pylamarzocco.const import MachineModel +from lmcloud.const import MachineModel from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 210dd9406cc..d8047dfbabf 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -5,9 +5,9 @@ import json from unittest.mock import MagicMock, patch from bleak.backends.device import BLEDevice -from pylamarzocco.const import FirmwareType, MachineModel, SteamLevel -from pylamarzocco.lm_machine import LaMarzoccoMachine -from pylamarzocco.models import LaMarzoccoDeviceInfo +from lmcloud.const import FirmwareType, MachineModel, SteamLevel +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoDeviceInfo import pytest from homeassistant.components.lamarzocco.const import DOMAIN diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 956bfe90dd4..120d825c804 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.exceptions import RequestNotSuccessful from syrupy import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE diff --git a/tests/components/lamarzocco/test_button.py b/tests/components/lamarzocco/test_button.py index 61b7ba77c22..b754688f369 100644 --- a/tests/components/lamarzocco/test_button.py +++ b/tests/components/lamarzocco/test_button.py @@ -1,8 +1,8 @@ """Tests for the La Marzocco Buttons.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -33,18 +33,14 @@ async def test_start_backflush( assert entry assert entry == snapshot - with patch( - "homeassistant.components.lamarzocco.button.asyncio.sleep", - new_callable=AsyncMock, - ): - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - { - ATTR_ENTITY_ID: f"button.{serial_number}_start_backflush", - }, - blocking=True, - ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: f"button.{serial_number}_start_backflush", + }, + blocking=True, + ) assert len(mock_lamarzocco.start_backflush.mock_calls) == 1 mock_lamarzocco.start_backflush.assert_called_once() diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index be93779848f..13cf6a72b81 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -2,9 +2,9 @@ from unittest.mock import MagicMock, patch -from pylamarzocco.const import MachineModel -from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful -from pylamarzocco.models import LaMarzoccoDeviceInfo +from lmcloud.const import MachineModel +from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo import pytest from homeassistant.components.dhcp import DhcpServiceInfo diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index b99077a9059..2c812f79438 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock, MagicMock, patch -from pylamarzocco.const import FirmwareType -from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.const import FirmwareType +from lmcloud.exceptions import AuthFail, RequestNotSuccessful import pytest from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 710a0220e06..352271f26cf 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -3,14 +3,14 @@ from typing import Any from unittest.mock import MagicMock -from pylamarzocco.const import ( +from lmcloud.const import ( KEYS_PER_MODEL, BoilerType, MachineModel, PhysicalKey, PrebrewMode, ) -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 24b96f84f37..415954d30be 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock -from pylamarzocco.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index 6f14d52d1fc..760dcffd28f 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from pylamarzocco.const import MachineModel +from lmcloud.const import MachineModel import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index 5c6d1cb1e42..802ab59148e 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -3,7 +3,7 @@ from typing import Any from unittest.mock import MagicMock -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index aef37d7c921..3dc2a86b574 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock -from pylamarzocco.const import FirmwareType -from pylamarzocco.exceptions import RequestNotSuccessful +from lmcloud.const import FirmwareType +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index b7967c247ec..4ef83aeaf8a 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -23,7 +23,9 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import issue_registry as ir from tests.common import MockConfigEntry @@ -46,6 +48,83 @@ IMPORT_DATA = { } +async def test_step_import( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test for import step.""" + + with ( + patch("homeassistant.components.lcn.PchkConnectionManager.async_connect"), + patch("homeassistant.components.lcn.async_setup", return_value=True), + patch("homeassistant.components.lcn.async_setup_entry", return_value=True), + ): + data = IMPORT_DATA.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "pchk" + assert result["data"] == IMPORT_DATA + assert issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + + +async def test_step_import_existing_host( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test for update of config_entry if imported host already exists.""" + + # Create config entry and add it to hass + mock_data = IMPORT_DATA.copy() + mock_data.update({CONF_SK_NUM_TRIES: 3, CONF_DIM_MODE: 50}) + mock_entry = MockConfigEntry(domain=DOMAIN, data=mock_data) + mock_entry.add_to_hass(hass) + # Initialize a config flow with different data but same host address + with patch("homeassistant.components.lcn.PchkConnectionManager.async_connect"): + imported_data = IMPORT_DATA.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=imported_data + ) + + # Check if config entry was updated + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "existing_configuration_updated" + assert mock_entry.source == config_entries.SOURCE_IMPORT + assert mock_entry.data == IMPORT_DATA + assert issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (PchkAuthenticationError, "authentication_error"), + (PchkLicenseError, "license_error"), + (TimeoutError, "connection_refused"), + ], +) +async def test_step_import_error( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, error, reason +) -> None: + """Test for error in import is handled correctly.""" + with patch( + "homeassistant.components.lcn.PchkConnectionManager.async_connect", + side_effect=error, + ): + data = IMPORT_DATA.copy() + data.update({CONF_HOST: "pchk"}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + assert issue_registry.async_get_issue(DOMAIN, reason) + + async def test_show_form(hass: HomeAssistant) -> None: """Test that the form is served with no input.""" flow = LcnFlowHandler() @@ -61,6 +140,7 @@ async def test_step_user(hass: HomeAssistant) -> None: """Test for user step.""" with ( patch("homeassistant.components.lcn.PchkConnectionManager.async_connect"), + patch("homeassistant.components.lcn.async_setup", return_value=True), patch("homeassistant.components.lcn.async_setup_entry", return_value=True), ): data = CONNECTION_DATA.copy() @@ -130,6 +210,7 @@ async def test_step_reconfigure(hass: HomeAssistant, entry: MockConfigEntry) -> with ( patch("homeassistant.components.lcn.PchkConnectionManager.async_connect"), + patch("homeassistant.components.lcn.async_setup", return_value=True), patch("homeassistant.components.lcn.async_setup_entry", return_value=True), ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 2327635e356..1bd225c5d47 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -16,6 +16,7 @@ from .conftest import ( MockPchkConnectionManager, create_config_entry, init_integration, + setup_component, ) @@ -82,6 +83,18 @@ async def test_async_setup_entry_update( assert dummy_entity in entity_registry.entities.values() assert dummy_device in device_registry.devices.values() + # setup new entry with same data via import step (should cleanup dummy device) + with patch( + "homeassistant.components.lcn.config_flow.validate_connection", + return_value=None, + ): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=entry.data + ) + + assert dummy_device not in device_registry.devices.values() + assert dummy_entity not in entity_registry.entities.values() + @pytest.mark.parametrize( "exception", [PchkAuthenticationError, PchkLicenseError, TimeoutError] @@ -101,6 +114,20 @@ async def test_async_setup_entry_raises_authentication_error( assert entry.state is ConfigEntryState.SETUP_ERROR +async def test_async_setup_from_configuration_yaml(hass: HomeAssistant) -> None: + """Test a successful setup using data from configuration.yaml.""" + with ( + patch( + "homeassistant.components.lcn.config_flow.validate_connection", + return_value=None, + ), + patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry, + ): + await setup_component(hass) + + assert async_setup_entry.await_count == 2 + + @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: """Test migration config entry.""" diff --git a/tests/components/linkplay/__init__.py b/tests/components/linkplay/__init__.py index f825826f196..5962f7fdaba 100644 --- a/tests/components/linkplay/__init__.py +++ b/tests/components/linkplay/__init__.py @@ -1,16 +1 @@ """Tests for the LinkPlay integration.""" - -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration( - hass: HomeAssistant, - config_entry: MockConfigEntry, -) -> None: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/linkplay/conftest.py b/tests/components/linkplay/conftest.py index 81ae993f6c3..be83dd2412d 100644 --- a/tests/components/linkplay/conftest.py +++ b/tests/components/linkplay/conftest.py @@ -1,22 +1,12 @@ """Test configuration and mocks for LinkPlay component.""" -from collections.abc import Generator, Iterator -from contextlib import contextmanager -from typing import Any -from unittest import mock +from collections.abc import Generator from unittest.mock import AsyncMock, patch from aiohttp import ClientSession from linkplay.bridge import LinkPlayBridge, LinkPlayDevice import pytest -from homeassistant.components.linkplay.const import DOMAIN -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_CLOSE -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry, load_fixture -from tests.conftest import AiohttpClientMocker - HOST = "10.0.0.150" HOST_REENTRY = "10.0.0.66" UUID = "FF31F09E-5001-FBDE-0546-2DBFFF31F09E" @@ -34,15 +24,15 @@ def mock_linkplay_factory_bridge() -> Generator[AsyncMock]: ), patch( "homeassistant.components.linkplay.config_flow.linkplay_factory_httpapi_bridge", - ) as conf_factory, + ) as factory, ): bridge = AsyncMock(spec=LinkPlayBridge) bridge.endpoint = HOST bridge.device = AsyncMock(spec=LinkPlayDevice) bridge.device.uuid = UUID bridge.device.name = NAME - conf_factory.return_value = bridge - yield conf_factory + factory.return_value = bridge + yield factory @pytest.fixture @@ -53,55 +43,3 @@ def mock_setup_entry() -> Generator[AsyncMock]: return_value=True, ) as mock_setup_entry: yield mock_setup_entry - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Mock a config entry.""" - return MockConfigEntry( - domain=DOMAIN, - title=NAME, - data={CONF_HOST: HOST}, - unique_id=UUID, - ) - - -@pytest.fixture -def mock_player_ex( - mock_player_ex: AsyncMock, -) -> AsyncMock: - """Mock a update_status of the LinkPlayPlayer.""" - mock_player_ex.return_value = load_fixture("getPlayerEx.json", DOMAIN) - return mock_player_ex - - -@pytest.fixture -def mock_status_ex( - mock_status_ex: AsyncMock, -) -> AsyncMock: - """Mock a update_status of the LinkPlayDevice.""" - mock_status_ex.return_value = load_fixture("getStatusEx.json", DOMAIN) - return mock_status_ex - - -@contextmanager -def mock_lp_aiohttp_client() -> Iterator[AiohttpClientMocker]: - """Context manager to mock aiohttp client.""" - mocker = AiohttpClientMocker() - - def create_session(hass: HomeAssistant, *args: Any, **kwargs: Any) -> ClientSession: - session = mocker.create_session(hass.loop) - - async def close_session(event): - """Close session.""" - await session.close() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, close_session) - - return session - - with mock.patch( - "homeassistant.components.linkplay.async_get_client_session", - side_effect=create_session, - ): - yield mocker diff --git a/tests/components/linkplay/fixtures/getPlayerEx.json b/tests/components/linkplay/fixtures/getPlayerEx.json deleted file mode 100644 index 79d09f942df..00000000000 --- a/tests/components/linkplay/fixtures/getPlayerEx.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "0", - "ch": "0", - "mode": "0", - "loop": "0", - "eq": "0", - "status": "stop", - "curpos": "0", - "offset_pts": "0", - "totlen": "0", - "Title": "", - "Artist": "", - "Album": "", - "alarmflag": "0", - "plicount": "0", - "plicurr": "0", - "vol": "80", - "mute": "0" -} diff --git a/tests/components/linkplay/fixtures/getStatusEx.json b/tests/components/linkplay/fixtures/getStatusEx.json deleted file mode 100644 index 17eda4aeee8..00000000000 --- a/tests/components/linkplay/fixtures/getStatusEx.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "uuid": "FF31F09E5001FBDE05462DBFFF31F09E", - "DeviceName": "Smart Zone 1_54B9", - "GroupName": "Smart Zone 1_54B9", - "ssid": "Smart Zone 1_54B9", - "language": "en_us", - "firmware": "4.6.415145", - "hardware": "A31", - "build": "release", - "project": "SMART_ZONE4_AMP", - "priv_prj": "SMART_ZONE4_AMP", - "project_build_name": "a31rakoit", - "Release": "20220427", - "temp_uuid": "97296CE38DE8CC3D", - "hideSSID": "1", - "SSIDStrategy": "2", - "branch": "A31_stable_4.6", - "group": "0", - "wmrm_version": "4.2", - "internet": "1", - "MAC": "00:22:6C:21:7F:1D", - "STA_MAC": "00:00:00:00:00:00", - "CountryCode": "CN", - "CountryRegion": "1", - "netstat": "0", - "essid": "", - "apcli0": "", - "eth2": "192.168.168.197", - "ra0": "10.10.10.254", - "eth_dhcp": "1", - "VersionUpdate": "0", - "NewVer": "0", - "set_dns_enable": "1", - "mcu_ver": "37", - "mcu_ver_new": "0", - "dsp_ver": "0", - "dsp_ver_new": "0", - "date": "2024:10:29", - "time": "17:13:22", - "tz": "1.0000", - "dst_enable": "1", - "region": "unknown", - "prompt_status": "1", - "iot_ver": "1.0.0", - "upnp_version": "1005", - "cap1": "0x305200", - "capability": "0x28e90b80", - "languages": "0x6", - "streams_all": "0x7bff7ffe", - "streams": "0x7b9831fe", - "external": "0x0", - "plm_support": "0x40152", - "preset_key": "10", - "spotify_active": "0", - "lbc_support": "0", - "privacy_mode": "0", - "WifiChannel": "11", - "RSSI": "0", - "BSSID": "", - "battery": "0", - "battery_percent": "0", - "securemode": "1", - "auth": "WPAPSKWPA2PSK", - "encry": "AES", - "upnp_uuid": "uuid:FF31F09E-5001-FBDE-0546-2DBFFF31F09E", - "uart_pass_port": "8899", - "communication_port": "8819", - "web_firmware_update_hide": "0", - "ignore_talkstart": "0", - "web_login_result": "-1", - "silenceOTATime": "", - "ignore_silenceOTATime": "1", - "new_tunein_preset_and_alarm": "1", - "iheartradio_new": "1", - "new_iheart_podcast": "1", - "tidal_version": "2.0", - "service_version": "1.0", - "ETH_MAC": "00:22:6C:21:7F:20", - "security": "https/2.0", - "security_version": "2.0" -} diff --git a/tests/components/linkplay/snapshots/test_diagnostics.ambr b/tests/components/linkplay/snapshots/test_diagnostics.ambr deleted file mode 100644 index d8c52a25649..00000000000 --- a/tests/components/linkplay/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,115 +0,0 @@ -# serializer version: 1 -# name: test_diagnostics - dict({ - 'device_info': dict({ - 'device': dict({ - 'properties': dict({ - 'BSSID': '', - 'CountryCode': 'CN', - 'CountryRegion': '1', - 'DeviceName': 'Smart Zone 1_54B9', - 'ETH_MAC': '00:22:6C:21:7F:20', - 'GroupName': 'Smart Zone 1_54B9', - 'MAC': '00:22:6C:21:7F:1D', - 'NewVer': '0', - 'RSSI': '0', - 'Release': '20220427', - 'SSIDStrategy': '2', - 'STA_MAC': '00:00:00:00:00:00', - 'VersionUpdate': '0', - 'WifiChannel': '11', - 'apcli0': '', - 'auth': 'WPAPSKWPA2PSK', - 'battery': '0', - 'battery_percent': '0', - 'branch': 'A31_stable_4.6', - 'build': 'release', - 'cap1': '0x305200', - 'capability': '0x28e90b80', - 'communication_port': '8819', - 'date': '2024:10:29', - 'dsp_ver': '0', - 'dsp_ver_new': '0', - 'dst_enable': '1', - 'encry': 'AES', - 'essid': '', - 'eth2': '192.168.168.197', - 'eth_dhcp': '1', - 'external': '0x0', - 'firmware': '4.6.415145', - 'group': '0', - 'hardware': 'A31', - 'hideSSID': '1', - 'ignore_silenceOTATime': '1', - 'ignore_talkstart': '0', - 'iheartradio_new': '1', - 'internet': '1', - 'iot_ver': '1.0.0', - 'language': 'en_us', - 'languages': '0x6', - 'lbc_support': '0', - 'mcu_ver': '37', - 'mcu_ver_new': '0', - 'netstat': '0', - 'new_iheart_podcast': '1', - 'new_tunein_preset_and_alarm': '1', - 'plm_support': '0x40152', - 'preset_key': '10', - 'priv_prj': 'SMART_ZONE4_AMP', - 'privacy_mode': '0', - 'project': 'SMART_ZONE4_AMP', - 'project_build_name': 'a31rakoit', - 'prompt_status': '1', - 'ra0': '10.10.10.254', - 'region': 'unknown', - 'securemode': '1', - 'security': 'https/2.0', - 'security_version': '2.0', - 'service_version': '1.0', - 'set_dns_enable': '1', - 'silenceOTATime': '', - 'spotify_active': '0', - 'ssid': 'Smart Zone 1_54B9', - 'streams': '0x7b9831fe', - 'streams_all': '0x7bff7ffe', - 'temp_uuid': '97296CE38DE8CC3D', - 'tidal_version': '2.0', - 'time': '17:13:22', - 'tz': '1.0000', - 'uart_pass_port': '8899', - 'upnp_uuid': 'uuid:FF31F09E-5001-FBDE-0546-2DBFFF31F09E', - 'upnp_version': '1005', - 'uuid': 'FF31F09E5001FBDE05462DBFFF31F09E', - 'web_firmware_update_hide': '0', - 'web_login_result': '-1', - 'wmrm_version': '4.2', - }), - }), - 'endpoint': dict({ - 'endpoint': 'https://10.0.0.150', - }), - 'multiroom': None, - 'player': dict({ - 'properties': dict({ - 'Album': '', - 'Artist': '', - 'Title': '', - 'alarmflag': '0', - 'ch': '0', - 'curpos': '0', - 'eq': '0', - 'loop': '0', - 'mode': '0', - 'mute': '0', - 'offset_pts': '0', - 'plicount': '0', - 'plicurr': '0', - 'status': 'stop', - 'totlen': '0', - 'type': '0', - 'vol': '80', - }), - }), - }), - }) -# --- diff --git a/tests/components/linkplay/test_diagnostics.py b/tests/components/linkplay/test_diagnostics.py deleted file mode 100644 index 369142978a3..00000000000 --- a/tests/components/linkplay/test_diagnostics.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Tests for the LinkPlay diagnostics.""" - -from unittest.mock import patch - -from linkplay.bridge import LinkPlayMultiroom -from linkplay.consts import API_ENDPOINT -from linkplay.endpoint import LinkPlayApiEndpoint -from syrupy import SnapshotAssertion - -from homeassistant.components.linkplay.const import DOMAIN -from homeassistant.core import HomeAssistant - -from . import setup_integration -from .conftest import HOST, mock_lp_aiohttp_client - -from tests.common import MockConfigEntry, load_fixture -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - - -async def test_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_config_entry: MockConfigEntry, - snapshot: SnapshotAssertion, -) -> None: - """Test diagnostics.""" - - with ( - mock_lp_aiohttp_client() as mock_session, - patch.object(LinkPlayMultiroom, "update_status", return_value=None), - ): - endpoints = [ - LinkPlayApiEndpoint(protocol="https", endpoint=HOST, session=None), - LinkPlayApiEndpoint(protocol="http", endpoint=HOST, session=None), - ] - for endpoint in endpoints: - mock_session.get( - API_ENDPOINT.format(str(endpoint), "getPlayerStatusEx"), - text=load_fixture("getPlayerEx.json", DOMAIN), - ) - - mock_session.get( - API_ENDPOINT.format(str(endpoint), "getStatusEx"), - text=load_fixture("getStatusEx.json", DOMAIN), - ) - - await setup_integration(hass, mock_config_entry) - - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) - == snapshot - ) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index cdea046ceea..5c612f9f8ad 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -57,7 +57,7 @@ def check_config_loaded_fixture(): @pytest.fixture(name="register_words") def register_words_fixture(): """Set default for register_words.""" - return [0x00] + return [0x00, 0x00] @pytest.fixture(name="config_addon") diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index f34c40e09f9..5d4719918a6 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -107,7 +107,6 @@ class FakeSubscriber(GoogleNestSubscriber): def __init__(self) -> None: # pylint: disable=super-init-not-called """Initialize Fake Subscriber.""" self._device_manager = DeviceManager() - self._subscriber_name = "fake-name" def set_update_callback(self, target: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index b070d025612..85c64aff379 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -22,7 +22,6 @@ from homeassistant.components.application_credentials import ( ) from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID, SDM_SCOPES -from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -288,8 +287,6 @@ async def setup_base_platform( await hass.async_block_till_done() yield _setup_func - if config_entry and config_entry.state == ConfigEntryState.LOADED: - await hass.config_entries.async_unload(config_entry.entry_id) @pytest.fixture diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 029879f1413..500dbc0f46f 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -483,50 +483,6 @@ async def test_stream_response_already_expired( assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" -async def test_extending_stream_already_expired( - hass: HomeAssistant, - auth: FakeAuth, - setup_platform: PlatformSetup, - camera_device: None, -) -> None: - """Test a API response when extending the stream returns an expired stream url.""" - now = utcnow() - stream_1_expiration = now + datetime.timedelta(seconds=180) - stream_2_expiration = now + datetime.timedelta(seconds=30) # Will be in the past - stream_3_expiration = now + datetime.timedelta(seconds=600) - auth.responses = [ - make_stream_url_response(stream_1_expiration, token_num=1), - make_stream_url_response(stream_2_expiration, token_num=2), - make_stream_url_response(stream_3_expiration, token_num=3), - ] - await setup_platform() - - assert len(hass.states.async_all()) == 1 - cam = hass.states.get("camera.my_camera") - assert cam is not None - assert cam.state == CameraState.STREAMING - - # The stream is expired, but we return it anyway - stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") - assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" - - # Jump to when the stream will be refreshed - await fire_alarm(hass, now + datetime.timedelta(seconds=160)) - stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") - assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" - - # The stream will have expired in the past, but 1 minute min refresh interval is applied. - # The stream token is not updated. - await fire_alarm(hass, now + datetime.timedelta(seconds=170)) - stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") - assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" - - # Now go past the min update interval and the stream is refreshed - await fire_alarm(hass, now + datetime.timedelta(seconds=225)) - stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") - assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" - - async def test_camera_removed( hass: HomeAssistant, auth: FakeAuth, diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 309c8860c20..cd0904b181d 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import patch from pynina import ApiError +import pytest from homeassistant.components.nina.const import ( CONF_AREA_FILTER, @@ -278,6 +279,10 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} +@pytest.mark.parametrize( # Remove when translations fixed + "ignore_translations", + ["component.nina.options.error.unknown"], +) async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: """Test config flow options but with an unexpected exception.""" config_entry = MockConfigEntry( diff --git a/tests/components/nordpool/conftest.py b/tests/components/nordpool/conftest.py index d1c1972c568..305179c531a 100644 --- a/tests/components/nordpool/conftest.py +++ b/tests/components/nordpool/conftest.py @@ -23,6 +23,7 @@ from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") @pytest.fixture async def load_int( hass: HomeAssistant, get_data: DeliveryPeriodData diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr deleted file mode 100644 index dde2eca0022..00000000000 --- a/tests/components/nordpool/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,283 +0,0 @@ -# serializer version: 1 -# name: test_diagnostics - dict({ - 'raw': dict({ - 'areaAverages': list([ - dict({ - 'areaCode': 'SE3', - 'price': 900.74, - }), - dict({ - 'areaCode': 'SE4', - 'price': 1166.12, - }), - ]), - 'areaStates': list([ - dict({ - 'areas': list([ - 'SE3', - 'SE4', - ]), - 'state': 'Final', - }), - ]), - 'blockPriceAggregates': list([ - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 422.87, - 'max': 1406.14, - 'min': 61.69, - }), - 'SE4': dict({ - 'average': 497.97, - 'max': 1648.25, - 'min': 65.19, - }), - }), - 'blockName': 'Off-peak 1', - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', - }), - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 1315.97, - 'max': 2512.65, - 'min': 925.05, - }), - 'SE4': dict({ - 'average': 1735.59, - 'max': 3533.03, - 'min': 1081.72, - }), - }), - 'blockName': 'Peak', - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', - }), - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 610.79, - 'max': 835.53, - 'min': 289.14, - }), - 'SE4': dict({ - 'average': 793.98, - 'max': 1112.57, - 'min': 349.21, - }), - }), - 'blockName': 'Off-peak 2', - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', - }), - ]), - 'currency': 'SEK', - 'deliveryAreas': list([ - 'SE3', - 'SE4', - ]), - 'deliveryDateCET': '2024-11-05', - 'exchangeRate': 11.6402, - 'market': 'DayAhead', - 'multiAreaEntries': list([ - dict({ - 'deliveryEnd': '2024-11-05T00:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', - 'entryPerArea': dict({ - 'SE3': 250.73, - 'SE4': 283.79, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T01:00:00Z', - 'deliveryStart': '2024-11-05T00:00:00Z', - 'entryPerArea': dict({ - 'SE3': 76.36, - 'SE4': 81.36, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T02:00:00Z', - 'deliveryStart': '2024-11-05T01:00:00Z', - 'entryPerArea': dict({ - 'SE3': 73.92, - 'SE4': 79.15, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T03:00:00Z', - 'deliveryStart': '2024-11-05T02:00:00Z', - 'entryPerArea': dict({ - 'SE3': 61.69, - 'SE4': 65.19, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T04:00:00Z', - 'deliveryStart': '2024-11-05T03:00:00Z', - 'entryPerArea': dict({ - 'SE3': 64.6, - 'SE4': 68.44, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T05:00:00Z', - 'deliveryStart': '2024-11-05T04:00:00Z', - 'entryPerArea': dict({ - 'SE3': 453.27, - 'SE4': 516.71, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T06:00:00Z', - 'deliveryStart': '2024-11-05T05:00:00Z', - 'entryPerArea': dict({ - 'SE3': 996.28, - 'SE4': 1240.85, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-05T06:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1406.14, - 'SE4': 1648.25, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T08:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1346.54, - 'SE4': 1570.5, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T09:00:00Z', - 'deliveryStart': '2024-11-05T08:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1150.28, - 'SE4': 1345.37, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T10:00:00Z', - 'deliveryStart': '2024-11-05T09:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1031.32, - 'SE4': 1206.51, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T11:00:00Z', - 'deliveryStart': '2024-11-05T10:00:00Z', - 'entryPerArea': dict({ - 'SE3': 927.37, - 'SE4': 1085.8, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T12:00:00Z', - 'deliveryStart': '2024-11-05T11:00:00Z', - 'entryPerArea': dict({ - 'SE3': 925.05, - 'SE4': 1081.72, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T13:00:00Z', - 'deliveryStart': '2024-11-05T12:00:00Z', - 'entryPerArea': dict({ - 'SE3': 949.49, - 'SE4': 1130.38, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T14:00:00Z', - 'deliveryStart': '2024-11-05T13:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1042.03, - 'SE4': 1256.91, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T15:00:00Z', - 'deliveryStart': '2024-11-05T14:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1258.89, - 'SE4': 1765.82, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T16:00:00Z', - 'deliveryStart': '2024-11-05T15:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1816.45, - 'SE4': 2522.55, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T17:00:00Z', - 'deliveryStart': '2024-11-05T16:00:00Z', - 'entryPerArea': dict({ - 'SE3': 2512.65, - 'SE4': 3533.03, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T18:00:00Z', - 'deliveryStart': '2024-11-05T17:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1819.83, - 'SE4': 2524.06, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T18:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1011.77, - 'SE4': 1804.46, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T20:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', - 'entryPerArea': dict({ - 'SE3': 835.53, - 'SE4': 1112.57, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T21:00:00Z', - 'deliveryStart': '2024-11-05T20:00:00Z', - 'entryPerArea': dict({ - 'SE3': 796.19, - 'SE4': 1051.69, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T22:00:00Z', - 'deliveryStart': '2024-11-05T21:00:00Z', - 'entryPerArea': dict({ - 'SE3': 522.3, - 'SE4': 662.44, - }), - }), - dict({ - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T22:00:00Z', - 'entryPerArea': dict({ - 'SE3': 289.14, - 'SE4': 349.21, - }), - }), - ]), - 'updatedAt': '2024-11-04T12:15:03.9456464Z', - 'version': 3, - }), - }) -# --- diff --git a/tests/components/nordpool/test_config_flow.py b/tests/components/nordpool/test_config_flow.py index cfdfc63aca7..d17db619b02 100644 --- a/tests/components/nordpool/test_config_flow.py +++ b/tests/components/nordpool/test_config_flow.py @@ -2,12 +2,13 @@ from __future__ import annotations +from dataclasses import replace from unittest.mock import patch from pynordpool import ( DeliveryPeriodData, + NordPoolAuthenticationError, NordPoolConnectionError, - NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -70,7 +71,7 @@ async def test_single_config_entry( ("error_message", "p_error"), [ (NordPoolConnectionError, "cannot_connect"), - (NordPoolEmptyResponseError, "no_data"), + (NordPoolAuthenticationError, "cannot_connect"), (NordPoolError, "cannot_connect"), (NordPoolResponseError, "cannot_connect"), ], @@ -115,6 +116,44 @@ async def test_cannot_connect( assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +async def test_empty_data(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: + """Test empty data error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + + invalid_data = replace(get_data, raw={}) + + with patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + return_value=invalid_data, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["errors"] == {"base": "no_data"} + + with patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + return_value=get_data, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Nord Pool" + assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} + + @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") async def test_reconfigure( hass: HomeAssistant, @@ -154,7 +193,7 @@ async def test_reconfigure( ("error_message", "p_error"), [ (NordPoolConnectionError, "cannot_connect"), - (NordPoolEmptyResponseError, "no_data"), + (NordPoolAuthenticationError, "cannot_connect"), (NordPoolError, "cannot_connect"), (NordPoolResponseError, "cannot_connect"), ], diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index d2d912b1b99..9cff34adb1f 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -9,7 +9,6 @@ from freezegun.api import FrozenDateTimeFactory from pynordpool import ( DeliveryPeriodData, NordPoolAuthenticationError, - NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, ) @@ -19,13 +18,14 @@ from homeassistant.components.nordpool.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed -@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +@pytest.mark.freeze_time("2024-11-05T12:00:00+00:00") async def test_coordinator( hass: HomeAssistant, get_data: DeliveryPeriodData, @@ -51,7 +51,7 @@ async def test_coordinator( await hass.async_block_till_done() mock_data.assert_called_once() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.92737" + assert state.state == "0.94949" mock_data.reset_mock() mock_data.side_effect = NordPoolError("error") @@ -74,17 +74,6 @@ async def test_coordinator( assert "Authentication error" in caplog.text mock_data.reset_mock() - assert "Empty response" not in caplog.text - mock_data.side_effect = NordPoolEmptyResponseError("Empty response") - freezer.tick(timedelta(hours=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - mock_data.assert_called_once() - state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == STATE_UNAVAILABLE - assert "Empty response" in caplog.text - mock_data.reset_mock() - assert "Response error" not in caplog.text mock_data.side_effect = NordPoolResponseError("Response error") freezer.tick(timedelta(hours=1)) @@ -96,6 +85,25 @@ async def test_coordinator( assert "Response error" in caplog.text mock_data.reset_mock() + mock_data.return_value = DeliveryPeriodData( + raw={}, + requested_date="2024-11-05", + updated_at=dt_util.utcnow(), + entries=[], + block_prices=[], + currency="SEK", + exchange_rate=1, + area_average={}, + ) + mock_data.side_effect = None + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() + mock_data.return_value = get_data mock_data.side_effect = None freezer.tick(timedelta(hours=1)) @@ -103,4 +111,4 @@ async def test_coordinator( await hass.async_block_till_done() mock_data.assert_called_once() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.81645" + assert state.state == "1.81983" diff --git a/tests/components/nordpool/test_diagnostics.py b/tests/components/nordpool/test_diagnostics.py deleted file mode 100644 index 4639186ecf1..00000000000 --- a/tests/components/nordpool/test_diagnostics.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Test Nord Pool diagnostics.""" - -from __future__ import annotations - -from syrupy.assertion import SnapshotAssertion - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - - -async def test_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - load_int: ConfigEntry, - snapshot: SnapshotAssertion, -) -> None: - """Test generating diagnostics for a config entry.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, load_int) == snapshot - ) diff --git a/tests/components/palazzetti/test_config_flow.py b/tests/components/palazzetti/test_config_flow.py index 03c56c33d0c..960ad7a1184 100644 --- a/tests/components/palazzetti/test_config_flow.py +++ b/tests/components/palazzetti/test_config_flow.py @@ -4,9 +4,8 @@ from unittest.mock import AsyncMock from pypalazzetti.exceptions import CommunicationError -from homeassistant.components import dhcp from homeassistant.components.palazzetti.const import DOMAIN -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -93,48 +92,3 @@ async def test_duplicate( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_dhcp_flow( - hass: HomeAssistant, mock_palazzetti_client: AsyncMock, mock_setup_entry: AsyncMock -) -> None: - """Test the DHCP flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=dhcp.DhcpServiceInfo( - hostname="connbox1234", ip="192.168.1.1", macaddress="11:22:33:44:55:66" - ), - context={"source": SOURCE_DHCP}, - ) - - await hass.async_block_till_done() - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) - - await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Stove" - assert result["result"].unique_id == "11:22:33:44:55:66" - - -async def test_dhcp_flow_error( - hass: HomeAssistant, mock_palazzetti_client: AsyncMock, mock_setup_entry: AsyncMock -) -> None: - """Test the DHCP flow.""" - mock_palazzetti_client.connect.side_effect = CommunicationError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=dhcp.DhcpServiceInfo( - hostname="connbox1234", ip="192.168.1.1", macaddress="11:22:33:44:55:66" - ), - context={"source": SOURCE_DHCP}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 1ff1470f81c..5074a289d19 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -339,11 +339,6 @@ async def test_form_reauth(hass: HomeAssistant) -> None: result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - flow = hass.config_entries.flow.async_get(result["flow_id"]) - assert flow["context"]["title_placeholders"] == { - "ip_address": VALID_CONFIG[CONF_IP_ADDRESS], - "name": entry.title, - } mock_powerwall = await _mock_powerwall_site_name(hass, "My site") diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 37940df437b..3f0e0b92056 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -5,7 +5,6 @@ from functools import lru_cache import logging import os from pathlib import Path -import sys from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory @@ -71,9 +70,6 @@ async def test_basic_usage(hass: HomeAssistant, tmp_path: Path) -> None: await hass.async_block_till_done() -@pytest.mark.skipif( - sys.version_info >= (3, 13), reason="not yet available on Python 3.13" -) async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None: """Test we can setup and the service is registered.""" test_dir = tmp_path / "profiles" @@ -105,24 +101,6 @@ async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None: await hass.async_block_till_done() -@pytest.mark.skipif(sys.version_info < (3, 13), reason="still works on python 3.12") -async def test_memory_usage_py313(hass: HomeAssistant, tmp_path: Path) -> None: - """Test raise an error on python3.13.""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert hass.services.has_service(DOMAIN, SERVICE_MEMORY) - with pytest.raises( - HomeAssistantError, - match="Memory profiling is not supported on Python 3.13. Please use Python 3.12.", - ): - await hass.services.async_call( - DOMAIN, SERVICE_MEMORY, {CONF_SECONDS: 0.000001}, blocking=True - ) - - async def test_object_growth_logging( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/russound_rio/__init__.py b/tests/components/russound_rio/__init__.py index d0e6d77f1ee..96171071907 100644 --- a/tests/components/russound_rio/__init__.py +++ b/tests/components/russound_rio/__init__.py @@ -1,13 +1 @@ """Tests for the Russound RIO integration.""" - -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 09cccd7d83f..91d009f13f4 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -1,19 +1,16 @@ """Test fixtures for Russound RIO integration.""" from collections.abc import Generator -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, patch -from aiorussound import Controller, RussoundTcpConnectionHandler, Source -from aiorussound.rio import ZoneControlSurface -from aiorussound.util import controller_device_str, zone_device_str import pytest from homeassistant.components.russound_rio.const import DOMAIN from homeassistant.core import HomeAssistant -from .const import HARDWARE_MAC, HOST, MOCK_CONFIG, MODEL, PORT +from .const import HARDWARE_MAC, MOCK_CONFIG, MOCK_CONTROLLERS, MODEL -from tests.common import MockConfigEntry, load_json_object_fixture +from tests.common import MockConfigEntry @pytest.fixture @@ -28,13 +25,15 @@ def mock_setup_entry(): @pytest.fixture def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Mock a Russound RIO config entry.""" - return MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, unique_id=HARDWARE_MAC, title=MODEL ) + entry.add_to_hass(hass) + return entry @pytest.fixture -def mock_russound_client() -> Generator[AsyncMock]: +def mock_russound() -> Generator[AsyncMock]: """Mock the Russound RIO client.""" with ( patch( @@ -42,32 +41,8 @@ def mock_russound_client() -> Generator[AsyncMock]: ) as mock_client, patch( "homeassistant.components.russound_rio.config_flow.RussoundClient", - new=mock_client, + return_value=mock_client, ), ): - client = mock_client.return_value - zones = { - int(k): ZoneControlSurface.from_dict(v) - for k, v in load_json_object_fixture("get_zones.json", DOMAIN).items() - } - client.sources = { - int(k): Source.from_dict(v) - for k, v in load_json_object_fixture("get_sources.json", DOMAIN).items() - } - for k, v in zones.items(): - v.device_str = zone_device_str(1, k) - v.fetch_current_source = Mock( - side_effect=lambda current_source=v.current_source: client.sources.get( - int(current_source) - ) - ) - - client.controllers = { - 1: Controller( - 1, "MCA-C5", client, controller_device_str(1), HARDWARE_MAC, None, zones - ) - } - client.connection_handler = RussoundTcpConnectionHandler(HOST, PORT) - client.is_connected = Mock(return_value=True) - client.unregister_state_update_callbacks.return_value = True - yield client + mock_client.controllers = MOCK_CONTROLLERS + yield mock_client diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 3d2924693d2..527f4fe3377 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -2,8 +2,6 @@ from collections import namedtuple -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN - HOST = "127.0.0.1" PORT = 9621 MODEL = "MCA-C5" @@ -16,7 +14,3 @@ MOCK_CONFIG = { _CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) # noqa: PYI024 MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} - -DEVICE_NAME = "mca_c5" -NAME_ZONE_1 = "backyard" -ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{DEVICE_NAME}_{NAME_ZONE_1}" diff --git a/tests/components/russound_rio/fixtures/get_sources.json b/tests/components/russound_rio/fixtures/get_sources.json deleted file mode 100644 index e39d702b8a1..00000000000 --- a/tests/components/russound_rio/fixtures/get_sources.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "1": { - "name": "Aux", - "type": "Miscellaneous Audio" - }, - "2": { - "name": "Spotify", - "type": "Russound Media Streamer" - } -} diff --git a/tests/components/russound_rio/fixtures/get_zones.json b/tests/components/russound_rio/fixtures/get_zones.json deleted file mode 100644 index 396310339b3..00000000000 --- a/tests/components/russound_rio/fixtures/get_zones.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "1": { - "name": "Backyard", - "volume": "10", - "status": "ON", - "enabled": "True", - "current_source": "1" - }, - "2": { - "name": "Kitchen", - "volume": "50", - "status": "OFF", - "enabled": "True", - "current_source": "2" - }, - "3": { - "name": "Bedroom", - "volume": "10", - "status": "OFF", - "enabled": "False" - } -} diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr deleted file mode 100644 index fcd59dd06f7..00000000000 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ /dev/null @@ -1,37 +0,0 @@ -# serializer version: 1 -# name: test_device_info - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'http://127.0.0.1', - 'connections': set({ - tuple( - 'mac', - '00:11:22:33:44:55', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'russound_rio', - '00:11:22:33:44:55', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Russound', - 'model': 'MCA-C5', - 'model_id': None, - 'name': 'MCA-C5', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py index cf754852731..9461fe1d5be 100644 --- a/tests/components/russound_rio/test_config_flow.py +++ b/tests/components/russound_rio/test_config_flow.py @@ -11,7 +11,7 @@ from .const import MOCK_CONFIG, MODEL async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -32,13 +32,13 @@ async def test_form( async def test_form_cannot_connect( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - mock_russound_client.connect.side_effect = TimeoutError + mock_russound.connect.side_effect = TimeoutError result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -48,7 +48,7 @@ async def test_form_cannot_connect( assert result["errors"] == {"base": "cannot_connect"} # Recover with correct information - mock_russound_client.connect.side_effect = None + mock_russound.connect.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -61,7 +61,7 @@ async def test_form_cannot_connect( async def test_import( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock ) -> None: """Test we import a config entry.""" result = await hass.config_entries.flow.async_init( @@ -77,10 +77,10 @@ async def test_import( async def test_import_cannot_connect( - hass: HomeAssistant, mock_russound_client: AsyncMock + hass: HomeAssistant, mock_russound: AsyncMock ) -> None: """Test we handle import cannot connect error.""" - mock_russound_client.connect.side_effect = TimeoutError + mock_russound.connect.side_effect = TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG diff --git a/tests/components/russound_rio/test_init.py b/tests/components/russound_rio/test_init.py deleted file mode 100644 index 6787ee37c79..00000000000 --- a/tests/components/russound_rio/test_init.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Tests for the Russound RIO integration.""" - -from unittest.mock import AsyncMock - -from syrupy import SnapshotAssertion - -from homeassistant.components.russound_rio.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from . import setup_integration - -from tests.common import MockConfigEntry - - -async def test_config_entry_not_ready( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_russound_client: AsyncMock, -) -> None: - """Test the Cambridge Audio configuration entry not ready.""" - mock_russound_client.connect.side_effect = TimeoutError - await setup_integration(hass, mock_config_entry) - - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - mock_russound_client.connect = AsyncMock(return_value=True) - - -async def test_device_info( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_russound_client: AsyncMock, - mock_config_entry: MockConfigEntry, - device_registry: dr.DeviceRegistry, -) -> None: - """Test device registry integration.""" - await setup_integration(hass, mock_config_entry) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.unique_id)} - ) - assert device_entry is not None - assert device_entry == snapshot diff --git a/tests/components/russound_rio/test_media_player.py b/tests/components/russound_rio/test_media_player.py deleted file mode 100644 index e720e2c7f65..00000000000 --- a/tests/components/russound_rio/test_media_player.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Tests for the Russound RIO media player.""" - -from unittest.mock import AsyncMock - -from aiorussound.models import CallbackType, PlayStatus -import pytest - -from homeassistant.const import ( - STATE_BUFFERING, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant - -from . import setup_integration -from .const import ENTITY_ID_ZONE_1 - -from tests.common import MockConfigEntry - - -async def mock_state_update(client: AsyncMock) -> None: - """Trigger a callback in the media player.""" - for callback in client.register_state_update_callbacks.call_args_list: - await callback[0][0](client, CallbackType.STATE) - - -@pytest.mark.parametrize( - ("zone_status", "source_play_status", "media_player_state"), - [ - (True, None, STATE_ON), - (True, PlayStatus.PLAYING, STATE_PLAYING), - (True, PlayStatus.PAUSED, STATE_PAUSED), - (True, PlayStatus.TRANSITIONING, STATE_BUFFERING), - (True, PlayStatus.STOPPED, STATE_IDLE), - (False, None, STATE_OFF), - (False, PlayStatus.STOPPED, STATE_OFF), - ], -) -async def test_entity_state( - hass: HomeAssistant, - mock_russound_client: AsyncMock, - mock_config_entry: MockConfigEntry, - zone_status: bool, - source_play_status: PlayStatus | None, - media_player_state: str, -) -> None: - """Test media player state.""" - await setup_integration(hass, mock_config_entry) - mock_russound_client.controllers[1].zones[1].status = zone_status - mock_russound_client.sources[1].play_status = source_play_status - await mock_state_update(mock_russound_client) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID_ZONE_1) - assert state.state == media_player_state 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/smarty/conftest.py b/tests/components/smarty/conftest.py index a9b518d88f4..c61ec4b1022 100644 --- a/tests/components/smarty/conftest.py +++ b/tests/components/smarty/conftest.py @@ -50,7 +50,6 @@ def mock_smarty() -> Generator[AsyncMock]: client.filter_timer = 31 client.get_configuration_version.return_value = 111 client.get_software_version.return_value = 127 - client.reset_filters_timer.return_value = True yield client diff --git a/tests/components/smarty/snapshots/test_button.ambr b/tests/components/smarty/snapshots/test_button.ambr deleted file mode 100644 index 38849bd2b2e..00000000000 --- a/tests/components/smarty/snapshots/test_button.ambr +++ /dev/null @@ -1,47 +0,0 @@ -# serializer version: 1 -# name: test_all_entities[button.mock_title_reset_filters_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.mock_title_reset_filters_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 filters timer', - 'platform': 'smarty', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'reset_filters_timer', - 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_reset_filters_timer', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[button.mock_title_reset_filters_timer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Reset filters timer', - }), - 'context': , - 'entity_id': 'button.mock_title_reset_filters_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- diff --git a/tests/components/smarty/test_button.py b/tests/components/smarty/test_button.py deleted file mode 100644 index 0a7b67f2be6..00000000000 --- a/tests/components/smarty/test_button.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Tests for the Smarty button platform.""" - -from unittest.mock import AsyncMock, patch - -from syrupy import SnapshotAssertion - -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import MockConfigEntry, snapshot_platform - - -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_smarty: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - with patch("homeassistant.components.smarty.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_setting_value( - hass: HomeAssistant, - mock_smarty: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test setting value.""" - await setup_integration(hass, mock_config_entry) - - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - target={ATTR_ENTITY_ID: "button.mock_title_reset_filters_timer"}, - blocking=True, - ) - mock_smarty.reset_filters_timer.assert_called_once_with() diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 660102ed082..b6050808a34 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -5,6 +5,6 @@ from homeassistant.const import CONF_API_KEY, CONF_URL MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"} MOCK_USER_INPUT = { - CONF_URL: "http://192.168.1.189:8989/", + CONF_URL: "http://192.168.1.189:8989", CONF_API_KEY: "MOCK_API_KEY", } diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index efbfbd749b3..118d5020cba 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -50,34 +50,6 @@ async def test_cannot_connect( assert result["errors"] == {"base": "cannot_connect"} -async def test_url_rewrite( - hass: HomeAssistant, - mock_sonarr_config_flow: MagicMock, - mock_setup_entry: None, -) -> None: - """Test the full manual user flow from start to finish.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - user_input = MOCK_USER_INPUT.copy() - user_input[CONF_URL] = "https://192.168.1.189" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "192.168.1.189" - - assert result["data"] - assert result["data"][CONF_URL] == "https://192.168.1.189:443/" - - async def test_invalid_auth( hass: HomeAssistant, mock_sonarr_config_flow: MagicMock ) -> None: @@ -173,7 +145,7 @@ async def test_full_user_flow_implementation( assert result["title"] == "192.168.1.189" assert result["data"] - assert result["data"][CONF_URL] == "http://192.168.1.189:8989/" + assert result["data"][CONF_URL] == "http://192.168.1.189:8989" async def test_full_user_flow_advanced_options( @@ -203,7 +175,7 @@ async def test_full_user_flow_advanced_options( assert result["title"] == "192.168.1.189" assert result["data"] - assert result["data"][CONF_URL] == "http://192.168.1.189:8989/" + assert result["data"][CONF_URL] == "http://192.168.1.189:8989" assert result["data"][CONF_VERIFY_SSL] 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/suez_water/conftest.py b/tests/components/suez_water/conftest.py index f634a053c65..0cbf16095bf 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -3,11 +3,10 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from pysuez import AggregatedData, PriceResult -from pysuez.const import ATTRIBUTION import pytest from homeassistant.components.suez_water.const import DOMAIN +from homeassistant.components.suez_water.coordinator import AggregatedData from tests.common import MockConfigEntry @@ -39,7 +38,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="suez_client") -def mock_suez_client() -> Generator[AsyncMock]: +def mock_suez_data() -> Generator[AsyncMock]: """Create mock for suez_water external api.""" with ( patch( @@ -65,7 +64,7 @@ def mock_suez_client() -> Generator[AsyncMock]: }, current_year=1500, previous_year=1000, - attribution=ATTRIBUTION, + attribution="suez water mock test", highest_monthly_consumption=2558, history={ "2024-01-01": 130, @@ -76,5 +75,4 @@ def mock_suez_client() -> Generator[AsyncMock]: ) suez_client.fetch_aggregated_data.return_value = result - suez_client.get_price.return_value = PriceResult("4.74") yield suez_client diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index da0ed3df7dd..acc3042f93b 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -1,53 +1,4 @@ # serializer version: 1 -# name: test_sensors_valid_state[sensor.suez_mock_device_water_price-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.suez_mock_device_water_price', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Water price', - 'platform': 'suez_water', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'water_price', - 'unique_id': 'test-counter_water_price', - 'unit_of_measurement': '€', - }) -# --- -# name: test_sensors_valid_state[sensor.suez_mock_device_water_price-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by toutsurmoneau.fr', - 'device_class': 'monetary', - 'friendly_name': 'Suez mock device Water price', - 'unit_of_measurement': '€', - }), - 'context': , - 'entity_id': 'sensor.suez_mock_device_water_price', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4.74', - }) -# --- # name: test_sensors_valid_state[sensor.suez_mock_device_water_usage_yesterday-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -84,7 +35,7 @@ # name: test_sensors_valid_state[sensor.suez_mock_device_water_usage_yesterday-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by toutsurmoneau.fr', + 'attribution': 'suez water mock test', 'device_class': 'water', 'friendly_name': 'Suez mock device Water usage yesterday', 'highest_monthly_consumption': 2558, diff --git a/tests/components/suez_water/test_sensor.py b/tests/components/suez_water/test_sensor.py index cb578432f62..1cd40dff75b 100644 --- a/tests/components/suez_water/test_sensor.py +++ b/tests/components/suez_water/test_sensor.py @@ -3,7 +3,6 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -import pytest from syrupy import SnapshotAssertion from homeassistant.components.suez_water.const import DATA_REFRESH_INTERVAL @@ -33,13 +32,11 @@ async def test_sensors_valid_state( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize("method", [("fetch_aggregated_data"), ("get_price")]) async def test_sensors_failed_update( hass: HomeAssistant, suez_client: AsyncMock, mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, - method: str, ) -> None: """Test that suez_water sensor reflect failure when api fails.""" @@ -48,20 +45,18 @@ async def test_sensors_failed_update( assert mock_config_entry.state is ConfigEntryState.LOADED entity_ids = await hass.async_add_executor_job(hass.states.entity_ids) - assert len(entity_ids) == 2 + assert len(entity_ids) == 1 - for entity in entity_ids: - state = hass.states.get(entity) - assert entity - assert state.state != STATE_UNAVAILABLE + state = hass.states.get(entity_ids[0]) + assert entity_ids[0] + assert state.state != STATE_UNAVAILABLE - getattr(suez_client, method).side_effect = PySuezError("Should fail to update") + suez_client.fetch_aggregated_data.side_effect = PySuezError("Should fail to update") freezer.tick(DATA_REFRESH_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(True) - for entity in entity_ids: - state = hass.states.get(entity) - assert entity - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(entity_ids[0]) + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index 43431ae04c0..25ea370efe5 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -50,18 +50,6 @@ async def test_setup_entry_success( remoteType="DIY Plug", hubDeviceId="test-hub-id", ), - Remote( - deviceId="meter-pro-1", - deviceName="meter-pro-name-1", - deviceType="MeterPro(CO2)", - hubDeviceId="test-hub-id", - ), - Remote( - deviceId="hub2-1", - deviceName="hub2-name-1", - deviceType="Hub 2", - hubDeviceId="test-hub-id", - ), ] mock_get_status.return_value = {"power": PowerState.ON.value} entry = configure_integration(hass) diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index 8e028cb5300..68444de640c 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -6,8 +6,8 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch -from aiotedee.bridge import TedeeBridge -from aiotedee.lock import TedeeLock +from pytedee_async.bridge import TedeeBridge +from pytedee_async.lock import TedeeLock import pytest from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN diff --git a/tests/components/tedee/test_binary_sensor.py b/tests/components/tedee/test_binary_sensor.py index dfe70e7a2ea..788d31c84d2 100644 --- a/tests/components/tedee/test_binary_sensor.py +++ b/tests/components/tedee/test_binary_sensor.py @@ -3,8 +3,8 @@ from datetime import timedelta from unittest.mock import MagicMock -from aiotedee import TedeeLock from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index 825e01aca70..2e86286c8da 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -2,12 +2,12 @@ from unittest.mock import MagicMock, patch -from aiotedee import ( +from pytedee_async import ( TedeeClientException, TedeeDataUpdateException, TedeeLocalAuthException, ) -from aiotedee.bridge import TedeeBridge +from pytedee_async.bridge import TedeeBridge import pytest from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN diff --git a/tests/components/tedee/test_init.py b/tests/components/tedee/test_init.py index 63701bb1788..d4ac1c9d290 100644 --- a/tests/components/tedee/test_init.py +++ b/tests/components/tedee/test_init.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import MagicMock, patch from urllib.parse import urlparse -from aiotedee.exception import ( +from pytedee_async.exception import ( TedeeAuthException, TedeeClientException, TedeeWebhookException, diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index 45eae6e22d9..3f6b97e2c70 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -4,13 +4,13 @@ from datetime import timedelta from unittest.mock import MagicMock from urllib.parse import urlparse -from aiotedee import TedeeLock, TedeeLockState -from aiotedee.exception import ( +from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock, TedeeLockState +from pytedee_async.exception import ( TedeeClientException, TedeeDataUpdateException, TedeeLocalAuthException, ) -from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/tedee/test_sensor.py b/tests/components/tedee/test_sensor.py index ddbcd5086af..72fbd9cbe8d 100644 --- a/tests/components/tedee/test_sensor.py +++ b/tests/components/tedee/test_sensor.py @@ -3,8 +3,8 @@ from datetime import timedelta from unittest.mock import MagicMock -from aiotedee import TedeeLock from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock import pytest from syrupy import SnapshotAssertion diff --git a/tests/helpers/test_trigger_template_entity.py b/tests/components/template/test_manual_trigger_entity.py similarity index 100% rename from tests/helpers/test_trigger_template_entity.py rename to tests/components/template/test_manual_trigger_entity.py diff --git a/tests/components/tesla_fleet/test_button.py b/tests/components/tesla_fleet/test_button.py index ef1cfd90357..07fdc962be9 100644 --- a/tests/components/tesla_fleet/test_button.py +++ b/tests/components/tesla_fleet/test_button.py @@ -77,13 +77,9 @@ async def test_press_signing_error( new_product["response"][0]["command_signing"] = "required" mock_products.return_value = new_product - with ( - patch("homeassistant.components.tesla_fleet.TeslaFleetApi.get_private_key"), - ): - await setup_platform(hass, normal_config_entry, [Platform.BUTTON]) + await setup_platform(hass, normal_config_entry, [Platform.BUTTON]) with ( - patch("homeassistant.components.tesla_fleet.TeslaFleetApi.get_private_key"), patch( "homeassistant.components.tesla_fleet.VehicleSigned.flash_lights", side_effect=NotOnWhitelistFault, diff --git a/tests/components/thethingsnetwork/test_init.py b/tests/components/thethingsnetwork/test_init.py index e39c764d5f9..1e0b64c933d 100644 --- a/tests/components/thethingsnetwork/test_init.py +++ b/tests/components/thethingsnetwork/test_init.py @@ -4,6 +4,22 @@ import pytest from ttn_client import TTNAuthError from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from .conftest import DOMAIN + + +async def test_error_configuration( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test issue is logged when deprecated configuration is used.""" + await async_setup_component( + hass, DOMAIN, {DOMAIN: {"app_id": "123", "access_key": "42"}} + ) + await hass.async_block_till_done() + assert issue_registry.async_get_issue(DOMAIN, "manual_migration") @pytest.mark.parametrize(("exception_class"), [TTNAuthError, Exception]) diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index 259009c6319..e0973c7a580 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -538,7 +538,7 @@ async def test_sensor_no_lower_upper( await async_setup_component(hass, Platform.BINARY_SENSOR, config) await hass.async_block_till_done() - assert "Lower or Upper thresholds are not provided" in caplog.text + assert "Lower or Upper thresholds not provided" in caplog.text async def test_device_id( diff --git a/tests/components/tibber/test_diagnostics.py b/tests/components/tibber/test_diagnostics.py index 16c735596d0..34ecb63dfec 100644 --- a/tests/components/tibber/test_diagnostics.py +++ b/tests/components/tibber/test_diagnostics.py @@ -19,9 +19,12 @@ async def test_entry_diagnostics( config_entry, ) -> None: """Test config entry diagnostics.""" - with patch( - "tibber.Tibber.update_info", - return_value=None, + with ( + patch( + "tibber.Tibber.update_info", + return_value=None, + ), + patch("homeassistant.components.tibber.discovery.async_load_platform"), ): assert await async_setup_component(hass, "tibber", {}) diff --git a/tests/components/tibber/test_notify.py b/tests/components/tibber/test_notify.py index 9b731e78bf6..69af92c4d5d 100644 --- a/tests/components/tibber/test_notify.py +++ b/tests/components/tibber/test_notify.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.recorder import Recorder +from homeassistant.components.tibber import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -18,8 +19,18 @@ async def test_notification_services( notify_state = hass.states.get("notify.tibber") assert notify_state is not None + # Assert legacy notify service hass been added + assert hass.services.has_service("notify", DOMAIN) + + # Test legacy notify service + service = "tibber" + service_data = {"message": "The message", "title": "A title"} + await hass.services.async_call("notify", service, service_data, blocking=True) calls: MagicMock = mock_tibber_setup.send_notification + calls.assert_called_once_with(message="The message", title="A title") + calls.reset_mock() + # Test notify entity service service = "send_message" service_data = { @@ -33,6 +44,15 @@ async def test_notification_services( calls.side_effect = TimeoutError + with pytest.raises(HomeAssistantError): + # Test legacy notify service + await hass.services.async_call( + "notify", + service="tibber", + service_data={"message": "The message", "title": "A title"}, + blocking=True, + ) + with pytest.raises(HomeAssistantError): # Test notify entity service await hass.services.async_call( diff --git a/tests/components/tibber/test_repairs.py b/tests/components/tibber/test_repairs.py new file mode 100644 index 00000000000..5e5fde4569e --- /dev/null +++ b/tests/components/tibber/test_repairs.py @@ -0,0 +1,56 @@ +"""Test loading of the Tibber config entry.""" + +from unittest.mock import MagicMock + +from homeassistant.components.recorder import Recorder +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.components.repairs import process_repair_fix_flow, start_repair_fix_flow +from tests.typing import ClientSessionGenerator + + +async def test_repair_flow( + recorder_mock: Recorder, + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + mock_tibber_setup: MagicMock, + hass_client: ClientSessionGenerator, +) -> None: + """Test unloading the entry.""" + + # Test legacy notify service + service = "tibber" + service_data = {"message": "The message", "title": "A title"} + await hass.services.async_call("notify", service, service_data, blocking=True) + calls: MagicMock = mock_tibber_setup.send_notification + + calls.assert_called_once_with(message="The message", title="A title") + calls.reset_mock() + + http_client = await hass_client() + # Assert the issue is present + assert issue_registry.async_get_issue( + domain="notify", + issue_id=f"migrate_notify_tibber_{service}", + ) + assert len(issue_registry.issues) == 1 + + data = await start_repair_fix_flow( + http_client, "notify", f"migrate_notify_tibber_{service}" + ) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + # Simulate the users confirmed the repair flow + data = await process_repair_fix_flow(http_client, flow_id) + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + # Assert the issue is no longer present + assert not issue_registry.async_get_issue( + domain="notify", + issue_id=f"migrate_notify_tibber_{service}", + ) + assert len(issue_registry.issues) == 0 diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 60af4ae3d5b..21985afd7bf 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -67,7 +67,7 @@ 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'vesync', + 'translation_key': None, 'unique_id': 'air-purifier', 'unit_of_measurement': None, }), @@ -158,7 +158,7 @@ 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'vesync', + 'translation_key': None, 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', 'unit_of_measurement': None, }), @@ -256,7 +256,7 @@ 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'vesync', + 'translation_key': None, 'unique_id': '400s-purifier', 'unit_of_measurement': None, }), @@ -355,7 +355,7 @@ 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'vesync', + 'translation_key': None, 'unique_id': '600s-purifier', 'unit_of_measurement': 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()])) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 41af8af3f21..eb2a719eab8 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7501,7 +7501,6 @@ async def test_options_flow_config_entry( assert result["reason"] == "abort" -@pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"]) @pytest.mark.usefixtures("mock_integration_frame") @patch.object(frame, "_REPORTED_INTEGRATIONS", set()) async def test_options_flow_deprecated_config_entry_setter( @@ -7510,15 +7509,13 @@ async def test_options_flow_deprecated_config_entry_setter( caplog: pytest.LogCaptureFixture, ) -> None: """Test that setting config_entry explicitly still works.""" - original_entry = MockConfigEntry(domain="my_integration", data={}) + original_entry = MockConfigEntry(domain="hue", data={}) original_entry.add_to_hass(hass) mock_setup_entry = AsyncMock(return_value=True) - mock_integration( - hass, MockModule("my_integration", async_setup_entry=mock_setup_entry) - ) - mock_platform(hass, "my_integration.config_flow", None) + mock_integration(hass, MockModule("hue", async_setup_entry=mock_setup_entry)) + mock_platform(hass, "hue.config_flow", None) class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -7552,16 +7549,15 @@ async def test_options_flow_deprecated_config_entry_setter( return _OptionsFlow(config_entry) - with mock_config_flow("my_integration", TestFlow): + with mock_config_flow("hue", TestFlow): result = await hass.config_entries.options.async_init(original_entry.entry_id) options_flow = hass.config_entries.options._progress.get(result["flow_id"]) assert options_flow.config_entry is original_entry assert ( - "Detected that custom integration 'my_integration' sets option flow " - "config_entry explicitly, which is deprecated and will stop working " - "in 2025.12" in caplog.text + "Detected that integration 'hue' sets option flow config_entry explicitly, " + "which is deprecated and will stop working in 2025.12" in caplog.text ) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 609809a96e8..b07b96e0de7 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - UnitOfBloodGlucoseConcentration, UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, @@ -33,7 +32,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( BaseUnitConverter, - BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -61,7 +59,6 @@ INVALID_SYMBOL = "bob" _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) for converter in ( - BloodGlucoseConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -83,11 +80,6 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { # Dict containing all converters with a corresponding unit ratio. _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { - BloodGlucoseConcentrationConverter: ( - UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, - UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, - 18, - ), ConductivityConverter: ( UnitOfConductivity.MICROSIEMENS_PER_CM, UnitOfConductivity.MILLISIEMENS_PER_CM, @@ -138,20 +130,6 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo _CONVERTED_VALUE: dict[ type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] ] = { - BloodGlucoseConcentrationConverter: [ - ( - 90, - UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, - 5, - UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, - ), - ( - 1, - UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, - 18, - UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, - ), - ], ConductivityConverter: [ # Deprecated to deprecated (5, UnitOfConductivity.SIEMENS, 5e3, UnitOfConductivity.MILLISIEMENS), diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 12a7eca5f9d..8db3f49ab8e 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -494,6 +494,31 @@ def mock_integration_frame() -> Generator[Mock]: yield correct_frame +@pytest.mark.parametrize( + ("loader_class", "message"), + [ + (yaml.loader.SafeLoader, "'SafeLoader' instead of 'FastSafeLoader'"), + ( + yaml.loader.SafeLineLoader, + "'SafeLineLoader' instead of 'PythonSafeLoader'", + ), + ], +) +@pytest.mark.usefixtures("mock_integration_frame") +async def test_deprecated_loaders( + caplog: pytest.LogCaptureFixture, + loader_class: type, + message: str, +) -> None: + """Test instantiating the deprecated yaml loaders logs a warning.""" + with ( + pytest.raises(TypeError), + patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()), + ): + loader_class() + assert (f"Detected that integration 'hue' uses deprecated {message}") in caplog.text + + @pytest.mark.usefixtures("try_both_loaders") def test_string_annotated() -> None: """Test strings are annotated with file + line."""