From 6a81bf6f5eb03370c7a22c52c73c9ee2b5e9bdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 8 Aug 2025 11:40:04 +0200 Subject: [PATCH 01/15] Improve interface between Miele integration and pymiele library (#150214) --- homeassistant/components/miele/__init__.py | 4 +++- homeassistant/components/miele/coordinator.py | 5 ++--- homeassistant/components/miele/entity.py | 5 ++--- tests/components/miele/conftest.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 2c5c250aee7319..173865195dfe81 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from aiohttp import ClientError, ClientResponseError +from pymiele import MieleAPI from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -66,7 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo ) from err # Setup MieleAPI and coordinator for data fetch - coordinator = MieleDataUpdateCoordinator(hass, entry, auth) + api = MieleAPI(auth) + coordinator = MieleDataUpdateCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index d5de2d79cb9c4b..98f5c9f8b1cfd8 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -8,13 +8,12 @@ from datetime import timedelta import logging -from pymiele import MieleAction, MieleDevice +from pymiele import MieleAction, MieleAPI, MieleDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .api import AsyncConfigEntryAuth from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -43,7 +42,7 @@ def __init__( self, hass: HomeAssistant, config_entry: MieleConfigEntry, - api: AsyncConfigEntryAuth, + api: MieleAPI, ) -> None: """Initialize the Miele data coordinator.""" super().__init__( diff --git a/homeassistant/components/miele/entity.py b/homeassistant/components/miele/entity.py index 4c6e61f6ea5c80..57c10f6f7bd8cf 100644 --- a/homeassistant/components/miele/entity.py +++ b/homeassistant/components/miele/entity.py @@ -1,12 +1,11 @@ """Entity base class for the Miele integration.""" -from pymiele import MieleAction, MieleDevice +from pymiele import MieleAction, MieleAPI, MieleDevice from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .api import AsyncConfigEntryAuth from .const import DEVICE_TYPE_TAGS, DOMAIN, MANUFACTURER, MieleAppliance, StateStatus from .coordinator import MieleDataUpdateCoordinator @@ -57,7 +56,7 @@ def action(self) -> MieleAction: return self.coordinator.data.actions[self._device_id] @property - def api(self) -> AsyncConfigEntryAuth: + def api(self) -> MieleAPI: """Return the api object.""" return self.coordinator.api diff --git a/tests/components/miele/conftest.py b/tests/components/miele/conftest.py index d91485ffc590e1..c8a47eb2b59efb 100644 --- a/tests/components/miele/conftest.py +++ b/tests/components/miele/conftest.py @@ -117,7 +117,7 @@ def mock_miele_client( """Mock a Miele client.""" with patch( - "homeassistant.components.miele.AsyncConfigEntryAuth", + "homeassistant.components.miele.MieleAPI", autospec=True, ) as mock_client: client = mock_client.return_value From 5b046def8e6c332f3e21494acd65bb6f56e4eb3b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 8 Aug 2025 12:02:07 +0200 Subject: [PATCH 02/15] Move holiday object to runtime data in workday (#149122) --- homeassistant/components/workday/__init__.py | 133 ++++----- .../components/workday/binary_sensor.py | 215 +-------------- homeassistant/components/workday/util.py | 254 ++++++++++++++++++ 3 files changed, 312 insertions(+), 290 deletions(-) create mode 100644 homeassistant/components/workday/util.py diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 0df4224a4ca9dd..cbcf12cf31c6d9 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -2,103 +2,72 @@ from __future__ import annotations -from functools import partial +from datetime import timedelta +from typing import cast -from holidays import HolidayBase, country_holidays +from holidays import DateLike, HolidayBase from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.setup import SetupPhases, async_pause_setup - -from .const import CONF_PROVINCE, DOMAIN, PLATFORMS - - -async def _async_validate_country_and_province( - hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None -) -> None: - """Validate country and province.""" - - if not country: - return - try: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - await hass.async_add_import_executor_job(country_holidays, country) - except NotImplementedError as ex: - async_create_issue( - hass, - DOMAIN, - "bad_country", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.ERROR, - translation_key="bad_country", - translation_placeholders={"title": entry.title}, - data={"entry_id": entry.entry_id, "country": None}, - ) - raise ConfigEntryError(f"Selected country {country} is not valid") from ex - - if not province: - return - try: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - await hass.async_add_import_executor_job( - partial(country_holidays, country, subdiv=province) - ) - except NotImplementedError as ex: - async_create_issue( - hass, - DOMAIN, - "bad_province", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.ERROR, - translation_key="bad_province", - translation_placeholders={ - CONF_COUNTRY: country, - "title": entry.title, - }, - data={"entry_id": entry.entry_id, "country": country}, - ) - raise ConfigEntryError( - f"Selected province {province} for country {country} is not valid" - ) from ex - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +from homeassistant.util import dt as dt_util + +from .const import ( + CONF_ADD_HOLIDAYS, + CONF_CATEGORY, + CONF_OFFSET, + CONF_PROVINCE, + CONF_REMOVE_HOLIDAYS, + LOGGER, + PLATFORMS, +) +from .util import ( + add_remove_custom_holidays, + async_validate_country_and_province, + get_holidays_object, + validate_dates, +) + +type WorkdayConfigEntry = ConfigEntry[HolidayBase] + + +async def async_setup_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> bool: """Set up Workday from a config entry.""" + calc_add_holidays = cast( + list[DateLike], validate_dates(entry.options[CONF_ADD_HOLIDAYS]) + ) + calc_remove_holidays: list[str] = validate_dates( + entry.options[CONF_REMOVE_HOLIDAYS] + ) + categories: list[str] | None = entry.options.get(CONF_CATEGORY) country: str | None = entry.options.get(CONF_COUNTRY) + days_offset: int = int(entry.options[CONF_OFFSET]) + language: str | None = entry.options.get(CONF_LANGUAGE) province: str | None = entry.options.get(CONF_PROVINCE) + year: int = (dt_util.now() + timedelta(days=days_offset)).year - await _async_validate_country_and_province(hass, entry, country, province) - - if country and CONF_LANGUAGE not in entry.options: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - cls: HolidayBase = await hass.async_add_import_executor_job( - partial(country_holidays, country, subdiv=province) - ) - default_language = cls.default_language - new_options = entry.options.copy() - new_options[CONF_LANGUAGE] = default_language - hass.config_entries.async_update_entry(entry, options=new_options) + await async_validate_country_and_province(hass, entry, country, province) + + entry.runtime_data = await hass.async_add_executor_job( + get_holidays_object, country, province, year, language, categories + ) + + add_remove_custom_holidays( + hass, entry, country, calc_add_holidays, calc_remove_holidays + ) + + LOGGER.debug("Found the following holidays for your configuration:") + for holiday_date, name in sorted(entry.runtime_data.items()): + # Make explicit str variable to avoid "Incompatible types in assignment" + _holiday_string = holiday_date.strftime("%Y-%m-%d") + LOGGER.debug("%s %s", _holiday_string, name) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> bool: """Unload Workday config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index a48e19e59b2e59..dcda7b901a120f 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -5,17 +5,11 @@ from datetime import date, datetime, timedelta from typing import Final -from holidays import ( - PUBLIC, - HolidayBase, - __version__ as python_holidays_version, - country_holidays, -) +from holidays import HolidayBase, __version__ as python_holidays_version import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME +from homeassistant.const import CONF_NAME from homeassistant.core import ( CALLBACK_TYPE, HomeAssistant, @@ -30,221 +24,26 @@ async_get_current_platform, ) from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.util import dt as dt_util, slugify +from homeassistant.util import dt as dt_util -from .const import ( - ALLOWED_DAYS, - CONF_ADD_HOLIDAYS, - CONF_CATEGORY, - CONF_EXCLUDES, - CONF_OFFSET, - CONF_PROVINCE, - CONF_REMOVE_HOLIDAYS, - CONF_WORKDAYS, - DOMAIN, - LOGGER, -) +from . import WorkdayConfigEntry +from .const import ALLOWED_DAYS, CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS, DOMAIN SERVICE_CHECK_DATE: Final = "check_date" CHECK_DATE: Final = "check_date" -def validate_dates(holiday_list: list[str]) -> list[str]: - """Validate and adds to list of dates to add or remove.""" - calc_holidays: list[str] = [] - for add_date in holiday_list: - if add_date.find(",") > 0: - dates = add_date.split(",", maxsplit=1) - d1 = dt_util.parse_date(dates[0]) - d2 = dt_util.parse_date(dates[1]) - if d1 is None or d2 is None: - LOGGER.error("Incorrect dates in date range: %s", add_date) - continue - _range: timedelta = d2 - d1 - for i in range(_range.days + 1): - day: date = d1 + timedelta(days=i) - calc_holidays.append(day.strftime("%Y-%m-%d")) - continue - calc_holidays.append(add_date) - return calc_holidays - - -def _get_obj_holidays( - country: str | None, - province: str | None, - year: int, - language: str | None, - categories: list[str] | None, -) -> HolidayBase: - """Get the object for the requested country and year.""" - if not country: - return HolidayBase() - - set_categories = None - if categories: - category_list = [PUBLIC] - category_list.extend(categories) - set_categories = tuple(category_list) - - obj_holidays: HolidayBase = country_holidays( - country, - subdiv=province, - years=[year, year + 1], - language=language, - categories=set_categories, - ) - - supported_languages = obj_holidays.supported_languages - default_language = obj_holidays.default_language - - if default_language and not language: - # If no language is set, use the default language - LOGGER.debug("Changing language from None to %s", default_language) - return country_holidays( # Return default if no language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - - if ( - default_language - and language - and language not in supported_languages - and language.startswith("en") - ): - # If language does not match supported languages, use the first English variant - if default_language.startswith("en"): - LOGGER.debug("Changing language from %s to %s", language, default_language) - return country_holidays( # Return default English if default language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - for lang in supported_languages: - if lang.startswith("en"): - LOGGER.debug("Changing language from %s to %s", language, lang) - return country_holidays( - country, - subdiv=province, - years=year, - language=lang, - categories=set_categories, - ) - - if default_language and language and language not in supported_languages: - # If language does not match supported languages, use the default language - LOGGER.debug("Changing language from %s to %s", language, default_language) - return country_holidays( # Return default English if default language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - - return obj_holidays - - async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: WorkdayConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Workday sensor.""" - add_holidays: list[str] = entry.options[CONF_ADD_HOLIDAYS] - remove_holidays: list[str] = entry.options[CONF_REMOVE_HOLIDAYS] - country: str | None = entry.options.get(CONF_COUNTRY) days_offset: int = int(entry.options[CONF_OFFSET]) excludes: list[str] = entry.options[CONF_EXCLUDES] - province: str | None = entry.options.get(CONF_PROVINCE) sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] - language: str | None = entry.options.get(CONF_LANGUAGE) - categories: list[str] | None = entry.options.get(CONF_CATEGORY) - - year: int = (dt_util.now() + timedelta(days=days_offset)).year - obj_holidays: HolidayBase = await hass.async_add_executor_job( - _get_obj_holidays, country, province, year, language, categories - ) - calc_add_holidays: list[str] = validate_dates(add_holidays) - calc_remove_holidays: list[str] = validate_dates(remove_holidays) - next_year = dt_util.now().year + 1 - - # Add custom holidays - try: - obj_holidays.append(calc_add_holidays) # type: ignore[arg-type] - except ValueError as error: - LOGGER.error("Could not add custom holidays: %s", error) - - # Remove holidays - for remove_holiday in calc_remove_holidays: - try: - # is this formatted as a date? - if dt_util.parse_date(remove_holiday): - # remove holiday by date - removed = obj_holidays.pop(remove_holiday) - LOGGER.debug("Removed %s", remove_holiday) - else: - # remove holiday by name - LOGGER.debug("Treating '%s' as named holiday", remove_holiday) - removed = obj_holidays.pop_named(remove_holiday) - for holiday in removed: - LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) - except KeyError as unmatched: - LOGGER.warning("No holiday found matching %s", unmatched) - if _date := dt_util.parse_date(remove_holiday): - if _date.year <= next_year: - # Only check and raise issues for current and next year - async_create_issue( - hass, - DOMAIN, - f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_date_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_named_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - - LOGGER.debug("Found the following holidays for your configuration:") - for holiday_date, name in sorted(obj_holidays.items()): - # Make explicit str variable to avoid "Incompatible types in assignment" - _holiday_string = holiday_date.strftime("%Y-%m-%d") - LOGGER.debug("%s %s", _holiday_string, name) + obj_holidays = entry.runtime_data platform = async_get_current_platform() platform.async_register_entity_service( diff --git a/homeassistant/components/workday/util.py b/homeassistant/components/workday/util.py new file mode 100644 index 00000000000000..726563febafb9a --- /dev/null +++ b/homeassistant/components/workday/util.py @@ -0,0 +1,254 @@ +"""Helpers functions for the Workday component.""" + +from datetime import date, timedelta +from functools import partial +from typing import TYPE_CHECKING + +from holidays import PUBLIC, DateLike, HolidayBase, country_holidays + +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.setup import SetupPhases, async_pause_setup +from homeassistant.util import dt as dt_util, slugify + +if TYPE_CHECKING: + from . import WorkdayConfigEntry +from .const import CONF_REMOVE_HOLIDAYS, DOMAIN, LOGGER + + +async def async_validate_country_and_province( + hass: HomeAssistant, + entry: "WorkdayConfigEntry", + country: str | None, + province: str | None, +) -> None: + """Validate country and province.""" + + if not country: + return + try: + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job(country_holidays, country) + except NotImplementedError as ex: + async_create_issue( + hass, + DOMAIN, + "bad_country", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="bad_country", + translation_placeholders={"title": entry.title}, + data={"entry_id": entry.entry_id, "country": None}, + ) + raise ConfigEntryError(f"Selected country {country} is not valid") from ex + + if not province: + return + try: + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) + except NotImplementedError as ex: + async_create_issue( + hass, + DOMAIN, + "bad_province", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="bad_province", + translation_placeholders={ + CONF_COUNTRY: country, + "title": entry.title, + }, + data={"entry_id": entry.entry_id, "country": country}, + ) + raise ConfigEntryError( + f"Selected province {province} for country {country} is not valid" + ) from ex + + +def validate_dates(holiday_list: list[str]) -> list[str]: + """Validate and add to list of dates to add or remove.""" + calc_holidays: list[str] = [] + for add_date in holiday_list: + if add_date.find(",") > 0: + dates = add_date.split(",", maxsplit=1) + d1 = dt_util.parse_date(dates[0]) + d2 = dt_util.parse_date(dates[1]) + if d1 is None or d2 is None: + LOGGER.error("Incorrect dates in date range: %s", add_date) + continue + _range: timedelta = d2 - d1 + for i in range(_range.days + 1): + day: date = d1 + timedelta(days=i) + calc_holidays.append(day.strftime("%Y-%m-%d")) + continue + calc_holidays.append(add_date) + return calc_holidays + + +def get_holidays_object( + country: str | None, + province: str | None, + year: int, + language: str | None, + categories: list[str] | None, +) -> HolidayBase: + """Get the object for the requested country and year.""" + if not country: + return HolidayBase() + + set_categories = None + if categories: + category_list = [PUBLIC] + category_list.extend(categories) + set_categories = tuple(category_list) + + obj_holidays: HolidayBase = country_holidays( + country, + subdiv=province, + years=[year, year + 1], + language=language, + categories=set_categories, + ) + + supported_languages = obj_holidays.supported_languages + default_language = obj_holidays.default_language + + if default_language and not language: + # If no language is set, use the default language + LOGGER.debug("Changing language from None to %s", default_language) + return country_holidays( # Return default if no language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + + if ( + default_language + and language + and language not in supported_languages + and language.startswith("en") + ): + # If language does not match supported languages, use the first English variant + if default_language.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + for lang in supported_languages: + if lang.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, lang) + return country_holidays( + country, + subdiv=province, + years=year, + language=lang, + categories=set_categories, + ) + + if default_language and language and language not in supported_languages: + # If language does not match supported languages, use the default language + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + + return obj_holidays + + +def add_remove_custom_holidays( + hass: HomeAssistant, + entry: "WorkdayConfigEntry", + country: str | None, + calc_add_holidays: list[DateLike], + calc_remove_holidays: list[str], +) -> None: + """Add or remove custom holidays.""" + next_year = dt_util.now().year + 1 + + # Add custom holidays + try: + entry.runtime_data.append(calc_add_holidays) + except ValueError as error: + LOGGER.error("Could not add custom holidays: %s", error) + + # Remove custom holidays + for remove_holiday in calc_remove_holidays: + try: + # is this formatted as a date? + if dt_util.parse_date(remove_holiday): + # remove holiday by date + removed = entry.runtime_data.pop(remove_holiday) + LOGGER.debug("Removed %s", remove_holiday) + else: + # remove holiday by name + LOGGER.debug("Treating '%s' as named holiday", remove_holiday) + removed = entry.runtime_data.pop_named(remove_holiday) + for holiday in removed: + LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) + except KeyError as unmatched: + LOGGER.warning("No holiday found matching %s", unmatched) + if _date := dt_util.parse_date(remove_holiday): + if _date.year <= next_year: + # Only check and raise issues for max next year + async_create_issue( + hass, + DOMAIN, + f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_date_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_named_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) From 8e12d2028d7b731f5510b46ec5f8969585a532cc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 8 Aug 2025 13:09:01 +0200 Subject: [PATCH 03/15] Remove previously deprecated linear_garage_door (#150109) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .strict-typing | 1 - CODEOWNERS | 2 - .../components/linear_garage_door/__init__.py | 54 ---- .../linear_garage_door/config_flow.py | 160 ------------ .../components/linear_garage_door/const.py | 3 - .../linear_garage_door/coordinator.py | 86 ------- .../components/linear_garage_door/cover.py | 85 ------- .../linear_garage_door/diagnostics.py | 29 --- .../components/linear_garage_door/entity.py | 43 ---- .../components/linear_garage_door/light.py | 78 ------ .../linear_garage_door/manifest.json | 9 - .../linear_garage_door/strings.json | 33 --- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - mypy.ini | 10 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../components/linear_garage_door/__init__.py | 22 -- .../components/linear_garage_door/conftest.py | 67 ----- .../fixtures/get_device_state.json | 42 ---- .../fixtures/get_device_state_1.json | 42 ---- .../fixtures/get_devices.json | 22 -- .../fixtures/get_sites.json | 1 - .../snapshots/test_cover.ambr | 201 --------------- .../snapshots/test_diagnostics.ambr | 83 ------- .../snapshots/test_light.ambr | 233 ------------------ .../linear_garage_door/test_config_flow.py | 132 ---------- .../linear_garage_door/test_cover.py | 120 --------- .../linear_garage_door/test_diagnostics.py | 29 --- .../linear_garage_door/test_init.py | 135 ---------- .../linear_garage_door/test_light.py | 126 ---------- 31 files changed, 1861 deletions(-) delete mode 100644 homeassistant/components/linear_garage_door/__init__.py delete mode 100644 homeassistant/components/linear_garage_door/config_flow.py delete mode 100644 homeassistant/components/linear_garage_door/const.py delete mode 100644 homeassistant/components/linear_garage_door/coordinator.py delete mode 100644 homeassistant/components/linear_garage_door/cover.py delete mode 100644 homeassistant/components/linear_garage_door/diagnostics.py delete mode 100644 homeassistant/components/linear_garage_door/entity.py delete mode 100644 homeassistant/components/linear_garage_door/light.py delete mode 100644 homeassistant/components/linear_garage_door/manifest.json delete mode 100644 homeassistant/components/linear_garage_door/strings.json delete mode 100644 tests/components/linear_garage_door/__init__.py delete mode 100644 tests/components/linear_garage_door/conftest.py delete mode 100644 tests/components/linear_garage_door/fixtures/get_device_state.json delete mode 100644 tests/components/linear_garage_door/fixtures/get_device_state_1.json delete mode 100644 tests/components/linear_garage_door/fixtures/get_devices.json delete mode 100644 tests/components/linear_garage_door/fixtures/get_sites.json delete mode 100644 tests/components/linear_garage_door/snapshots/test_cover.ambr delete mode 100644 tests/components/linear_garage_door/snapshots/test_diagnostics.ambr delete mode 100644 tests/components/linear_garage_door/snapshots/test_light.ambr delete mode 100644 tests/components/linear_garage_door/test_config_flow.py delete mode 100644 tests/components/linear_garage_door/test_cover.py delete mode 100644 tests/components/linear_garage_door/test_diagnostics.py delete mode 100644 tests/components/linear_garage_door/test_init.py delete mode 100644 tests/components/linear_garage_door/test_light.py diff --git a/.strict-typing b/.strict-typing index c125e85bbfc4ce..98973c89a5a7b4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -310,7 +310,6 @@ homeassistant.components.letpot.* homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* -homeassistant.components.linear_garage_door.* homeassistant.components.linkplay.* homeassistant.components.litejet.* homeassistant.components.litterrobot.* diff --git a/CODEOWNERS b/CODEOWNERS index 84a07305d366fe..d52349d49e816a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -862,8 +862,6 @@ build.json @home-assistant/supervisor /tests/components/lifx/ @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core -/homeassistant/components/linear_garage_door/ @IceBotYT -/tests/components/linear_garage_door/ @IceBotYT /homeassistant/components/linkplay/ @Velleman /tests/components/linkplay/ @Velleman /homeassistant/components/linux_battery/ @fabaff diff --git a/homeassistant/components/linear_garage_door/__init__.py b/homeassistant/components/linear_garage_door/__init__.py deleted file mode 100644 index a80aa99628b034..00000000000000 --- a/homeassistant/components/linear_garage_door/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -"""The Linear Garage Door integration.""" - -from __future__ import annotations - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from .const import DOMAIN -from .coordinator import LinearConfigEntry, LinearUpdateCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT] - - -async def async_setup_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> bool: - """Set up Linear Garage Door from a config entry.""" - - ir.async_create_issue( - hass, - DOMAIN, - DOMAIN, - breaks_in_ha_version="2025.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_integration", - translation_placeholders={ - "nice_go": "https://www.home-assistant.io/integrations/linear_garage_door", - "entries": "/config/integrations/integration/linear_garage_door", - }, - ) - - coordinator = LinearUpdateCoordinator(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: LinearConfigEntry) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_remove_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> None: - """Remove a config entry.""" - if not hass.config_entries.async_loaded_entries(DOMAIN): - ir.async_delete_issue(hass, DOMAIN, DOMAIN) - # Remove any remaining disabled or ignored entries - for _entry in hass.config_entries.async_entries(DOMAIN): - hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id)) diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py deleted file mode 100644 index 2cfd0af6a8f543..00000000000000 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Config flow for Linear Garage Door integration.""" - -from __future__ import annotations - -from collections.abc import Collection, Mapping, Sequence -import logging -from typing import Any -import uuid - -from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -STEP_USER_DATA_SCHEMA = { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, -} - - -async def validate_input( - hass: HomeAssistant, - data: dict[str, str], -) -> dict[str, Sequence[Collection[str]]]: - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - - hub = Linear() - - device_id = str(uuid.uuid4()) - try: - await hub.login( - data["email"], - data["password"], - device_id=device_id, - client_session=async_get_clientsession(hass), - ) - - sites = await hub.get_sites() - except InvalidLoginError as err: - raise InvalidAuth from err - finally: - await hub.close() - - return { - "email": data["email"], - "password": data["password"], - "sites": sites, - "device_id": device_id, - } - - -class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Linear Garage Door.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize the config flow.""" - self.data: dict[str, Sequence[Collection[str]]] = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" - data_schema = vol.Schema(STEP_USER_DATA_SCHEMA) - - if user_input is None: - return self.async_show_form(step_id="user", data_schema=data_schema) - - errors = {} - - try: - info = await validate_input(self.hass, user_input) - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - self.data = info - - # Check if we are reauthenticating - if self.source == SOURCE_REAUTH: - return self.async_update_reload_and_abort( - self._get_reauth_entry(), - data_updates={ - CONF_EMAIL: self.data["email"], - CONF_PASSWORD: self.data["password"], - }, - ) - - return await self.async_step_site() - - return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors - ) - - async def async_step_site( - self, - user_input: dict[str, Any] | None = None, - ) -> ConfigFlowResult: - """Handle the site step.""" - - if isinstance(self.data["sites"], list): - sites: list[dict[str, str]] = self.data["sites"] - - if not user_input: - return self.async_show_form( - step_id="site", - data_schema=vol.Schema( - { - vol.Required("site"): vol.In( - {site["id"]: site["name"] for site in sites} - ) - } - ), - ) - - site_id = user_input["site"] - - site_name = next(site["name"] for site in sites if site["id"] == site_id) - - await self.async_set_unique_id(site_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=site_name, - data={ - "site_id": site_id, - "email": self.data["email"], - "password": self.data["password"], - "device_id": self.data["device_id"], - }, - ) - - async def async_step_reauth( - self, entry_data: Mapping[str, Any] - ) -> ConfigFlowResult: - """Reauth in case of a password change or other error.""" - return await self.async_step_user() - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" - - -class InvalidDeviceID(HomeAssistantError): - """Error to indicate there is invalid device ID.""" diff --git a/homeassistant/components/linear_garage_door/const.py b/homeassistant/components/linear_garage_door/const.py deleted file mode 100644 index 7b3625c7c67e6f..00000000000000 --- a/homeassistant/components/linear_garage_door/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Linear Garage Door integration.""" - -DOMAIN = "linear_garage_door" diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py deleted file mode 100644 index 3844e1ae7debc6..00000000000000 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ /dev/null @@ -1,86 +0,0 @@ -"""DataUpdateCoordinator for Linear.""" - -from __future__ import annotations - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from datetime import timedelta -import logging -from typing import Any, cast - -from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - -type LinearConfigEntry = ConfigEntry[LinearUpdateCoordinator] - - -@dataclass -class LinearDevice: - """Linear device dataclass.""" - - name: str - subdevices: dict[str, dict[str, str]] - - -class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, LinearDevice]]): - """DataUpdateCoordinator for Linear.""" - - _devices: list[dict[str, Any]] | None = None - config_entry: LinearConfigEntry - - def __init__(self, hass: HomeAssistant, config_entry: LinearConfigEntry) -> None: - """Initialize DataUpdateCoordinator for Linear.""" - super().__init__( - hass, - _LOGGER, - config_entry=config_entry, - name="Linear Garage Door", - update_interval=timedelta(seconds=60), - ) - self.site_id = config_entry.data["site_id"] - - async def _async_update_data(self) -> dict[str, LinearDevice]: - """Get the data for Linear.""" - - async def update_data(linear: Linear) -> dict[str, Any]: - if not self._devices: - self._devices = await linear.get_devices(self.site_id) - - data = {} - - for device in self._devices: - device_id = str(device["id"]) - state = await linear.get_device_state(device_id) - data[device_id] = LinearDevice(cast(str, device["name"]), state) - return data - - return await self.execute(update_data) - - async def execute[_T](self, func: Callable[[Linear], Awaitable[_T]]) -> _T: - """Execute an API call.""" - linear = Linear() - try: - await linear.login( - email=self.config_entry.data["email"], - password=self.config_entry.data["password"], - device_id=self.config_entry.data["device_id"], - client_session=async_get_clientsession(self.hass), - ) - except InvalidLoginError as err: - if ( - str(err) - == "Login error: Login provided is invalid, please check the email and password" - ): - raise ConfigEntryAuthFailed from err - raise ConfigEntryNotReady from err - result = await func(linear) - await linear.close() - return result diff --git a/homeassistant/components/linear_garage_door/cover.py b/homeassistant/components/linear_garage_door/cover.py deleted file mode 100644 index 1f6c09995313cb..00000000000000 --- a/homeassistant/components/linear_garage_door/cover.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Cover entity for Linear Garage Doors.""" - -from datetime import timedelta -from typing import Any - -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .coordinator import LinearConfigEntry -from .entity import LinearEntity - -SUPPORTED_SUBDEVICES = ["GDO"] -PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(seconds=10) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: LinearConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Linear Garage Door cover.""" - coordinator = config_entry.runtime_data - - async_add_entities( - LinearCoverEntity(coordinator, device_id, device_data.name, sub_device_id) - for device_id, device_data in coordinator.data.items() - for sub_device_id in device_data.subdevices - if sub_device_id in SUPPORTED_SUBDEVICES - ) - - -class LinearCoverEntity(LinearEntity, CoverEntity): - """Representation of a Linear cover.""" - - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - _attr_device_class = CoverDeviceClass.GARAGE - - @property - def is_closed(self) -> bool: - """Return if cover is closed.""" - return self.sub_device.get("Open_B") == "false" - - @property - def is_opened(self) -> bool: - """Return if cover is open.""" - return self.sub_device.get("Open_B") == "true" - - @property - def is_opening(self) -> bool: - """Return if cover is opening.""" - return self.sub_device.get("Opening_P") == "0" - - @property - def is_closing(self) -> bool: - """Return if cover is closing.""" - return self.sub_device.get("Opening_P") == "100" - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the garage door.""" - if self.is_closed: - return - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Close" - ) - ) - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the garage door.""" - if self.is_opened: - return - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Open" - ) - ) diff --git a/homeassistant/components/linear_garage_door/diagnostics.py b/homeassistant/components/linear_garage_door/diagnostics.py deleted file mode 100644 index ff5ca5639bfa2e..00000000000000 --- a/homeassistant/components/linear_garage_door/diagnostics.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Diagnostics support for Linear Garage Door.""" - -from __future__ import annotations - -from dataclasses import asdict -from typing import Any - -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant - -from .coordinator import LinearConfigEntry - -TO_REDACT = {CONF_PASSWORD, CONF_EMAIL} - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: LinearConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - coordinator = entry.runtime_data - - return { - "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "coordinator_data": { - device_id: asdict(device_data) - for device_id, device_data in coordinator.data.items() - }, - } diff --git a/homeassistant/components/linear_garage_door/entity.py b/homeassistant/components/linear_garage_door/entity.py deleted file mode 100644 index a7adf95f82e37c..00000000000000 --- a/homeassistant/components/linear_garage_door/entity.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Base entity for Linear.""" - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import LinearDevice, LinearUpdateCoordinator - - -class LinearEntity(CoordinatorEntity[LinearUpdateCoordinator]): - """Common base for Linear entities.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: LinearUpdateCoordinator, - device_id: str, - device_name: str, - sub_device_id: str, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - - self._attr_unique_id = f"{device_id}-{sub_device_id}" - self._device_id = device_id - self._sub_device_id = sub_device_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - name=device_name, - manufacturer="Linear", - model="Garage Door Opener", - ) - - @property - def linear_device(self) -> LinearDevice: - """Return the Linear device.""" - return self.coordinator.data[self._device_id] - - @property - def sub_device(self) -> dict[str, str]: - """Return the subdevice.""" - return self.linear_device.subdevices[self._sub_device_id] diff --git a/homeassistant/components/linear_garage_door/light.py b/homeassistant/components/linear_garage_door/light.py deleted file mode 100644 index 59243817fbb07c..00000000000000 --- a/homeassistant/components/linear_garage_door/light.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Linear garage door light.""" - -from typing import Any - -from linear_garage_door import Linear - -from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .coordinator import LinearConfigEntry -from .entity import LinearEntity - -SUPPORTED_SUBDEVICES = ["Light"] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: LinearConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Linear Garage Door cover.""" - coordinator = config_entry.runtime_data - data = coordinator.data - - async_add_entities( - LinearLightEntity( - device_id=device_id, - device_name=data[device_id].name, - sub_device_id=subdev, - coordinator=coordinator, - ) - for device_id in data - for subdev in data[device_id].subdevices - if subdev in SUPPORTED_SUBDEVICES - ) - - -class LinearLightEntity(LinearEntity, LightEntity): - """Light for Linear devices.""" - - _attr_color_mode = ColorMode.BRIGHTNESS - _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - _attr_translation_key = "light" - - @property - def is_on(self) -> bool: - """Return if the light is on or not.""" - return bool(self.sub_device["On_B"] == "true") - - @property - def brightness(self) -> int | None: - """Return the brightness of the light.""" - return round(int(self.sub_device["On_P"]) / 100 * 255) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the light.""" - - async def _turn_on(linear: Linear) -> None: - """Turn on the light.""" - if not kwargs: - await linear.operate_device(self._device_id, self._sub_device_id, "On") - elif ATTR_BRIGHTNESS in kwargs: - brightness = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) - await linear.operate_device( - self._device_id, self._sub_device_id, f"DimPercent:{brightness}" - ) - - await self.coordinator.execute(_turn_on) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the light.""" - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Off" - ) - ) diff --git a/homeassistant/components/linear_garage_door/manifest.json b/homeassistant/components/linear_garage_door/manifest.json deleted file mode 100644 index f1eb4302cf046b..00000000000000 --- a/homeassistant/components/linear_garage_door/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "linear_garage_door", - "name": "Linear Garage Door", - "codeowners": ["@IceBotYT"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/linear_garage_door", - "iot_class": "cloud_polling", - "requirements": ["linear-garage-door==0.2.9"] -} diff --git a/homeassistant/components/linear_garage_door/strings.json b/homeassistant/components/linear_garage_door/strings.json deleted file mode 100644 index 40ffcf22e8d15b..00000000000000 --- a/homeassistant/components/linear_garage_door/strings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "entity": { - "light": { - "light": { - "name": "[%key:component::light::title%]" - } - } - }, - "issues": { - "deprecated_integration": { - "title": "The Linear Garage Door integration will be removed", - "description": "The Linear Garage Door integration will be removed as it has been replaced by the [Nice G.O.]({nice_go}) integration. Please migrate to the new integration.\n\nTo resolve this issue, please remove all Linear Garage Door entries from your configuration and add the new Nice G.O. integration. [Click here to see your existing Linear Garage Door integration entries]({entries})." - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5816a0ddbd97b5..8de75b21bba9f2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -348,7 +348,6 @@ "lg_thinq", "lidarr", "lifx", - "linear_garage_door", "linkplay", "litejet", "litterrobot", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c606d79f2c57a2..e9a8f46a496392 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3512,12 +3512,6 @@ "integration_type": "virtual", "supported_by": "idasen_desk" }, - "linear_garage_door": { - "name": "Linear Garage Door", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "linkedgo": { "name": "LinkedGo", "integration_type": "virtual", diff --git a/mypy.ini b/mypy.ini index 8482138cc45877..91c75beb64a9d8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2856,16 +2856,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.linear_garage_door.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.linkplay.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1e27ad4ded9fc9..c29a2c81b08e95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,9 +1363,6 @@ lightwave==0.24 # homeassistant.components.limitlessled limitlessled==1.1.3 -# homeassistant.components.linear_garage_door -linear-garage-door==0.2.9 - # homeassistant.components.linode linode-api==4.1.9b1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index caf90997fa57e4..dc27d14f1ad355 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1170,9 +1170,6 @@ librouteros==3.2.0 # homeassistant.components.soundtouch libsoundtouch==0.8 -# homeassistant.components.linear_garage_door -linear-garage-door==0.2.9 - # homeassistant.components.livisi livisi==0.0.25 diff --git a/tests/components/linear_garage_door/__init__.py b/tests/components/linear_garage_door/__init__.py deleted file mode 100644 index 67bd1ee2da295e..00000000000000 --- a/tests/components/linear_garage_door/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tests for the Linear Garage Door integration.""" - -from unittest.mock import patch - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration( - hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform] -) -> None: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.linear_garage_door.PLATFORMS", - platforms, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/linear_garage_door/conftest.py b/tests/components/linear_garage_door/conftest.py deleted file mode 100644 index 4ed7662e5d0c72..00000000000000 --- a/tests/components/linear_garage_door/conftest.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Common fixtures for the Linear Garage Door tests.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, patch - -import pytest - -from homeassistant.components.linear_garage_door import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - -from tests.common import ( - MockConfigEntry, - load_json_array_fixture, - load_json_object_fixture, -) - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.linear_garage_door.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_linear() -> Generator[AsyncMock]: - """Mock a Linear Garage Door client.""" - with ( - patch( - "homeassistant.components.linear_garage_door.coordinator.Linear", - autospec=True, - ) as mock_client, - patch( - "homeassistant.components.linear_garage_door.config_flow.Linear", - new=mock_client, - ), - ): - client = mock_client.return_value - client.login.return_value = True - client.get_devices.return_value = load_json_array_fixture( - "get_devices.json", DOMAIN - ) - client.get_sites.return_value = load_json_array_fixture( - "get_sites.json", DOMAIN - ) - device_states = load_json_object_fixture("get_device_state.json", DOMAIN) - client.get_device_state.side_effect = lambda device_id: device_states[device_id] - yield client - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Mock a config entry.""" - return MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201e", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) diff --git a/tests/components/linear_garage_door/fixtures/get_device_state.json b/tests/components/linear_garage_door/fixtures/get_device_state.json deleted file mode 100644 index 14247610e063d5..00000000000000 --- a/tests/components/linear_garage_door/fixtures/get_device_state.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "test1": { - "GDO": { - "Open_B": "true", - "Open_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - }, - "test2": { - "GDO": { - "Open_B": "false", - "Open_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test3": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test4": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - } -} diff --git a/tests/components/linear_garage_door/fixtures/get_device_state_1.json b/tests/components/linear_garage_door/fixtures/get_device_state_1.json deleted file mode 100644 index 1f41d4fd153488..00000000000000 --- a/tests/components/linear_garage_door/fixtures/get_device_state_1.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "test1": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test2": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - }, - "test3": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test4": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - } -} diff --git a/tests/components/linear_garage_door/fixtures/get_devices.json b/tests/components/linear_garage_door/fixtures/get_devices.json deleted file mode 100644 index da6eeaf7448bdb..00000000000000 --- a/tests/components/linear_garage_door/fixtures/get_devices.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "id": "test1", - "name": "Test Garage 1", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test2", - "name": "Test Garage 2", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test3", - "name": "Test Garage 3", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test4", - "name": "Test Garage 4", - "subdevices": ["GDO", "Light"] - } -] diff --git a/tests/components/linear_garage_door/fixtures/get_sites.json b/tests/components/linear_garage_door/fixtures/get_sites.json deleted file mode 100644 index 2b0a49b90078dc..00000000000000 --- a/tests/components/linear_garage_door/fixtures/get_sites.json +++ /dev/null @@ -1 +0,0 @@ -[{ "id": "test-site-id", "name": "test-site-name" }] diff --git a/tests/components/linear_garage_door/snapshots/test_cover.ambr b/tests/components/linear_garage_door/snapshots/test_cover.ambr deleted file mode 100644 index dc3df6684bcd3a..00000000000000 --- a/tests/components/linear_garage_door/snapshots/test_cover.ambr +++ /dev/null @@ -1,201 +0,0 @@ -# serializer version: 1 -# name: test_covers[cover.test_garage_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test1-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 1', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'open', - }) -# --- -# name: test_covers[cover.test_garage_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test2-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 2', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'closed', - }) -# --- -# name: test_covers[cover.test_garage_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test3-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 3', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'opening', - }) -# --- -# name: test_covers[cover.test_garage_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test4-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 4', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'closing', - }) -# --- diff --git a/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr b/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr deleted file mode 100644 index db82f41eb73b55..00000000000000 --- a/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,83 +0,0 @@ -# serializer version: 1 -# name: test_entry_diagnostics - dict({ - 'coordinator_data': dict({ - 'test1': dict({ - 'name': 'Test Garage 1', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'true', - 'Open_P': '100', - }), - 'Light': dict({ - 'On_B': 'true', - 'On_P': '100', - }), - }), - }), - 'test2': dict({ - 'name': 'Test Garage 2', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'false', - 'Open_P': '0', - }), - 'Light': dict({ - 'On_B': 'false', - 'On_P': '0', - }), - }), - }), - 'test3': dict({ - 'name': 'Test Garage 3', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'false', - 'Opening_P': '0', - }), - 'Light': dict({ - 'On_B': 'false', - 'On_P': '0', - }), - }), - }), - 'test4': dict({ - 'name': 'Test Garage 4', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'true', - 'Opening_P': '100', - }), - 'Light': dict({ - 'On_B': 'true', - 'On_P': '100', - }), - }), - }), - }), - 'entry': dict({ - 'data': dict({ - 'device_id': 'test-uuid', - 'email': '**REDACTED**', - 'password': '**REDACTED**', - 'site_id': 'test-site-id', - }), - 'disabled_by': None, - 'discovery_keys': dict({ - }), - 'domain': 'linear_garage_door', - 'entry_id': 'acefdd4b3a4a0911067d1cf51414201e', - 'minor_version': 1, - 'options': dict({ - }), - 'pref_disable_new_entities': False, - 'pref_disable_polling': False, - 'source': 'user', - 'subentries': list([ - ]), - 'title': 'test-site-name', - 'unique_id': None, - 'version': 1, - }), - }) -# --- diff --git a/tests/components/linear_garage_door/snapshots/test_light.ambr b/tests/components/linear_garage_door/snapshots/test_light.ambr deleted file mode 100644 index 930d78d4706563..00000000000000 --- a/tests/components/linear_garage_door/snapshots/test_light.ambr +++ /dev/null @@ -1,233 +0,0 @@ -# serializer version: 1 -# name: test_data[light.test_garage_1_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_1_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test1-Light', - 'unit_of_measurement': None, - }) -# --- -# name: test_data[light.test_garage_1_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Test Garage 1 Light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.test_garage_1_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_data[light.test_garage_2_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_2_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test2-Light', - 'unit_of_measurement': None, - }) -# --- -# name: test_data[light.test_garage_2_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Test Garage 2 Light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.test_garage_2_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_data[light.test_garage_3_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_3_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test3-Light', - 'unit_of_measurement': None, - }) -# --- -# name: test_data[light.test_garage_3_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Test Garage 3 Light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.test_garage_3_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_data[light.test_garage_4_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_4_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test4-Light', - 'unit_of_measurement': None, - }) -# --- -# name: test_data[light.test_garage_4_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Test Garage 4 Light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.test_garage_4_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py deleted file mode 100644 index 64bdc589194494..00000000000000 --- a/tests/components/linear_garage_door/test_config_flow.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Test the Linear Garage Door config flow.""" - -from unittest.mock import AsyncMock, patch - -from linear_garage_door.errors import InvalidLoginError -import pytest - -from homeassistant.components.linear_garage_door.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry - - -async def test_form( - hass: HomeAssistant, mock_linear: AsyncMock, mock_setup_entry: 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 not result["errors"] - - with patch( - "uuid.uuid4", - return_value="test-uuid", - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"site": "test-site-id"} - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-site-name" - assert result["data"] == { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_reauth( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_setup_entry: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test reauthentication.""" - mock_config_entry.add_to_hass(hass) - result = await mock_config_entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - with patch( - "uuid.uuid4", - return_value="test-uuid", - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "new-email", - CONF_PASSWORD: "new-password", - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert mock_config_entry.data == { - CONF_EMAIL: "new-email", - CONF_PASSWORD: "new-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } - - -@pytest.mark.parametrize( - ("side_effect", "expected_error"), - [(InvalidLoginError, "invalid_auth"), (Exception, "unknown")], -) -async def test_form_exceptions( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_setup_entry: AsyncMock, - side_effect: Exception, - expected_error: str, -) -> None: - """Test we handle invalid auth.""" - mock_linear.login.side_effect = side_effect - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": expected_error} - mock_linear.login.side_effect = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"site": "test-site-id"} - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py deleted file mode 100644 index c031db88180304..00000000000000 --- a/tests/components/linear_garage_door/test_cover.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Linear Garage Door cover.""" - -from datetime import timedelta -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.cover import ( - DOMAIN as COVER_DOMAIN, - SERVICE_CLOSE_COVER, - SERVICE_OPEN_COVER, - CoverState, -) -from homeassistant.components.linear_garage_door import DOMAIN -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, - async_fire_time_changed, - async_load_json_object_fixture, - snapshot_platform, -) - - -async def test_covers( - hass: HomeAssistant, - mock_linear: AsyncMock, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that data gets parsed and returned appropriately.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_open_cover( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that opening the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_1"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 0 - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_2"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_close_cover( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that closing the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_2"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 0 - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_1"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_update_cover_state( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test that closing the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - assert hass.states.get("cover.test_garage_1").state == CoverState.OPEN - assert hass.states.get("cover.test_garage_2").state == CoverState.CLOSED - - device_states = await async_load_json_object_fixture( - hass, "get_device_state_1.json", DOMAIN - ) - mock_linear.get_device_state.side_effect = lambda device_id: device_states[ - device_id - ] - - freezer.tick(timedelta(seconds=60)) - async_fire_time_changed(hass) - - assert hass.states.get("cover.test_garage_1").state == CoverState.CLOSING - assert hass.states.get("cover.test_garage_2").state == CoverState.OPENING diff --git a/tests/components/linear_garage_door/test_diagnostics.py b/tests/components/linear_garage_door/test_diagnostics.py deleted file mode 100644 index f51bb0a366c77a..00000000000000 --- a/tests/components/linear_garage_door/test_diagnostics.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test diagnostics of Linear Garage Door.""" - -from unittest.mock import AsyncMock - -from syrupy.assertion import SnapshotAssertion -from syrupy.filters import props - -from homeassistant.core import HomeAssistant - -from . import setup_integration - -from tests.common import MockConfigEntry -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - snapshot: SnapshotAssertion, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test config entry diagnostics.""" - await setup_integration(hass, mock_config_entry, []) - result = await get_diagnostics_for_config_entry( - hass, hass_client, mock_config_entry - ) - assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py deleted file mode 100644 index 2693eda60bb915..00000000000000 --- a/tests/components/linear_garage_door/test_init.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Test Linear Garage Door init.""" - -from unittest.mock import AsyncMock - -from linear_garage_door import InvalidLoginError -import pytest - -from homeassistant.components.linear_garage_door.const import DOMAIN -from homeassistant.config_entries import ( - SOURCE_IGNORE, - ConfigEntryDisabler, - ConfigEntryState, -) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from . import setup_integration - -from tests.common import MockConfigEntry - - -async def test_unload_entry( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test the unload entry.""" - - await setup_integration(hass, mock_config_entry, []) - 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( - ("side_effect", "entry_state"), - [ - ( - InvalidLoginError( - "Login provided is invalid, please check the email and password" - ), - ConfigEntryState.SETUP_ERROR, - ), - (InvalidLoginError("Invalid login"), ConfigEntryState.SETUP_RETRY), - ], -) -async def test_setup_failure( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - side_effect: Exception, - entry_state: ConfigEntryState, -) -> None: - """Test reauth trigger setup.""" - - mock_linear.login.side_effect = side_effect - - await setup_integration(hass, mock_config_entry, []) - assert mock_config_entry.state == entry_state - - -async def test_repair_issue( - hass: HomeAssistant, - mock_linear: AsyncMock, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the Linear Garage Door configuration entry loading/unloading handles the repair.""" - config_entry_1 = MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201e", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - await setup_integration(hass, config_entry_1, []) - assert config_entry_1.state is ConfigEntryState.LOADED - - # Add a second one - config_entry_2 = MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201f", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - await setup_integration(hass, config_entry_2, []) - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Add an ignored entry - config_entry_3 = MockConfigEntry( - source=SOURCE_IGNORE, - domain=DOMAIN, - ) - config_entry_3.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_3.entry_id) - await hass.async_block_till_done() - - assert config_entry_3.state is ConfigEntryState.NOT_LOADED - - # Add a disabled entry - config_entry_4 = MockConfigEntry( - disabled_by=ConfigEntryDisabler.USER, - domain=DOMAIN, - ) - config_entry_4.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_4.entry_id) - await hass.async_block_till_done() - - assert config_entry_4.state is ConfigEntryState.NOT_LOADED - - # Remove the first one - await hass.config_entries.async_remove(config_entry_1.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - # Remove the second one - await hass.config_entries.async_remove(config_entry_2.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.NOT_LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None - - # Check the ignored and disabled entries are removed - assert not hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/linear_garage_door/test_light.py b/tests/components/linear_garage_door/test_light.py deleted file mode 100644 index 1985b27aacdfd3..00000000000000 --- a/tests/components/linear_garage_door/test_light.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Test Linear Garage Door light.""" - -from datetime import timedelta -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.light import ( - DOMAIN as LIGHT_DOMAIN, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) -from homeassistant.components.linear_garage_door import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_BRIGHTNESS, - STATE_OFF, - STATE_ON, - 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, - async_load_json_object_fixture, - snapshot_platform, -) - - -async def test_data( - hass: HomeAssistant, - mock_linear: AsyncMock, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that data gets parsed and returned appropriately.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_turn_on( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning on the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_garage_2_light"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_turn_on_with_brightness( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning on the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_garage_2_light", CONF_BRIGHTNESS: 50}, - blocking=True, - ) - - mock_linear.operate_device.assert_called_once_with( - "test2", "Light", "DimPercent:20" - ) - - -async def test_turn_off( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning off the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_garage_1_light"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_update_light_state( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test that turning off the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - assert hass.states.get("light.test_garage_1_light").state == STATE_ON - assert hass.states.get("light.test_garage_2_light").state == STATE_OFF - - device_states = await async_load_json_object_fixture( - hass, "get_device_state_1.json", DOMAIN - ) - mock_linear.get_device_state.side_effect = lambda device_id: device_states[ - device_id - ] - - freezer.tick(timedelta(seconds=60)) - async_fire_time_changed(hass) - - assert hass.states.get("light.test_garage_1_light").state == STATE_OFF - assert hass.states.get("light.test_garage_2_light").state == STATE_ON From 4cb2af4d08e553cdd5818419ac8c5f2e89d56421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Fri, 8 Aug 2025 13:47:13 +0200 Subject: [PATCH 04/15] Add select platform to LetPot integration (#150212) --- homeassistant/components/letpot/__init__.py | 1 + homeassistant/components/letpot/icons.json | 17 ++ homeassistant/components/letpot/select.py | 163 +++++++++++++ homeassistant/components/letpot/strings.json | 23 ++ tests/components/letpot/__init__.py | 2 +- tests/components/letpot/conftest.py | 24 +- .../letpot/snapshots/test_select.ambr | 229 ++++++++++++++++++ tests/components/letpot/test_select.py | 102 ++++++++ 8 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/letpot/select.py create mode 100644 tests/components/letpot/snapshots/test_select.ambr create mode 100644 tests/components/letpot/test_select.py diff --git a/homeassistant/components/letpot/__init__.py b/homeassistant/components/letpot/__init__.py index 4b84a023675688..7bcb04b2b4d09a 100644 --- a/homeassistant/components/letpot/__init__.py +++ b/homeassistant/components/letpot/__init__.py @@ -25,6 +25,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.TIME, diff --git a/homeassistant/components/letpot/icons.json b/homeassistant/components/letpot/icons.json index 43541b571506ac..1f5e79b04dd17e 100644 --- a/homeassistant/components/letpot/icons.json +++ b/homeassistant/components/letpot/icons.json @@ -20,6 +20,23 @@ } } }, + "select": { + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "light_brightness": { + "default": "mdi:brightness-6", + "state": { + "high": "mdi:brightness-7" + } + }, + "light_mode": { + "default": "mdi:sprout", + "state": { + "flower": "mdi:flower" + } + } + }, "sensor": { "water_level": { "default": "mdi:water-percent" diff --git a/homeassistant/components/letpot/select.py b/homeassistant/components/letpot/select.py new file mode 100644 index 00000000000000..0a9f6b0704606e --- /dev/null +++ b/homeassistant/components/letpot/select.py @@ -0,0 +1,163 @@ +"""Support for LetPot select entities.""" + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from letpot.deviceclient import LetPotDeviceClient +from letpot.models import DeviceFeature, LightMode, TemperatureUnit + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator +from .entity import LetPotEntity, LetPotEntityDescription, exception_handler + +# Each change pushes a 'full' device status with the change. The library will cache +# pending changes to avoid overwriting, but try to avoid a lot of parallelism. +PARALLEL_UPDATES = 1 + + +class LightBrightnessLowHigh(StrEnum): + """Light brightness low/high model.""" + + LOW = "low" + HIGH = "high" + + +def _get_brightness_low_high_value(coordinator: LetPotDeviceCoordinator) -> str | None: + """Return brightness as low/high for a device which only has a low and high value.""" + brightness = coordinator.data.light_brightness + levels = coordinator.device_client.get_light_brightness_levels( + coordinator.device.serial_number + ) + return ( + LightBrightnessLowHigh.LOW.value + if levels[0] == brightness + else LightBrightnessLowHigh.HIGH.value + ) + + +async def _set_brightness_low_high_value( + device_client: LetPotDeviceClient, serial: str, option: str +) -> None: + """Set brightness from low/high for a device which only has a low and high value.""" + levels = device_client.get_light_brightness_levels(serial) + await device_client.set_light_brightness( + serial, levels[0] if option == LightBrightnessLowHigh.LOW.value else levels[1] + ) + + +@dataclass(frozen=True, kw_only=True) +class LetPotSelectEntityDescription(LetPotEntityDescription, SelectEntityDescription): + """Describes a LetPot select entity.""" + + value_fn: Callable[[LetPotDeviceCoordinator], str | None] + set_value_fn: Callable[[LetPotDeviceClient, str, str], Coroutine[Any, Any, None]] + + +SELECTORS: tuple[LetPotSelectEntityDescription, ...] = ( + LetPotSelectEntityDescription( + key="display_temperature_unit", + translation_key="display_temperature_unit", + options=[x.name.lower() for x in TemperatureUnit], + value_fn=( + lambda coordinator: coordinator.data.temperature_unit.name.lower() + if coordinator.data.temperature_unit is not None + else None + ), + set_value_fn=( + lambda device_client, serial, option: device_client.set_temperature_unit( + serial, TemperatureUnit[option.upper()] + ) + ), + supported_fn=( + lambda coordinator: DeviceFeature.TEMPERATURE_SET_UNIT + in coordinator.device_client.device_info( + coordinator.device.serial_number + ).features + ), + entity_category=EntityCategory.CONFIG, + ), + LetPotSelectEntityDescription( + key="light_brightness_low_high", + translation_key="light_brightness", + options=[ + LightBrightnessLowHigh.LOW.value, + LightBrightnessLowHigh.HIGH.value, + ], + value_fn=_get_brightness_low_high_value, + set_value_fn=_set_brightness_low_high_value, + supported_fn=( + lambda coordinator: DeviceFeature.LIGHT_BRIGHTNESS_LOW_HIGH + in coordinator.device_client.device_info( + coordinator.device.serial_number + ).features + ), + entity_category=EntityCategory.CONFIG, + ), + LetPotSelectEntityDescription( + key="light_mode", + translation_key="light_mode", + options=[x.name.lower() for x in LightMode], + value_fn=( + lambda coordinator: coordinator.data.light_mode.name.lower() + if coordinator.data.light_mode is not None + else None + ), + set_value_fn=( + lambda device_client, serial, option: device_client.set_light_mode( + serial, LightMode[option.upper()] + ) + ), + entity_category=EntityCategory.CONFIG, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LetPotConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up LetPot select entities based on a config entry and device status/features.""" + coordinators = entry.runtime_data + async_add_entities( + LetPotSelectEntity(coordinator, description) + for description in SELECTORS + for coordinator in coordinators + if description.supported_fn(coordinator) + ) + + +class LetPotSelectEntity(LetPotEntity, SelectEntity): + """Defines a LetPot select entity.""" + + entity_description: LetPotSelectEntityDescription + + def __init__( + self, + coordinator: LetPotDeviceCoordinator, + description: LetPotSelectEntityDescription, + ) -> None: + """Initialize LetPot select entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option.""" + return self.entity_description.value_fn(self.coordinator) + + @exception_handler + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + return await self.entity_description.set_value_fn( + self.coordinator.device_client, + self.coordinator.device.serial_number, + option, + ) diff --git a/homeassistant/components/letpot/strings.json b/homeassistant/components/letpot/strings.json index cdc5a36a15f58e..6ebd79edf5d777 100644 --- a/homeassistant/components/letpot/strings.json +++ b/homeassistant/components/letpot/strings.json @@ -49,6 +49,29 @@ "name": "Refill error" } }, + "select": { + "display_temperature_unit": { + "name": "Temperature unit on display", + "state": { + "celsius": "Celsius", + "fahrenheit": "Fahrenheit" + } + }, + "light_brightness": { + "name": "Light brightness", + "state": { + "low": "[%key:common::state::low%]", + "high": "[%key:common::state::high%]" + } + }, + "light_mode": { + "name": "Light mode", + "state": { + "flower": "Fruits/Flowers", + "vegetable": "Veggies/Herbs" + } + } + }, "sensor": { "water_level": { "name": "Water level" diff --git a/tests/components/letpot/__init__.py b/tests/components/letpot/__init__.py index d8be422899a230..644b8e1580f14a 100644 --- a/tests/components/letpot/__init__.py +++ b/tests/components/letpot/__init__.py @@ -33,7 +33,7 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) MAX_STATUS = LetPotDeviceStatus( errors=LetPotDeviceErrors(low_water=True, low_nutrients=False, refill_error=False), - light_brightness=500, + light_brightness=750, light_mode=LightMode.VEGETABLE, light_schedule_end=datetime.time(18, 0), light_schedule_start=datetime.time(8, 0), diff --git a/tests/components/letpot/conftest.py b/tests/components/letpot/conftest.py index 03ce2ec4a0d237..4abf917cb9ffc4 100644 --- a/tests/components/letpot/conftest.py +++ b/tests/components/letpot/conftest.py @@ -49,6 +49,16 @@ def _mock_device_features(device_type: str) -> DeviceFeature: | DeviceFeature.LIGHT_BRIGHTNESS_LOW_HIGH | DeviceFeature.PUMP_STATUS ) + if device_type == "LPH62": + return ( + DeviceFeature.CATEGORY_HYDROPONIC_GARDEN + | DeviceFeature.LIGHT_BRIGHTNESS_LEVELS + | DeviceFeature.NUTRIENT_BUTTON + | DeviceFeature.PUMP_AUTO + | DeviceFeature.TEMPERATURE + | DeviceFeature.TEMPERATURE_SET_UNIT + | DeviceFeature.WATER_LEVEL + ) if device_type == "LPH63": return ( DeviceFeature.CATEGORY_HYDROPONIC_GARDEN @@ -66,11 +76,20 @@ def _mock_device_status(device_type: str) -> LetPotDeviceStatus: """Return mock device status for the given type.""" if device_type == "LPH31": return SE_STATUS - if device_type == "LPH63": + if device_type in {"LPH62", "LPH63"}: return MAX_STATUS raise ValueError(f"No mock data for device type {device_type}") +def _mock_light_brightness_levels(device_type: str) -> list[int]: + """Return mock brightness levels for the given type.""" + if device_type == "LPH31": + return [500, 1000] + if device_type in {"LPH62", "LPH63"}: + return [125, 250, 375, 500, 625, 750, 875, 1000] + raise ValueError(f"No mock data for device type {device_type}") + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" @@ -134,6 +153,9 @@ def get_current_status_side_effect(serial: str) -> LetPotDeviceStatus: device_client.device_info.side_effect = lambda serial: _mock_device_info( serial[:5] ) + device_client.get_light_brightness_levels.side_effect = ( + lambda serial: _mock_light_brightness_levels(serial[:5]) + ) device_client.get_current_status.side_effect = get_current_status_side_effect device_client.request_status_update.side_effect = request_status_side_effect device_client.subscribe.side_effect = subscribe_side_effect diff --git a/tests/components/letpot/snapshots/test_select.ambr b/tests/components/letpot/snapshots/test_select.ambr new file mode 100644 index 00000000000000..5d9ddf0d0d360a --- /dev/null +++ b/tests/components/letpot/snapshots/test_select.ambr @@ -0,0 +1,229 @@ +# serializer version: 1 +# name: test_all_entities[LPH31][select.garden_light_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light brightness', + 'platform': 'letpot', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_brightness', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_light_brightness_low_high', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH31][select.garden_light_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light brightness', + 'options': list([ + 'low', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.garden_light_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_all_entities[LPH31][select.garden_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'flower', + 'vegetable', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light mode', + 'platform': 'letpot', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_light_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH31][select.garden_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light mode', + 'options': list([ + 'flower', + 'vegetable', + ]), + }), + 'context': , + 'entity_id': 'select.garden_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'vegetable', + }) +# --- +# name: test_all_entities[LPH62][select.garden_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'flower', + 'vegetable', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light mode', + 'platform': 'letpot', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH62ABCD_light_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH62][select.garden_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light mode', + 'options': list([ + 'flower', + 'vegetable', + ]), + }), + 'context': , + 'entity_id': 'select.garden_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'vegetable', + }) +# --- +# name: test_all_entities[LPH62][select.garden_temperature_unit_on_display-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'fahrenheit', + 'celsius', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_temperature_unit_on_display', + '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': 'Temperature unit on display', + 'platform': 'letpot', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'display_temperature_unit', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH62ABCD_display_temperature_unit', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH62][select.garden_temperature_unit_on_display-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Temperature unit on display', + 'options': list([ + 'fahrenheit', + 'celsius', + ]), + }), + 'context': , + 'entity_id': 'select.garden_temperature_unit_on_display', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'celsius', + }) +# --- diff --git a/tests/components/letpot/test_select.py b/tests/components/letpot/test_select.py new file mode 100644 index 00000000000000..d576ca6fca67f6 --- /dev/null +++ b/tests/components/letpot/test_select.py @@ -0,0 +1,102 @@ +"""Test select entities for the LetPot integration.""" + +from unittest.mock import MagicMock, patch + +from letpot.exceptions import LetPotConnectionException, LetPotException +from letpot.models import LightMode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("device_type", ["LPH62", "LPH31"]) +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_type: str, +) -> None: + """Test switch entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("device_type", ["LPH31"]) +async def test_set_select( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + device_type: str, +) -> None: + """Test select entity set to value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.garden_light_brightness", + ATTR_OPTION: "high", + }, + blocking=True, + ) + + mock_device_client.set_light_brightness.assert_awaited_once_with( + f"{device_type}ABCD", 1000 + ) + + +@pytest.mark.parametrize( + ("exception", "user_error"), + [ + ( + LetPotConnectionException("Connection failed"), + "An error occurred while communicating with the LetPot device: Connection failed", + ), + ( + LetPotException("Random thing failed"), + "An unknown error occurred while communicating with the LetPot device: Random thing failed", + ), + ], +) +async def test_select_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + exception: Exception, + user_error: str, +) -> None: + """Test select entity exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_device_client.set_light_mode.side_effect = exception + + with pytest.raises(HomeAssistantError, match=user_error): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.garden_light_mode", + ATTR_OPTION: LightMode.FLOWER.name.lower(), + }, + blocking=True, + ) From 2948b1c58eeea94f2308d78eec685cd1faa1da61 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:56:44 +0200 Subject: [PATCH 05/15] Cleanup Tuya fixture files (#150190) --- .../tuya/fixtures/cl_3r8gc33pnqsxfe1g.json | 3 +-- .../components/tuya/fixtures/cl_cpbo62rn.json | 2 -- .../tuya/fixtures/cl_ebt12ypvexnixvtf.json | 3 +-- .../components/tuya/fixtures/cl_qqdxfdht.json | 2 -- .../components/tuya/fixtures/cl_zah67ekd.json | 1 - .../tuya/fixtures/clkg_nhyj64w2.json | 2 -- .../tuya/fixtures/co2bj_yrr3eiyiacm31ski.json | 2 -- .../tuya/fixtures/cs_ka2wfrdoogpvgzfi.json | 2 -- .../tuya/fixtures/cs_qhxmvae667uap4zh.json | 2 -- .../tuya/fixtures/cs_vmxuxszzjwp5smli.json | 2 -- .../tuya/fixtures/cs_zibqa9dutqyaxym2.json | 1 - .../tuya/fixtures/cwjwq_agwu93lr.json | 2 -- .../tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json | 2 -- .../tuya/fixtures/cwysj_z3rpyvznfcch99aa.json | 2 -- .../tuya/fixtures/cz_0g1fmqh6d5io7lcn.json | 2 -- .../tuya/fixtures/cz_2jxesipczks0kdct.json | 2 -- .../tuya/fixtures/cz_cuhokdii7ojyw8k2.json | 2 -- .../tuya/fixtures/cz_dntgh2ngvshfxpsz.json | 2 -- .../tuya/fixtures/cz_hj0a5c7ckzzexu8l.json | 2 -- .../tuya/fixtures/cz_t0a4hwsf8anfsadp.json | 2 -- .../tuya/fixtures/dc_l3bpgg8ibsagon4x.json | 2 -- .../tuya/fixtures/dj_8szt7whdvwpmxglk.json | 2 -- .../tuya/fixtures/dj_8y0aquaa8v6tho8w.json | 2 -- .../tuya/fixtures/dj_baf9tt9lb8t5uc7z.json | 2 -- .../tuya/fixtures/dj_d4g0fbsoaal841o6.json | 2 -- .../tuya/fixtures/dj_djnozmdyqyriow8z.json | 2 -- .../tuya/fixtures/dj_ekwolitfjhxn55js.json | 2 -- .../tuya/fixtures/dj_fuupmcr2mb1odkja.json | 2 -- .../tuya/fixtures/dj_hp6orhaqm6as3jnv.json | 2 -- .../tuya/fixtures/dj_hpc8ddyfv85haxa7.json | 2 -- .../tuya/fixtures/dj_iayz2jmtlipjnxj7.json | 2 -- .../tuya/fixtures/dj_idnfq7xbx8qewyoa.json | 2 -- .../tuya/fixtures/dj_ilddqqih3tucdk68.json | 2 -- .../tuya/fixtures/dj_j1bgp31cffutizub.json | 2 -- .../tuya/fixtures/dj_lmnt3uyltk1xffrt.json | 2 -- .../tuya/fixtures/dj_mki13ie507rlry4r.json | 2 -- .../tuya/fixtures/dj_nbumqpv8vz61enji.json | 2 -- .../tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json | 2 -- .../components/tuya/fixtures/dj_oe0cpnjg.json | 2 -- .../components/tuya/fixtures/dj_riwp3k79.json | 2 -- .../tuya/fixtures/dj_tmsloaroqavbucgn.json | 2 -- .../tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json | 2 -- .../tuya/fixtures/dj_vqwcnabamzrc2kab.json | 2 -- .../tuya/fixtures/dj_xokdfs6kh5ednakk.json | 2 -- .../tuya/fixtures/dj_zakhnlpdiu0ycdxn.json | 2 -- .../tuya/fixtures/dj_zav1pa32pyxray78.json | 2 -- .../tuya/fixtures/dj_zputiamzanuk6yky.json | 2 -- .../tuya/fixtures/dlq_kxdr6su0c55p7bbo.json | 2 -- .../tuya/fixtures/fs_g0ewlb1vmwqljzji.json | 2 -- .../tuya/fixtures/fs_ibytpo6fpnugft1c.json | 2 -- .../tuya/fixtures/gyd_lgekqfxdabipm3tn.json | 2 -- .../tuya/fixtures/hps_2aaelwxk.json | 2 -- .../tuya/fixtures/kg_gbm9ata1zrzaez4a.json | 2 -- .../tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json | 2 -- .../tuya/fixtures/kj_yrzylxax1qspdgpp.json | 2 -- .../tuya/fixtures/ks_j9fa8ahzac8uvlfl.json | 2 -- .../tuya/fixtures/kt_5wnlzekkstwcdsvm.json | 2 -- .../tuya/fixtures/ldcg_9kbbfeho.json | 2 -- .../tuya/fixtures/mal_gyitctrjj1kefxp2.json | 1 - .../tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json | 1 - .../tuya/fixtures/mzj_qavcakohisj5adyh.json | 2 -- .../tuya/fixtures/pc_t2afic7i3v1bwhfp.json | 2 -- .../tuya/fixtures/pc_trjopo1vdlt9q1tg.json | 2 -- .../tuya/fixtures/pir_3amxzozho9xp4mkh.json | 2 -- .../tuya/fixtures/pir_fcdjzz3s.json | 2 -- .../tuya/fixtures/pir_wqz93nrdomectyoz.json | 2 -- .../tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json | 2 -- .../tuya/fixtures/qxj_fsea1lat3vuktbt6.json | 2 -- .../tuya/fixtures/qxj_is2indt9nlth6esa.json | 2 -- .../tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json | 2 -- .../tuya/fixtures/sd_lr33znaodtyarrrz.json | 2 -- .../tuya/fixtures/sfkzq_o6dagifntoafakst.json | 2 -- .../tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json | 2 -- .../components/tuya/fixtures/sj_tgvtvdoc.json | 2 -- .../tuya/fixtures/sp_drezasavompxpcgm.json | 2 -- .../tuya/fixtures/sp_rjKXWRohlvOTyLBu.json | 2 -- .../tuya/fixtures/sp_sdd5f5f2dl5wydjf.json | 2 -- .../tuya/fixtures/tdq_1aegphq4yfd50e6b.json | 2 -- .../tuya/fixtures/tdq_9htyiowaf5rtdhrv.json | 2 -- .../tuya/fixtures/tdq_cq1p0nt0a4rixnex.json | 2 -- .../tuya/fixtures/tdq_nockvv2k39vbrxxk.json | 2 -- .../tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json | 2 -- .../tuya/fixtures/tdq_uoa3mayicscacseb.json | 2 -- .../tuya/fixtures/tyndj_pyakuuoc.json | 2 -- .../tuya/fixtures/wfcon_b25mh8sxawsgndck.json | 2 -- .../tuya/fixtures/wg2_nwxr8qcu4seltoro.json | 2 -- .../components/tuya/fixtures/wk_6kijc7nd.json | 2 -- .../components/tuya/fixtures/wk_aqoouq7x.json | 2 -- .../tuya/fixtures/wk_fi6dne5tu4t1nm6j.json | 2 -- .../tuya/fixtures/wk_gogb05wrtredz3bs.json | 2 -- .../tuya/fixtures/wk_y5obtqhuztqsf2mj.json | 2 -- .../tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json | 2 -- .../tuya/fixtures/ydkt_jevroj5aguwdbs2e.json | 2 -- .../tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json | 2 -- .../tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json | 2 -- .../tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json | 3 +-- .../tuya/fixtures/zndb_4ggkyflayu1h1ho9.json | 2 -- .../tuya/fixtures/zndb_ze8faryrxr0glqnn.json | 2 -- .../tuya/fixtures/zwjcy_myd45weu.json | 2 -- tests/components/tuya/test_init.py | 23 +++++++++++++++++-- 100 files changed, 24 insertions(+), 196 deletions(-) diff --git a/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json b/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json index de6c23a1c14e58..189938aa4f0afe 100644 --- a/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json +++ b/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json @@ -118,6 +118,5 @@ "countdown": "cancel", "countdown_left": 0, "time_total": 25400 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/cl_cpbo62rn.json b/tests/components/tuya/fixtures/cl_cpbo62rn.json index a5ed8e4b580bc9..b52bb31f588d08 100644 --- a/tests/components/tuya/fixtures/cl_cpbo62rn.json +++ b/tests/components/tuya/fixtures/cl_cpbo62rn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf216113c71bf01a18jtl0", "name": "blinds", "category": "cl", "product_id": "cpbo62rn", diff --git a/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json b/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json index 4b15a27bfd5afa..fd0ff1fb18179f 100644 --- a/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json +++ b/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json @@ -53,6 +53,5 @@ }, "status": { "percent_control": 0 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/cl_qqdxfdht.json b/tests/components/tuya/fixtures/cl_qqdxfdht.json index b8f568619dbe5d..c0a7bc1d0ba59e 100644 --- a/tests/components/tuya/fixtures/cl_qqdxfdht.json +++ b/tests/components/tuya/fixtures/cl_qqdxfdht.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb9c4958fd06d141djpqa", "name": "bedroom blinds", "category": "cl", "product_id": "qqdxfdht", diff --git a/tests/components/tuya/fixtures/cl_zah67ekd.json b/tests/components/tuya/fixtures/cl_zah67ekd.json index 14d1c39fc949e8..b1920f1ecc5915 100644 --- a/tests/components/tuya/fixtures/cl_zah67ekd.json +++ b/tests/components/tuya/fixtures/cl_zah67ekd.json @@ -1,5 +1,4 @@ { - "id": "zah67ekd", "name": "Kitchen Blinds", "category": "cl", "product_id": "zah67ekd", diff --git a/tests/components/tuya/fixtures/clkg_nhyj64w2.json b/tests/components/tuya/fixtures/clkg_nhyj64w2.json index 0f64bae778f5e1..1aa6ebebd2c9a9 100644 --- a/tests/components/tuya/fixtures/clkg_nhyj64w2.json +++ b/tests/components/tuya/fixtures/clkg_nhyj64w2.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf1fa053e0ba4e002c6we8", "name": "Tapparelle studio", "category": "clkg", "product_id": "nhyj64w2", diff --git a/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json index fb544fb7d5ec1d..c4657f300128dc 100644 --- a/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json +++ b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb14fd1dd93ca2ea34vpin", "name": "AQI", "category": "co2bj", "product_id": "yrr3eiyiacm31ski", diff --git a/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json index 755b46fa397f7f..2edd120cf8d625 100644 --- a/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json +++ b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Dehumidifer", "category": "cs", "product_id": "ka2wfrdoogpvgzfi", diff --git a/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json index 9b0b704e3dec3f..b11dfe8858214b 100644 --- a/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json +++ b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "28403630e8db84b7a963", "name": "DryFix", "category": "cs", "product_id": "qhxmvae667uap4zh", diff --git a/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json index 27d4e825ab16b5..f4d01c2bc911a5 100644 --- a/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json +++ b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Dehumidifier ", "category": "cs", "product_id": "vmxuxszzjwp5smli", diff --git a/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json index 5574153a439835..fbae30ad3eb124 100644 --- a/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json +++ b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json @@ -1,5 +1,4 @@ { - "id": "bf3fce6af592f12df3gbgq", "name": "Dehumidifier", "category": "cs", "product_id": "zibqa9dutqyaxym2", diff --git a/tests/components/tuya/fixtures/cwjwq_agwu93lr.json b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json index 84f769083384df..a421a69bf08799 100644 --- a/tests/components/tuya/fixtures/cwjwq_agwu93lr.json +++ b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf6574iutyikgwkx", "name": "Smart Odor Eliminator-Pro", "category": "cwjwq", "product_id": "agwu93lr", diff --git a/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json index 4bdd6f3167d128..e3858d376028a0 100644 --- a/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json +++ b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd0273e59494eb34esvrx", "name": "Cleverio PF100", "category": "cwwsq", "product_id": "wfkzyy0evslzsmoi", diff --git a/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json index 695da229041f5b..6f9a8391726b0d 100644 --- a/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json +++ b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "23536058083a8dc57d96", "name": "PIXI Smart Drinking Fountain", "category": "cwysj", "product_id": "z3rpyvznfcch99aa", diff --git a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json index 760972e7fb0e11..8301c806a71aea 100644 --- a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json +++ b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "01155072c4dd573f92b8", "name": "Apollo light", "category": "cz", "product_id": "0g1fmqh6d5io7lcn", diff --git a/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json index 27c3ae0c37f4b1..c8191f8a023d73 100644 --- a/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json +++ b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb0c772dabbb19d653ssi5", "name": "HVAC Meter", "category": "cz", "product_id": "2jxesipczks0kdct", diff --git a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json index f259ebd7d6cd60..8eaecf2407c0b6 100644 --- a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json +++ b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "53703774d8f15ba9efd3", "name": "Buitenverlichting", "category": "cz", "product_id": "cuhokdii7ojyw8k2", diff --git a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json index a92d2d370d006d..77e19d69a0ad8e 100644 --- a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json +++ b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf7a2cdaf3ce28d2f7uqnh", "name": "fakkel veranda ", "category": "cz", "product_id": "dntgh2ngvshfxpsz", diff --git a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json index 0638bb02d1e674..b40297eab8f17a 100644 --- a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json +++ b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "051724052462ab286504", "name": "droger", "category": "cz", "product_id": "hj0a5c7ckzzexu8l", diff --git a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json index b7f913a71532f0..04a2d12e853b7b 100644 --- a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json +++ b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf4c0c538bfe408aa9gr2e", "name": "wallwasher front", "category": "cz", "product_id": "t0a4hwsf8anfsadp", diff --git a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json index b37591786189d9..198a2462ad1676 100644 --- a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json +++ b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd9f45c6b882c9f46dxfc", "name": "LSC Party String Light RGBIC+CCT ", "category": "dc", "product_id": "l3bpgg8ibsagon4x", diff --git a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json index 6cd0ca55379736..8b6e491fa4329b 100644 --- a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json +++ b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb10549aadfc74b7c8q2ti", "name": "Porch light E", "category": "dj", "product_id": "8szt7whdvwpmxglk", diff --git a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json index ec8f6a0a4d5b90..d2e36e71f49384 100644 --- a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json +++ b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf71858c3d27943679dsx9", "name": "dressoir spot", "category": "dj", "product_id": "8y0aquaa8v6tho8w", diff --git a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json index 211c0bc12cf73f..86d1f8fd9d56ae 100644 --- a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json +++ b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "40611462e09806c73134", "name": "Pokerlamp 2", "category": "dj", "product_id": "baf9tt9lb8t5uc7z", diff --git a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json index 22650f7ae37518..024501d59deed1 100644 --- a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json +++ b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf671413db4cee1f9bqdcx", "name": "WC D1", "category": "dj", "product_id": "d4g0fbsoaal841o6", diff --git a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json index 67df13c674f219..d48e722856673e 100644 --- a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json +++ b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8885f3d18a73e395bfac", "name": "Fakkel 8", "category": "dj", "product_id": "djnozmdyqyriow8z", diff --git a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json index 90cad22fd09a43..ae3a53e606ea49 100644 --- a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json +++ b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb99bba00c9c90ba8gzgl", "name": "ab6", "category": "dj", "product_id": "ekwolitfjhxn55js", diff --git a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json index 5b189b6a3e4a22..39cb6b78460606 100644 --- a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json +++ b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0914a82b06ecf151xsf5", "name": "Slaapkamer", "category": "dj", "product_id": "fuupmcr2mb1odkja", diff --git a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json index e8166a192dc5d7..22e5eee1b6fcaf 100644 --- a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json +++ b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "00450321483fda81c529", "name": "Master bedroom TV lights", "category": "dj", "product_id": "hp6orhaqm6as3jnv", diff --git a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json index 893aafa37590fd..b7190caa78e427 100644 --- a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json +++ b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "63362034840d8eb9029f", "name": "Garage", "category": "dj", "product_id": "hpc8ddyfv85haxa7", diff --git a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json index f9062d9146de55..a8cddb4ee4f785 100644 --- a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json +++ b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0fc1d7d4caa71a59us7c", "name": "LED Porch 2", "category": "dj", "product_id": "iayz2jmtlipjnxj7", diff --git a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json index 295157d83701cd..299e8d573f1492 100644 --- a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json +++ b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf599f5cffe1a5985depyk", "name": "AB1", "category": "dj", "product_id": "idnfq7xbx8qewyoa", diff --git a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json index 1181b650f3eea2..affa875f3b4928 100644 --- a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json +++ b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "84178216d8f15be52dc4", "name": "Ieskas", "category": "dj", "product_id": "ilddqqih3tucdk68", diff --git a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json index d95179c921f9d6..01c7e37500248d 100644 --- a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json +++ b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfe49d7b6cd80536efdldi", "name": "Ceiling Portal", "category": "dj", "product_id": "j1bgp31cffutizub", diff --git a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json index 93a802a7ee33f7..54c08ba7762693 100644 --- a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json +++ b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "07608286600194e94248", "name": "DirectietKamer", "category": "dj", "product_id": "lmnt3uyltk1xffrt", diff --git a/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json index 49854adc889512..daea124e8e0dc8 100644 --- a/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json +++ b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "REDACTED", "name": "Garage light", "category": "dj", "product_id": "mki13ie507rlry4r", diff --git a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json index bc919dd92d2e44..3cac3935c27c8e 100644 --- a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json +++ b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf77c04cbd6a52a7be16ll", "name": "b2", "category": "dj", "product_id": "nbumqpv8vz61enji", diff --git a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json index c519f1aa593383..5fbea6fb287a1e 100644 --- a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json +++ b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "40350105dc4f229a464e", "name": "hall 💡 ", "category": "dj", "product_id": "nlxvjzy1hoeiqsg6", diff --git a/tests/components/tuya/fixtures/dj_oe0cpnjg.json b/tests/components/tuya/fixtures/dj_oe0cpnjg.json index 646ce8a93d731b..8c2a559a5c9feb 100644 --- a/tests/components/tuya/fixtures/dj_oe0cpnjg.json +++ b/tests/components/tuya/fixtures/dj_oe0cpnjg.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8d8af3ddfe75b0195r0h", "name": "Front right Lighting trap", "category": "dj", "product_id": "oe0cpnjg", diff --git a/tests/components/tuya/fixtures/dj_riwp3k79.json b/tests/components/tuya/fixtures/dj_riwp3k79.json index f1a3579e6607e1..bd4d013ab5b0d4 100644 --- a/tests/components/tuya/fixtures/dj_riwp3k79.json +++ b/tests/components/tuya/fixtures/dj_riwp3k79.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf46b2b81ca41ce0c1xpsw", "name": "LED KEUKEN 2", "category": "dj", "product_id": "riwp3k79", diff --git a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json index 20c91ad77393e8..91c4dff5a42059 100644 --- a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json +++ b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf252b8ee16b2e78bdoxlp", "name": "Pokerlamp 1", "category": "dj", "product_id": "tmsloaroqavbucgn", diff --git a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json index 7ea5905411dd47..4b7a3a4e8797ec 100644 --- a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json +++ b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8edbd51a52c01a4bfgqf", "name": "Sjiethoes", "category": "dj", "product_id": "ufq2xwuzd4nb0qdr", diff --git a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json index 4d6749ea0b4e5e..9aa3646a11b5b4 100644 --- a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json +++ b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd56f4718874ee8830xdw", "name": "Strip 2", "category": "dj", "product_id": "vqwcnabamzrc2kab", diff --git a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json index cce66d90b0c467..2e339c64678169 100644 --- a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json +++ b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfc1ef4da4accc0731oggw", "name": "ERKER 1-Gold ", "category": "dj", "product_id": "xokdfs6kh5ednakk", diff --git a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json index d1c236631440a5..2a6b4f34ce77fc 100644 --- a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json +++ b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "03010850c44f33966362", "name": "Stoel", "category": "dj", "product_id": "zakhnlpdiu0ycdxn", diff --git a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json index 624f7fb4347f79..0ae793b3d1bf7a 100644 --- a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json +++ b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "500425642462ab50909b", "name": "Gengske 💡 ", "category": "dj", "product_id": "zav1pa32pyxray78", diff --git a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json index cede2b656821e0..b500c67d0ea627 100644 --- a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json +++ b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf74164049de868395pbci", "name": "Floodlight", "category": "dj", "product_id": "zputiamzanuk6yky", diff --git a/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json index 2652399bdcb20d..eaec5aed56c5e3 100644 --- a/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json +++ b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": null, "disabled_by": null, "disabled_polling": false, - "id": "bf5e5bde2c52cb5994cd27", "name": "Metering_3PN_WiFi_stable", "category": "dlq", "product_id": "kxdr6su0c55p7bbo", diff --git a/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json index 3aae03c904a26e..9a82643e2f92ce 100644 --- a/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json +++ b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "XXX", "name": "Ceiling Fan With Light", "category": "fs", "product_id": "g0ewlb1vmwqljzji", diff --git a/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json index 02b3808f84d15b..e8c59f50d7fa76 100644 --- a/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json +++ b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "10706550a4e57c88b93a", "name": "Ventilador Cama", "category": "fs", "product_id": "ibytpo6fpnugft1c", diff --git a/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json index ddfbce3ae11992..62723670973528 100644 --- a/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json +++ b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb3e988f33c233290cfs3l", "name": "Colorful PIR Night Light", "category": "gyd", "product_id": "lgekqfxdabipm3tn", diff --git a/tests/components/tuya/fixtures/hps_2aaelwxk.json b/tests/components/tuya/fixtures/hps_2aaelwxk.json index 4e5066e77f4a22..77c4ad47839a3a 100644 --- a/tests/components/tuya/fixtures/hps_2aaelwxk.json +++ b/tests/components/tuya/fixtures/hps_2aaelwxk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf78687ad321a3aeb8a73m", "name": "Human presence Office", "category": "hps", "product_id": "2aaelwxk", diff --git a/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json index a190161953b7f6..a61ebc5265911d 100644 --- a/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json +++ b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": true, - "id": "0665305284f3ebe9fdc1", "name": "QT-Switch", "category": "kg", "product_id": "gbm9ata1zrzaez4a", diff --git a/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json index 5758fce2152fb0..4e1481406245f5 100644 --- a/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json +++ b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "152027113c6105cce49c", "name": "HL400", "category": "kj", "product_id": "CAjWAxBUZt7QZHfz", diff --git a/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json index 642ef9686082ad..45015bff0ac227 100644 --- a/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json +++ b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "CENSORED", "name": "Bree", "category": "kj", "product_id": "yrzylxax1qspdgpp", diff --git a/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json index cb158a967b4cb3..b36064724afccf 100644 --- a/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json +++ b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Tower Fan CA-407G Smart", "category": "ks", "product_id": "j9fa8ahzac8uvlfl", diff --git a/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json index 5b29fd0a1913ce..3dd9c3713dc41b 100644 --- a/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json +++ b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Air Conditioner", "category": "kt", "product_id": "5wnlzekkstwcdsvm", diff --git a/tests/components/tuya/fixtures/ldcg_9kbbfeho.json b/tests/components/tuya/fixtures/ldcg_9kbbfeho.json index 223e39a00d42d1..6281085a06cdac 100644 --- a/tests/components/tuya/fixtures/ldcg_9kbbfeho.json +++ b/tests/components/tuya/fixtures/ldcg_9kbbfeho.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfbc8a692eaeeef455fkct", "name": "Luminosité", "category": "ldcg", "product_id": "9kbbfeho", diff --git a/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json index 1a25a84ec2ceb5..ee69a811a929a2 100644 --- a/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json +++ b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json @@ -1,5 +1,4 @@ { - "id": "123123aba12312312dazub", "name": "Multifunction alarm", "category": "mal", "product_id": "gyitctrjj1kefxp2", diff --git a/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json index c73b6c34878143..0e0a947aff7bf9 100644 --- a/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json +++ b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json @@ -6,7 +6,6 @@ "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf5cccf9027080e2dbb9w3", "name": "Door Garage ", "model": "", "category": "mcs", diff --git a/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json b/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json index 402e73c732b885..df6375a6827056 100644 --- a/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json +++ b/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff434eca843ffc9afmthv", "name": "Sous Vide", "category": "mzj", "product_id": "qavcakohisj5adyh", diff --git a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json index 4ed7ecf0373a64..aa16d5a91d8550 100644 --- a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json +++ b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf2206da15147500969d6e", "name": "Bubbelbad", "category": "pc", "product_id": "t2afic7i3v1bwhfp", diff --git a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json index 99929616ec7a5e..ddff6df21a15cf 100644 --- a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json +++ b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "15727703c4dd5709cd78", "name": "Terras", "category": "pc", "product_id": "trjopo1vdlt9q1tg", diff --git a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json index 98843da5614934..6e68b1a92dbebd 100644 --- a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json +++ b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "73486068483fda10d633", "name": "rat trap hedge", "category": "pir", "product_id": "3amxzozho9xp4mkh", diff --git a/tests/components/tuya/fixtures/pir_fcdjzz3s.json b/tests/components/tuya/fixtures/pir_fcdjzz3s.json index 65740a4106cb06..74f223ee7eacc4 100644 --- a/tests/components/tuya/fixtures/pir_fcdjzz3s.json +++ b/tests/components/tuya/fixtures/pir_fcdjzz3s.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf445324326cbde7c5rg7b", "name": "Motion sensor lidl zigbee", "category": "pir", "product_id": "fcdjzz3s", diff --git a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json index e4122ee5f9d08d..8bf85a1d3390c0 100644 --- a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json +++ b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "20401777500291cfe3a2", "name": "PIR outside stairs", "category": "pir", "product_id": "wqz93nrdomectyoz", diff --git a/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json index 6cae732aedf0ef..97c4a21526c1b7 100644 --- a/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json +++ b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf83514d9c14b426f0fz5y", "name": "AC charging control box", "category": "qccdz", "product_id": "7bvgooyjhiua1yyq", diff --git a/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json index c538630c542b1b..549e23cc9142a7 100644 --- a/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json +++ b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf84c743a84eb2c8abeurz", "name": "BR 7-in-1 WLAN Wetterstation Anthrazit", "category": "qxj", "product_id": "fsea1lat3vuktbt6", diff --git a/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json index efffe12a2f946d..93b3aa580a09d7 100644 --- a/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json +++ b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff00f6abe0563b284t77p", "name": "Frysen", "category": "qxj", "product_id": "is2indt9nlth6esa", diff --git a/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json index 24b4dbda5949c4..6516626d789fee 100644 --- a/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json +++ b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "ebb9d0eb5014f98cfboxbz", "name": "Gas sensor", "category": "rqbj", "product_id": "4iqe2hsfyd86kwwc", diff --git a/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json b/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json index 77d94cb951b127..ba461a6226d13c 100644 --- a/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json +++ b/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfa951ca98fcf64fddqlmt", "name": "V20", "category": "sd", "product_id": "lr33znaodtyarrrz", diff --git a/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json index e57e9274690285..30eff8b5c8bced 100644 --- a/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json +++ b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb9bfc18eeaed2d85yt5m", "name": "Sprinkler Cesare", "category": "sfkzq", "product_id": "o6dagifntoafakst", diff --git a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json index a3068983c874b9..b0fd9d38bdfa6a 100644 --- a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json +++ b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0984adfeffe10d5a3ofd", "name": "Siren veranda ", "category": "sgbj", "product_id": "ulv4nnue7gqp0rjk", diff --git a/tests/components/tuya/fixtures/sj_tgvtvdoc.json b/tests/components/tuya/fixtures/sj_tgvtvdoc.json index a63fd7af508d3d..bba2d80da88112 100644 --- a/tests/components/tuya/fixtures/sj_tgvtvdoc.json +++ b/tests/components/tuya/fixtures/sj_tgvtvdoc.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf58e095fd2d86d592tveh", "name": "Tournesol", "category": "sj", "product_id": "tgvtvdoc", diff --git a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json index a6543eac5eaadc..ed30e930e2bd87 100644 --- a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json +++ b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf7b8e59f8cd49f425mmfm", "name": "CAM GARAGE", "category": "sp", "product_id": "drezasavompxpcgm", diff --git a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json index 9a7bb9f1ecad4e..6825c67efc279e 100644 --- a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json +++ b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf9d5b7ea61ea4c9a6rom9", "name": "CAM PORCH", "category": "sp", "product_id": "rjKXWRohlvOTyLBu", diff --git a/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json b/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json index 7e4705650b1ea8..e98e38b21c8ddd 100644 --- a/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json +++ b/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf3f8b448bbc123e29oghf", "name": "C9", "category": "sp", "product_id": "sdd5f5f2dl5wydjf", diff --git a/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json b/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json index fdfbae9fbbfa85..94a8a7da26f592 100644 --- a/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json +++ b/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfa008a4f82a56616c69uz", "name": "jardin Fraises", "category": "tdq", "product_id": "1aegphq4yfd50e6b", diff --git a/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json b/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json index e3476118f20d1d..3d7b24df7ec52e 100644 --- a/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json +++ b/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff35871a2f4430058vs8u", "name": "Framboisiers", "category": "tdq", "product_id": "9htyiowaf5rtdhrv", diff --git a/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json index e7c79f3fb41b24..844f8cd3742d61 100644 --- a/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json +++ b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf082711d275c0c883vb4p", "name": "4-433", "category": "tdq", "product_id": "cq1p0nt0a4rixnex", diff --git a/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json b/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json index 1e40823b93daaa..e1f0865658f4ca 100644 --- a/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json +++ b/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyain.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "d7ca553b5f406266350poc", "name": "Seating side 6-ch Smart Switch ", "category": "tdq", "product_id": "nockvv2k39vbrxxk", diff --git a/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json b/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json index da26a133014a5b..cc8d186513c5d8 100644 --- a/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json +++ b/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0dc19ab84dc3627ep2un", "name": "Socket3", "category": "tdq", "product_id": "pu8uhxhwcp3tgoz7", diff --git a/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json b/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json index 708764184ad2ae..54a8d78d92d238 100644 --- a/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json +++ b/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb3c90d87dac93d2bdxn3", "name": "Living room left", "category": "tdq", "product_id": "uoa3mayicscacseb", diff --git a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json index 656c626c4fe022..ce8ab6c1d63ea9 100644 --- a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json +++ b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfdb773e4ae317e3915h2i", "name": "Solar zijpad", "category": "tyndj", "product_id": "pyakuuoc", diff --git a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json index 2fa798b2f60819..7fedfb4826eb9f 100644 --- a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json +++ b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf63312cdd4722555bmsuv", "name": "ZigBee Gateway", "category": "wfcon", "product_id": "b25mh8sxawsgndck", diff --git a/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json b/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json index 0e39f713dd0b20..2fb4e9a60641ff 100644 --- a/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json +++ b/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "1752690839034sq255y", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf79ca977d67322eb2o68m", "name": "X5 Zigbee Gateway", "category": "wg2", "product_id": "nwxr8qcu4seltoro", diff --git a/tests/components/tuya/fixtures/wk_6kijc7nd.json b/tests/components/tuya/fixtures/wk_6kijc7nd.json index 068a9b676a7a80..552de66c1d982e 100644 --- a/tests/components/tuya/fixtures/wk_6kijc7nd.json +++ b/tests/components/tuya/fixtures/wk_6kijc7nd.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf3c2c83660b8e19e152jb", "name": "Кабінет", "category": "wk", "product_id": "6kijc7nd", diff --git a/tests/components/tuya/fixtures/wk_aqoouq7x.json b/tests/components/tuya/fixtures/wk_aqoouq7x.json index 900ae356f38171..3bf17e356ff627 100644 --- a/tests/components/tuya/fixtures/wk_aqoouq7x.json +++ b/tests/components/tuya/fixtures/wk_aqoouq7x.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf6fc1645146455a2efrex", "name": "Clima cucina", "category": "wk", "product_id": "aqoouq7x", diff --git a/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json index 002b0609464103..f7c28db10436ed 100644 --- a/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json +++ b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb45cb8a9452fba66lexg", "name": "WiFi Smart Gas Boiler Thermostat ", "category": "wk", "product_id": "fi6dne5tu4t1nm6j", diff --git a/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json b/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json index bac85a54ed275d..0841f77ca2ce19 100644 --- a/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json +++ b/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf1085bf049a74fcc1idy2", "name": "smart thermostats", "category": "wk", "product_id": "gogb05wrtredz3bs", diff --git a/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json b/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json index 352a0ded392d8f..efe02c633f32ac 100644 --- a/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json +++ b/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8d64588f4a61965ezszs", "name": "Term - Prizemi", "category": "wk", "product_id": "y5obtqhuztqsf2mj", diff --git a/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json index 2929872f4c158b..51367039d9f81f 100644 --- a/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json +++ b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf316b8707b061f044th18", "name": "NP DownStairs North", "category": "wsdcg", "product_id": "g2y6z3p3ja2qhyav", diff --git a/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json index a7ab15a451182d..5fd511c7506dac 100644 --- a/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json +++ b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "DOLCECLIMA 10 HP WIFI", "category": "ydkt", "product_id": "jevroj5aguwdbs2e", diff --git a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json index f80b0cd5cd1952..c39835694c7aa0 100644 --- a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json +++ b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "8670375210521cf1349c", "name": " Smoke detector upstairs ", "category": "ywbj", "product_id": "gf9dejhmzffgdyfj", diff --git a/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json b/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json index 8b1cff0c773298..31d26fbb715e2c 100644 --- a/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json +++ b/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf3d16d38b17d7034ddxd4", "name": "Rainwater Tank Level", "category": "ywcgq", "product_id": "h8lvyoahr6s6aybf", diff --git a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json index 52eda664345caa..200790afedb691 100644 --- a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json +++ b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json @@ -135,6 +135,5 @@ "installation_height": 560, "liquid_depth_max": 100, "liquid_level_percent": 100 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json b/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json index dc1d214308732e..92f507abaca0ee 100644 --- a/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json +++ b/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyacn.com", - "terminal_id": "1753864737914eTkTk2", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "6c0887b46a2eaf56e0ui7d", "name": "XOCA-DAC212XC V2-S1", "category": "zndb", "product_id": "4ggkyflayu1h1ho9", diff --git a/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json index 797ddba3587a7e..caf9074d277295 100644 --- a/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json +++ b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfe33b4c74661f1f1bgacy", "name": "Meter", "category": "zndb", "product_id": "ze8faryrxr0glqnn", diff --git a/tests/components/tuya/fixtures/zwjcy_myd45weu.json b/tests/components/tuya/fixtures/zwjcy_myd45weu.json index 3ea111abb0e2b6..dc6c0510ffc449 100644 --- a/tests/components/tuya/fixtures/zwjcy_myd45weu.json +++ b/tests/components/tuya/fixtures/zwjcy_myd45weu.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf1a0431555359ce06ie0z", "name": "Patates", "category": "zwjcy", "product_id": "myd45weu", diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index ab96f58ecd014f..c0311198fddec4 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -7,12 +7,13 @@ from tuya_sharing import CustomerDevice from homeassistant.components.tuya import ManagerCompat +from homeassistant.components.tuya.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import initialize_entry +from . import DEVICE_MOCKS, initialize_entry -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_load_json_object_fixture @pytest.mark.parametrize("mock_device_code", ["ydkt_jevroj5aguwdbs2e"]) @@ -38,3 +39,21 @@ async def test_unsupported_device( assert not er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ) + + +async def test_fixtures_valid(hass: HomeAssistant) -> None: + """Ensure Tuya fixture files are valid.""" + # We want to ensure that the fixture files do not contain + # `home_assistant`, `id`, or `terminal_id` keys. + # These are provided by the Tuya diagnostics and should be removed + # from the fixture. + EXCLUDE_KEYS = ("home_assistant", "id", "terminal_id") + + for device_code in DEVICE_MOCKS: + details = await async_load_json_object_fixture( + hass, f"{device_code}.json", DOMAIN + ) + for key in EXCLUDE_KEYS: + assert key not in details, ( + f"Please remove data[`'{key}']` from {device_code}.json" + ) From 0f3f8d5707dcb87c71337e335dbe6e8ae2cca29c Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 8 Aug 2025 14:57:12 +0300 Subject: [PATCH 06/15] Bump openai to 1.99.3 (#150232) --- homeassistant/components/open_router/entity.py | 15 ++++++++------- .../components/open_router/manifest.json | 2 +- .../components/openai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/open_router/test_conversation.py | 6 +++--- tests/components/openai_conversation/__init__.py | 2 ++ .../openai_conversation/test_conversation.py | 1 + tests/components/openai_conversation/test_init.py | 3 +++ 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/open_router/entity.py b/homeassistant/components/open_router/entity.py index ac01ec89704a20..aa74442f7f42ae 100644 --- a/homeassistant/components/open_router/entity.py +++ b/homeassistant/components/open_router/entity.py @@ -9,15 +9,15 @@ import openai from openai.types.chat import ( ChatCompletionAssistantMessageParam, + ChatCompletionFunctionToolParam, ChatCompletionMessage, + ChatCompletionMessageFunctionToolCallParam, ChatCompletionMessageParam, - ChatCompletionMessageToolCallParam, ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, - ChatCompletionToolParam, ChatCompletionUserMessageParam, ) -from openai.types.chat.chat_completion_message_tool_call_param import Function +from openai.types.chat.chat_completion_message_function_tool_call_param import Function from openai.types.shared_params import FunctionDefinition, ResponseFormatJSONSchema from openai.types.shared_params.response_format_json_schema import JSONSchema import voluptuous as vol @@ -84,7 +84,7 @@ def _format_structured_output( def _format_tool( tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None, -) -> ChatCompletionToolParam: +) -> ChatCompletionFunctionToolParam: """Format tool specification.""" tool_spec = FunctionDefinition( name=tool.name, @@ -92,7 +92,7 @@ def _format_tool( ) if tool.description: tool_spec["description"] = tool.description - return ChatCompletionToolParam(type="function", function=tool_spec) + return ChatCompletionFunctionToolParam(type="function", function=tool_spec) def _convert_content_to_chat_message( @@ -121,7 +121,7 @@ def _convert_content_to_chat_message( ) if isinstance(content, conversation.AssistantContent) and content.tool_calls: param["tool_calls"] = [ - ChatCompletionMessageToolCallParam( + ChatCompletionMessageFunctionToolCallParam( type="function", id=tool_call.id, function=Function( @@ -160,6 +160,7 @@ async def _transform_response( tool_args=_decode_tool_arguments(tool_call.function.arguments), ) for tool_call in message.tool_calls + if tool_call.type == "function" ] yield data @@ -199,7 +200,7 @@ async def _async_handle_chat_log( "extra_body": {"require_parameters": True}, } - tools: list[ChatCompletionToolParam] | None = None + tools: list[ChatCompletionFunctionToolParam] | None = None if chat_log.llm_api: tools = [ _format_tool(tool, chat_log.llm_api.custom_serializer) diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index 8f989e63189b31..7dd824c2587c01 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.93.3", "python-open-router==0.3.1"] + "requirements": ["openai==1.99.3", "python-open-router==0.3.1"] } diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 5a6d76a396bd55..12ee6278c5d47b 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.93.3"] + "requirements": ["openai==1.99.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c29a2c81b08e95..8e644cc2f414aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1601,7 +1601,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.93.3 +openai==1.99.3 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc27d14f1ad355..5aa75e48096fdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1369,7 +1369,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.93.3 +openai==1.99.3 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/tests/components/open_router/test_conversation.py b/tests/components/open_router/test_conversation.py index afbdd907f93d57..edd475721203f8 100644 --- a/tests/components/open_router/test_conversation.py +++ b/tests/components/open_router/test_conversation.py @@ -7,10 +7,10 @@ from openai.types.chat import ( ChatCompletion, ChatCompletionMessage, - ChatCompletionMessageToolCall, + ChatCompletionMessageFunctionToolCall, ) from openai.types.chat.chat_completion import Choice -from openai.types.chat.chat_completion_message_tool_call import Function +from openai.types.chat.chat_completion_message_function_tool_call_param import Function import pytest from syrupy.assertion import SnapshotAssertion @@ -133,7 +133,7 @@ async def completion_result(*args, messages, **kwargs): role="assistant", function_call=None, tool_calls=[ - ChatCompletionMessageToolCall( + ChatCompletionMessageFunctionToolCall( id="call_call_1", function=Function( arguments='{"param1":"call1"}', diff --git a/tests/components/openai_conversation/__init__.py b/tests/components/openai_conversation/__init__.py index c10c23df237339..0cdccb6d0cf698 100644 --- a/tests/components/openai_conversation/__init__.py +++ b/tests/components/openai_conversation/__init__.py @@ -65,6 +65,7 @@ def create_message_item( content_index=0, delta=delta, item_id=id, + logprobs=[], output_index=output_index, sequence_number=0, type="response.output_text.delta", @@ -77,6 +78,7 @@ def create_message_item( ResponseTextDoneEvent( content_index=0, item_id=id, + logprobs=[], output_index=output_index, text="".join(text), sequence_number=0, diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index dafcba7bfeb2ce..5abce689855ca0 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -289,6 +289,7 @@ async def test_function_call( ) assert mock_create_stream.call_args.kwargs["input"][2] == { + "content": None, "id": "rs_A", "summary": [], "type": "reasoning", diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index fb8be3b2e68646..66afc41826b1d8 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -94,6 +94,7 @@ async def test_generate_image_service( with patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, return_value=ImagesResponse( created=1700000000, data=[ @@ -130,6 +131,7 @@ async def test_generate_image_service_error( with ( patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, side_effect=RateLimitError( response=httpx.Response( status_code=500, request=httpx.Request(method="GET", url="") @@ -154,6 +156,7 @@ async def test_generate_image_service_error( with ( patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, return_value=ImagesResponse( created=1700000000, data=[ From 8aee05b8b0d444dee888898e24028160b53b20a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:57:20 +0200 Subject: [PATCH 07/15] Bump github/codeql-action from 3.29.5 to 3.29.7 (#150254) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c5dcf19ce6ed01..f9795c219c58db 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.29.5 + uses: github/codeql-action/init@v3.29.7 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.29.5 + uses: github/codeql-action/analyze@v3.29.7 with: category: "/language:python" From ef4f476844daac46313cd404a032bead5ba09511 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:26:04 +0100 Subject: [PATCH 08/15] Fix handing for zero volume error in Squeezebox (#150265) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 49aad4fd6984ae..839e419dd96cb4 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -325,7 +325,7 @@ async def async_will_remove_from_hass(self) -> None: @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._player.volume: + if self._player.volume is not None: return int(float(self._player.volume)) / 100.0 return None From 01c197e830eb535e05b9d41cd1ddf99ac1ab2767 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 8 Aug 2025 15:06:31 +0200 Subject: [PATCH 09/15] Constraint num2words to 0.5.14 (#150276) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1a2f0d182a43e5..b79b0ecf6be543 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -212,3 +212,6 @@ multidict>=6.4.2 # rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 rpds-py==0.26.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b13f586439d4d0..a62f2f62bc1914 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -238,6 +238,9 @@ # rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 rpds-py==0.26.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 """ GENERATED_MESSAGE = ( From a8779d5f52a6da361c56546bab5134a97761a3e9 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:24:41 +0100 Subject: [PATCH 10/15] Fix error on startup when no Apps or Radio plugins are installed for Squeezebox (#150267) --- .../components/squeezebox/browse_media.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4f2a1fa7aa5705..e14f1989cbe18e 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -157,26 +157,28 @@ async def async_init(self, player: Player, browse_limit: int) -> None: cmd = ["apps", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["appss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result["appss_loop"]: + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) cmd = ["radios", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["radioss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result["radioss_loop"]: + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( From 23a2d69984342b3f3a91bd33fc0d0a6575a3af40 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:25:19 +0200 Subject: [PATCH 11/15] Volvo: fix missing charging power options (#150272) --- homeassistant/components/volvo/sensor.py | 7 +++- homeassistant/components/volvo/strings.json | 4 +- .../fixtures/ex30_2024/energy_state.json | 41 +++++++++---------- .../volvo/snapshots/test_sensor.ambr | 24 +++++++---- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index dd982238a47cc9..647c7b578e88a7 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -87,7 +87,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: return None -_CHARGING_POWER_STATUS_OPTIONS = ["providing_power", "no_power_available"] +_CHARGING_POWER_STATUS_OPTIONS = [ + "fault", + "power_available_but_not_activated", + "providing_power", + "no_power_available", +] _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( # command-accessibility endpoint diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 4fe7429117ccdb..c429c10657403f 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -94,7 +94,7 @@ "state": { "connected": "[%key:common::state::connected%]", "disconnected": "[%key:common::state::disconnected%]", - "fault": "[%key:common::state::error%]" + "fault": "[%key:common::state::fault%]" } }, "charging_current_limit": { @@ -106,6 +106,8 @@ "charging_power_status": { "name": "Charging power status", "state": { + "fault": "[%key:common::state::fault%]", + "power_available_but_not_activated": "Power available", "providing_power": "Providing power", "no_power_available": "No power" } diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index fe42dba568a3f6..0170d1aa617f38 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -1,57 +1,56 @@ { "batteryChargeLevel": { "status": "OK", - "value": 38, + "value": 90.0, "unit": "percentage", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "electricRange": { "status": "OK", - "value": 90, + "value": 327, "unit": "km", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerConnectionStatus": { "status": "OK", - "value": "DISCONNECTED", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "CONNECTED", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingStatus": { "status": "OK", - "value": "IDLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "DONE", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingType": { "status": "OK", - "value": "NONE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "AC", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerPowerStatus": { "status": "OK", - "value": "NO_POWER_AVAILABLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "FAULT", + "updatedAt": "2025-08-07T14:30:32Z" }, "estimatedChargingTimeToTargetBatteryChargeLevel": { "status": "OK", - "value": 0, + "value": 2, "unit": "minutes", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingCurrentLimit": { - "status": "OK", - "value": 32, - "unit": "ampere", - "updatedAt": "2024-03-05T08:38:44Z" + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { "status": "OK", "value": 90, "unit": "percentage", - "updatedAt": "2024-09-22T09:40:12Z" + "updatedAt": "2025-08-07T14:49:50Z" }, "chargingPower": { "status": "ERROR", - "code": "PROPERTY_NOT_FOUND", - "message": "No valid value could be found for the requested property" + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" } } diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index d5346cf9cd8b9e..b651bbd526f1e1 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -52,7 +52,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '38', + 'state': '90.0', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_battery_capacity-entry] @@ -229,7 +229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'disconnected', + 'state': 'connected', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-entry] @@ -285,7 +285,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '32', + 'state': 'unavailable', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry] @@ -351,6 +351,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -390,6 +392,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo EX30 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -399,7 +403,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'no_power_available', + 'state': 'fault', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_status-entry] @@ -465,7 +469,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'done', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_type-entry] @@ -525,7 +529,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'none', + 'state': 'ac', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_empty_battery-entry] @@ -581,7 +585,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '90', + 'state': '327', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_service-entry] @@ -693,7 +697,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '2', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_odometer-entry] @@ -2276,6 +2280,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -2315,6 +2321,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo XC40 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), From c0155f5e809342a77f667c925a53b62356e0dc63 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:26:02 +0200 Subject: [PATCH 12/15] Handle Unifi Protect BadRequest exception during API key creation (#150223) --- .../components/unifiprotect/__init__.py | 4 +-- tests/components/unifiprotect/test_init.py | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5fa9a85d3418b6..97a5ca6718615c 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -8,7 +8,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Bootstrap -from uiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized # Import the test_util.anonymize module from the uiprotect package # in __init__ to ensure it gets imported in the executor since the @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: new_api_key = await protect.create_api_key( name=f"Home Assistant ({hass.config.location_name})" ) - except NotAuthorized as err: + except (NotAuthorized, BadRequest) as err: _LOGGER.error("Failed to create API key: %s", err) else: protect.set_api_key(new_api_key) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index b951d95fbdc9fa..0776feece542c3 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,9 +5,10 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect import NvrError, ProtectApiClient from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import NVR, Bootstrap, CloudAccount, Light +from uiprotect.exceptions import BadRequest, NotAuthorized from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, @@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure( ufp.api.set_api_key.assert_not_called() +@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True) +async def test_setup_handles_api_key_creation_bad_request( + hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock +) -> None: + """Test handling of API key creation BadRequest error.""" + # Setup: API key is not set, user has write permissions, but creation fails with BadRequest + ufp.api.is_api_key_set.return_value = False + ufp.api.create_api_key = AsyncMock( + side_effect=BadRequest("Invalid API key creation request") + ) + + # Should fail with auth error due to API key creation failure + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + assert ufp.entry.state is ConfigEntryState.SETUP_ERROR + + # Verify API key creation was attempted but set_api_key was not called + ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)") + ufp.api.set_api_key.assert_not_called() + + async def test_setup_with_existing_api_key( hass: HomeAssistant, ufp: MockUFPFixture ) -> None: From 2d720f0d32c6d58e29cb2b60ba1bb0e06d57598e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:27:00 -0400 Subject: [PATCH 13/15] Fix JSON serialization for ZHA diagnostics download (#150210) --- homeassistant/components/zha/diagnostics.py | 16 +++++++++++++++- .../zha/snapshots/test_diagnostics.ambr | 1 + tests/components/zha/test_diagnostics.py | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 6c5fcba1f8b3d8..4383aa52afa541 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -8,6 +8,7 @@ from zha.application.const import ATTR_IEEE from zha.application.gateway import Gateway +from zigpy.application import ControllerApplication from zigpy.config import CONF_NWK_EXTENDED_PAN_ID from zigpy.types import Channels @@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict: return obj +def get_application_state_diagnostics(app: ControllerApplication) -> dict: + """Dump the application state as a dictionary.""" + data = shallow_asdict(app.state) + + # EUI64 objects in zigpy are not subclasses of any JSON-serializable key type and + # must be converted to strings. + data["network_info"]["nwk_addresses"] = { + str(k): v for k, v in data["network_info"]["nwk_addresses"].items() + } + + return data + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: @@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics( { "config": zha_data.yaml_config, "config_entry": config_entry.as_dict(), - "application_state": shallow_asdict(app.state), + "application_state": get_application_state_diagnostics(app), "energy_scan": { channel: 100 * energy / 255 for channel, energy in energy_scan.items() }, diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 35eb320893f3f0..4d90942fb97183 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -36,6 +36,7 @@ }), 'network_key': '**REDACTED**', 'nwk_addresses': dict({ + '11:22:33:44:55:66:77:88': 4660, }), 'nwk_manager_id': 0, 'nwk_update_id': 0, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 0e78a9a1b5bb6c..d32dd19152711d 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -6,6 +6,7 @@ from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from zigpy.profiles import zha +from zigpy.types import EUI64, NWK from zigpy.zcl.clusters import security from homeassistant.components.zha.helpers import ( @@ -71,6 +72,10 @@ async def test_diagnostics_for_config_entry( gateway.application_controller.energy_scan.side_effect = None gateway.application_controller.energy_scan.return_value = scan + gateway.application_controller.state.network_info.nwk_addresses = { + EUI64.convert("11:22:33:44:55:66:77:88"): NWK(0x1234) + } + diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) From b126f3fa6615daf6139153463c7e28d4e44099b7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:27:17 -0400 Subject: [PATCH 14/15] Bump ZHA to 0.0.68 (#150208) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9842fa7a0f3522..5cad3c823b88f7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.67"], + "requirements": ["zha==0.0.68"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 8e644cc2f414aa..51b4a5d34e4674 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3200,7 +3200,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.68 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5aa75e48096fdf..ac747cfdaa2c9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2644,7 +2644,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.68 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 From eb6ae9d2d642495c3f18703ac42e0293c22b5ffa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:28:51 +0200 Subject: [PATCH 15/15] Bump actions/cache from 4.2.3 to 4.2.4 (#150253) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aca149bf020b0e..12dbe60f146ef2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -255,7 +255,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -271,7 +271,7 @@ jobs: uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true @@ -301,7 +301,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -310,7 +310,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -341,7 +341,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -350,7 +350,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -381,7 +381,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -390,7 +390,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -497,7 +497,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -505,7 +505,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: ${{ env.UV_CACHE_DIR }} key: >- @@ -593,7 +593,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -626,7 +626,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -683,7 +683,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -726,7 +726,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -773,7 +773,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -825,7 +825,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -833,7 +833,7 @@ jobs: ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: .mypy_cache key: >- @@ -895,7 +895,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -956,7 +956,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1089,7 +1089,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1231,7 +1231,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1390,7 +1390,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true