From f955dec1ba049de4bbc750bfeb8f114475080d19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Aug 2025 18:06:45 +0000 Subject: [PATCH 01/95] Bump version to 2025.9.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 16d361a7957e97..492e4b9b1a36dd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 16ea7ee63744cd..66415bf6deec2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0.dev0" +version = "2025.9.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 0eaf8c694685298e472f881f8674cc0ab9aa1a9c Mon Sep 17 00:00:00 2001 From: Ian Tewksbury Date: Thu, 28 Aug 2025 02:04:50 -0400 Subject: [PATCH 02/95] Add multiple NICs in govee_light_local (#128123) --- .../components/govee_light_local/__init__.py | 19 +++++- .../govee_light_local/config_flow.py | 38 ++++++++--- .../govee_light_local/coordinator.py | 63 ++++++++++++------- 3 files changed, 83 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index ee04dd8108831a..00f77189e2b8ba 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -9,6 +9,7 @@ from govee_local_api.controller import LISTENING_PORT +from homeassistant.components import network from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -23,12 +24,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool: """Set up Govee light local from a config entry.""" - coordinator = GoveeLocalApiCoordinator(hass, entry) + + # Get source IPs for all enabled adapters + source_ips = await network.async_get_enabled_source_ips(hass) + _LOGGER.debug("Enabled source IPs: %s", source_ips) + + coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator( + hass=hass, config_entry=entry, source_ips=source_ips + ) async def await_cleanup(): - cleanup_complete: asyncio.Event = coordinator.cleanup() + cleanup_complete_events: [asyncio.Event] = coordinator.cleanup() with suppress(TimeoutError): - await asyncio.wait_for(cleanup_complete.wait(), 1) + await asyncio.gather( + *[ + asyncio.wait_for(cleanup_complete_event.wait(), 1) + for cleanup_complete_event in cleanup_complete_events + ] + ) entry.async_on_unload(await_cleanup) diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index da70d44688b3ca..67fa4b548cdd7f 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -4,6 +4,7 @@ import asyncio from contextlib import suppress +from ipaddress import IPv4Address, IPv6Address import logging from govee_local_api import GoveeController @@ -23,15 +24,13 @@ _LOGGER = logging.getLogger(__name__) -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - - adapter = await network.async_get_source_ip(hass, network.PUBLIC_TARGET_IP) - +async def _async_discover( + hass: HomeAssistant, adapter_ip: IPv4Address | IPv6Address +) -> bool: controller: GoveeController = GoveeController( loop=hass.loop, logger=_LOGGER, - listening_address=adapter, + listening_address=str(adapter_ip), broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, broadcast_port=CONF_TARGET_PORT_DEFAULT, listening_port=CONF_LISTENING_PORT_DEFAULT, @@ -41,9 +40,10 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: ) try: + _LOGGER.debug("Starting discovery with IP %s", adapter_ip) await controller.start() except OSError as ex: - _LOGGER.error("Start failed, errno: %d", ex.errno) + _LOGGER.error("Start failed on IP %s, errno: %d", adapter_ip, ex.errno) return False try: @@ -51,16 +51,34 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: while not controller.devices: await asyncio.sleep(delay=1) except TimeoutError: - _LOGGER.debug("No devices found") + _LOGGER.debug("No devices found with IP %s", adapter_ip) devices_count = len(controller.devices) - cleanup_complete: asyncio.Event = controller.cleanup() + cleanup_complete_events: list[asyncio.Event] = [] with suppress(TimeoutError): - await asyncio.wait_for(cleanup_complete.wait(), 1) + await asyncio.gather( + *[ + asyncio.wait_for(cleanup_complete_event.wait(), 1) + for cleanup_complete_event in cleanup_complete_events + ] + ) return devices_count > 0 +async def _async_has_devices(hass: HomeAssistant) -> bool: + """Return if there are devices that can be discovered.""" + + # Get source IPs for all enabled adapters + source_ips = await network.async_get_enabled_source_ips(hass) + _LOGGER.debug("Enabled source IPs: %s", source_ips) + + # Run discovery on every IPv4 address and gather results + results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips]) + + return any(results) + + config_entry_flow.register_discovery_flow( DOMAIN, "Govee light local", _async_has_devices ) diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 530ade1f74308c..9e0792a132dce7 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Callable +from ipaddress import IPv4Address, IPv6Address import logging from govee_local_api import GoveeController, GoveeDevice @@ -11,7 +12,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( - CONF_DISCOVERY_INTERVAL_DEFAULT, CONF_LISTENING_PORT_DEFAULT, CONF_MULTICAST_ADDRESS_DEFAULT, CONF_TARGET_PORT_DEFAULT, @@ -26,10 +26,11 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): """Govee light local coordinator.""" - config_entry: GoveeLocalConfigEntry - def __init__( - self, hass: HomeAssistant, config_entry: GoveeLocalConfigEntry + self, + hass: HomeAssistant, + config_entry: GoveeLocalConfigEntry, + source_ips: list[IPv4Address | IPv6Address], ) -> None: """Initialize my coordinator.""" super().__init__( @@ -40,32 +41,40 @@ def __init__( update_interval=SCAN_INTERVAL, ) - self._controller = GoveeController( - loop=hass.loop, - logger=_LOGGER, - broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, - broadcast_port=CONF_TARGET_PORT_DEFAULT, - listening_port=CONF_LISTENING_PORT_DEFAULT, - discovery_enabled=True, - discovery_interval=CONF_DISCOVERY_INTERVAL_DEFAULT, - discovered_callback=None, - update_enabled=False, - ) + self._controllers: list[GoveeController] = [ + GoveeController( + loop=hass.loop, + logger=_LOGGER, + listening_address=str(source_ip), + broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, + broadcast_port=CONF_TARGET_PORT_DEFAULT, + listening_port=CONF_LISTENING_PORT_DEFAULT, + discovery_enabled=True, + discovery_interval=1, + update_enabled=False, + ) + for source_ip in source_ips + ] async def start(self) -> None: """Start the Govee coordinator.""" - await self._controller.start() - self._controller.send_update_message() + + for controller in self._controllers: + await controller.start() + controller.send_update_message() async def set_discovery_callback( self, callback: Callable[[GoveeDevice, bool], bool] ) -> None: """Set discovery callback for automatic Govee light discovery.""" - self._controller.set_device_discovered_callback(callback) - def cleanup(self) -> asyncio.Event: - """Stop and cleanup the cooridinator.""" - return self._controller.cleanup() + for controller in self._controllers: + controller.set_device_discovered_callback(callback) + + def cleanup(self) -> list[asyncio.Event]: + """Stop and cleanup the coordinator.""" + + return [controller.cleanup() for controller in self._controllers] async def turn_on(self, device: GoveeDevice) -> None: """Turn on the light.""" @@ -96,8 +105,14 @@ async def set_scene(self, device: GoveeController, scene: str) -> None: @property def devices(self) -> list[GoveeDevice]: """Return a list of discovered Govee devices.""" - return self._controller.devices + + devices: list[GoveeDevice] = [] + for controller in self._controllers: + devices = devices + controller.devices + return devices async def _async_update_data(self) -> list[GoveeDevice]: - self._controller.send_update_message() - return self._controller.devices + for controller in self._controllers: + controller.send_update_message() + + return self.devices From 821577dc2136a2d7ef48a5c352c082d36b7bb3d2 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:53:13 +0200 Subject: [PATCH 03/95] Ignore errors when PlayStation Network group fetch is blocked by parental controls (#150364) --- .../playstation_network/coordinator.py | 38 +++++++++++++++---- .../playstation_network/strings.json | 6 +++ .../playstation_network/test_notify.py | 29 +++++++++++++- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index 977632de23bc28..94b178dc0c3e8a 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -5,6 +5,7 @@ from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta +import json import logging from typing import TYPE_CHECKING, Any @@ -21,12 +22,14 @@ from psnawp_api.models.trophies import TrophyTitle from homeassistant.config_entries import ConfigEntry, ConfigSubentry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, ConfigEntryNotReady, ) +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -143,13 +146,34 @@ class PlaystationNetworkGroupsUpdateCoordinator( async def update_data(self) -> dict[str, GroupDetails]: """Update groups data.""" - return await self.hass.async_add_executor_job( - lambda: { - group_info.group_id: group_info.get_group_information() - for group_info in self.psn.client.get_groups() - if not group_info.group_id.startswith("~") - } - ) + try: + return await self.hass.async_add_executor_job( + lambda: { + group_info.group_id: group_info.get_group_information() + for group_info in self.psn.client.get_groups() + if not group_info.group_id.startswith("~") + } + ) + except PSNAWPForbiddenError as e: + try: + error = json.loads(e.args[0]) + except json.JSONDecodeError as err: + raise PSNAWPServerError from err + ir.async_create_issue( + self.hass, + DOMAIN, + f"group_chat_forbidden_{self.config_entry.entry_id}", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.ERROR, + translation_key="group_chat_forbidden", + translation_placeholders={ + CONF_NAME: self.config_entry.title, + "error_message": error["error"]["message"], + }, + ) + await self.async_shutdown() + return {} class PlaystationNetworkFriendDataCoordinator( diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index 15b83b7cd0d415..b774d3a1b67f61 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -164,5 +164,11 @@ "name": "Direct message" } } + }, + "issues": { + "group_chat_forbidden": { + "title": "Failed to retrieve group chats for {name}", + "description": "The PlayStation Network integration was unable to retrieve group chats for **{name}**.\n\nThis is likely due to insufficient permissions (Error: `{error_message}`).\n\nTo resolve this issue, please ensure the account's chat and messaging feature is not restricted by parental controls or other privacy settings.\n\nIf the restriction is intentional, you can safely ignore this message." + } } } diff --git a/tests/components/playstation_network/test_notify.py b/tests/components/playstation_network/test_notify.py index f81e03dfcc4847..4d5ad7df7d486b 100644 --- a/tests/components/playstation_network/test_notify.py +++ b/tests/components/playstation_network/test_notify.py @@ -18,11 +18,12 @@ DOMAIN as NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) +from homeassistant.components.playstation_network.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry, snapshot_platform @@ -130,3 +131,29 @@ async def test_send_message_exceptions( ) mock_psnawpapi.group.return_value.send_message.assert_called_once_with("henlo fren") + + +async def test_notify_skip_forbidden( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, + issue_registry: ir.IssueRegistry, +) -> None: + """Test we skip creation of notifiers if forbidden by parental controls.""" + + mock_psnawpapi.me.return_value.get_groups.side_effect = PSNAWPForbiddenError( + """{"error": {"message": "Not permitted by parental control"}}""" + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + state = hass.states.get("notify.testuser_group_publicuniversalfriend") + assert state is None + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"group_chat_forbidden_{config_entry.entry_id}" + ) From 5d64dae3a03b525df9beb1bdf2d4e798ab361920 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:37:01 +0200 Subject: [PATCH 04/95] Fix direct message notifiers in PlayStation Network (#150548) --- .../playstation_network/__init__.py | 5 +- .../playstation_network/config_flow.py | 17 +- .../playstation_network/coordinator.py | 25 +- .../components/playstation_network/helpers.py | 5 +- .../components/playstation_network/notify.py | 79 ++++-- .../playstation_network/strings.json | 10 +- .../playstation_network/conftest.py | 1 + .../snapshots/test_notify.ambr | 14 +- .../snapshots/test_sensor.ambr | 255 +++++++++--------- .../playstation_network/test_config_flow.py | 3 +- .../playstation_network/test_init.py | 21 +- .../playstation_network/test_notify.py | 8 +- 12 files changed, 244 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/playstation_network/__init__.py b/homeassistant/components/playstation_network/__init__.py index c2399c61f93e23..91214ba9ebed69 100644 --- a/homeassistant/components/playstation_network/__init__.py +++ b/homeassistant/components/playstation_network/__init__.py @@ -9,6 +9,7 @@ from .coordinator import ( PlaystationNetworkConfigEntry, PlaystationNetworkFriendDataCoordinator, + PlaystationNetworkFriendlistCoordinator, PlaystationNetworkGroupsUpdateCoordinator, PlaystationNetworkRuntimeData, PlaystationNetworkTrophyTitlesCoordinator, @@ -40,6 +41,8 @@ async def async_setup_entry( groups = PlaystationNetworkGroupsUpdateCoordinator(hass, psn, entry) await groups.async_config_entry_first_refresh() + friends_list = PlaystationNetworkFriendlistCoordinator(hass, psn, entry) + friends = {} for subentry_id, subentry in entry.subentries.items(): @@ -50,7 +53,7 @@ async def async_setup_entry( friends[subentry_id] = friend_coordinator entry.runtime_data = PlaystationNetworkRuntimeData( - coordinator, trophy_titles, groups, friends + coordinator, trophy_titles, groups, friends, friends_list ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/playstation_network/config_flow.py b/homeassistant/components/playstation_network/config_flow.py index d7d82292378ae4..72df14dd239622 100644 --- a/homeassistant/components/playstation_network/config_flow.py +++ b/homeassistant/components/playstation_network/config_flow.py @@ -10,7 +10,6 @@ PSNAWPInvalidTokenError, PSNAWPNotFoundError, ) -from psnawp_api.models import User from psnawp_api.utils.misc import parse_npsso_token import voluptuous as vol @@ -169,13 +168,12 @@ async def async_step_reauth_confirm( class FriendSubentryFlowHandler(ConfigSubentryFlow): """Handle subentry flow for adding a friend.""" - friends_list: dict[str, User] - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> SubentryFlowResult: """Subentry user flow.""" config_entry: PlaystationNetworkConfigEntry = self._get_entry() + friends_list = config_entry.runtime_data.user_data.psn.friends_list if user_input is not None: config_entries = self.hass.config_entries.async_entries(DOMAIN) @@ -190,19 +188,12 @@ async def async_step_user( return self.async_abort(reason="already_configured") return self.async_create_entry( - title=self.friends_list[user_input[CONF_ACCOUNT_ID]].online_id, + title=friends_list[user_input[CONF_ACCOUNT_ID]].online_id, data={}, unique_id=user_input[CONF_ACCOUNT_ID], ) - self.friends_list = await self.hass.async_add_executor_job( - lambda: { - friend.account_id: friend - for friend in config_entry.runtime_data.user_data.psn.user.friends_list() - } - ) - - if not self.friends_list: + if not friends_list: return self.async_abort(reason="no_friends") options = [ @@ -210,7 +201,7 @@ async def async_step_user( value=friend.account_id, label=friend.online_id, ) - for friend in self.friends_list.values() + for friend in friends_list.values() ] return self.async_show_form( diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py index 94b178dc0c3e8a..2dced4b64adb88 100644 --- a/homeassistant/components/playstation_network/coordinator.py +++ b/homeassistant/components/playstation_network/coordinator.py @@ -48,6 +48,7 @@ class PlaystationNetworkRuntimeData: trophy_titles: PlaystationNetworkTrophyTitlesCoordinator groups: PlaystationNetworkGroupsUpdateCoordinator friends: dict[str, PlaystationNetworkFriendDataCoordinator] + friends_list: PlaystationNetworkFriendlistCoordinator class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): @@ -137,6 +138,25 @@ async def update_data(self) -> list[TrophyTitle]: return self.psn.trophy_titles +class PlaystationNetworkFriendlistCoordinator( + PlayStationNetworkBaseCoordinator[dict[str, User]] +): + """Friend list data update coordinator for PSN.""" + + _update_interval = timedelta(hours=3) + + async def update_data(self) -> dict[str, User]: + """Update trophy titles data.""" + + self.psn.friends_list = await self.hass.async_add_executor_job( + lambda: { + friend.account_id: friend for friend in self.psn.user.friends_list() + } + ) + await self.config_entry.runtime_data.user_data.async_request_refresh() + return self.psn.friends_list + + class PlaystationNetworkGroupsUpdateCoordinator( PlayStationNetworkBaseCoordinator[dict[str, GroupDetails]] ): @@ -202,7 +222,10 @@ def _setup(self) -> None: """Set up the coordinator.""" if TYPE_CHECKING: assert self.subentry.unique_id - self.user = self.psn.psn.user(account_id=self.subentry.unique_id) + self.user = self.psn.friends_list.get( + self.subentry.unique_id + ) or self.psn.psn.user(account_id=self.subentry.unique_id) + self.profile = self.user.profile() async def _async_setup(self) -> None: diff --git a/homeassistant/components/playstation_network/helpers.py b/homeassistant/components/playstation_network/helpers.py index 492a011cf78809..d456cc110a4ee7 100644 --- a/homeassistant/components/playstation_network/helpers.py +++ b/homeassistant/components/playstation_network/helpers.py @@ -60,7 +60,7 @@ def __init__(self, hass: HomeAssistant, npsso: str) -> None: self.legacy_profile: dict[str, Any] | None = None self.trophy_titles: list[TrophyTitle] = [] self._title_icon_urls: dict[str, str] = {} - self.friends_list: dict[str, User] | None = None + self.friends_list: dict[str, User] = {} def _setup(self) -> None: """Setup PSN.""" @@ -68,6 +68,9 @@ def _setup(self) -> None: self.client = self.psn.me() self.shareable_profile_link = self.client.get_shareable_profile_link() self.trophy_titles = list(self.user.trophy_titles(page_size=500)) + self.friends_list = { + friend.account_id: friend for friend in self.user.friends_list() + } async def async_setup(self) -> None: """Setup PSN.""" diff --git a/homeassistant/components/playstation_network/notify.py b/homeassistant/components/playstation_network/notify.py index a06359ebffc8ec..25c01960e3f518 100644 --- a/homeassistant/components/playstation_network/notify.py +++ b/homeassistant/components/playstation_network/notify.py @@ -18,7 +18,7 @@ NotifyEntity, NotifyEntityDescription, ) -from homeassistant.config_entries import ConfigSubentry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -27,7 +27,7 @@ from .const import DOMAIN from .coordinator import ( PlaystationNetworkConfigEntry, - PlaystationNetworkFriendDataCoordinator, + PlaystationNetworkFriendlistCoordinator, PlaystationNetworkGroupsUpdateCoordinator, ) from .entity import PlaystationNetworkServiceEntity @@ -50,8 +50,10 @@ async def async_setup_entry( """Set up the notify entity platform.""" coordinator = config_entry.runtime_data.groups + friends_list = config_entry.runtime_data.friends_list groups_added: set[str] = set() + friends_added: set[str] = set() entity_registry = er.async_get(hass) @callback @@ -78,16 +80,32 @@ def add_entities() -> None: coordinator.async_add_listener(add_entities) add_entities() - for subentry_id, friend_coordinator in config_entry.runtime_data.friends.items(): - async_add_entities( - [ - PlaystationNetworkDirectMessageNotifyEntity( - friend_coordinator, - config_entry.subentries[subentry_id], - ) - ], - config_subentry_id=subentry_id, - ) + @callback + def add_dm_entities() -> None: + nonlocal friends_added + + new_friends = set(friends_list.psn.friends_list.keys()) - friends_added + if new_friends: + async_add_entities( + [ + PlaystationNetworkDirectMessageNotifyEntity( + friends_list, account_id + ) + for account_id in new_friends + ], + ) + friends_added |= new_friends + deleted_friends = friends_added - set(coordinator.psn.friends_list.keys()) + for account_id in deleted_friends: + if entity_id := entity_registry.async_get_entity_id( + NOTIFY_DOMAIN, + DOMAIN, + f"{coordinator.config_entry.unique_id}_{account_id}_{PlaystationNetworkNotify.DIRECT_MESSAGE}", + ): + entity_registry.async_remove(entity_id) + + friends_list.async_add_listener(add_dm_entities) + add_dm_entities() class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity): @@ -95,12 +113,17 @@ class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, Notify group: Group | None = None - def send_message(self, message: str, title: str | None = None) -> None: - """Send a message.""" + def _send_message(self, message: str) -> None: + """Send message.""" if TYPE_CHECKING: assert self.group + self.group.send_message(message) + + def send_message(self, message: str, title: str | None = None) -> None: + """Send a message.""" + try: - self.group.send_message(message) + self._send_message(message) except PSNAWPNotFoundError as e: raise HomeAssistantError( translation_domain=DOMAIN, @@ -138,7 +161,7 @@ def __init__( key=group_id, translation_key=PlaystationNetworkNotify.GROUP_MESSAGE, translation_placeholders={ - "group_name": group_details["groupName"]["value"] + CONF_NAME: group_details["groupName"]["value"] or ", ".join( member["onlineId"] for member in group_details["members"] @@ -153,27 +176,29 @@ def __init__( class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity): """Representation of a PlayStation Network notify entity for sending direct messages.""" - coordinator: PlaystationNetworkFriendDataCoordinator + coordinator: PlaystationNetworkFriendlistCoordinator def __init__( self, - coordinator: PlaystationNetworkFriendDataCoordinator, - subentry: ConfigSubentry, + coordinator: PlaystationNetworkFriendlistCoordinator, + account_id: str, ) -> None: """Initialize a notification entity.""" - + self.account_id = account_id self.entity_description = NotifyEntityDescription( - key=PlaystationNetworkNotify.DIRECT_MESSAGE, + key=f"{account_id}_{PlaystationNetworkNotify.DIRECT_MESSAGE}", translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE, + translation_placeholders={ + CONF_NAME: coordinator.psn.friends_list[account_id].online_id + }, + entity_registry_enabled_default=False, ) - super().__init__(coordinator, self.entity_description, subentry) - - def send_message(self, message: str, title: str | None = None) -> None: - """Send a message.""" + super().__init__(coordinator, self.entity_description) + def _send_message(self, message: str) -> None: if not self.group: self.group = self.coordinator.psn.psn.group( - users_list=[self.coordinator.user] + users_list=[self.coordinator.psn.friends_list[self.account_id]] ) - super().send_message(message, title) + super()._send_message(message) diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index b774d3a1b67f61..72648be2cc29b6 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -82,13 +82,13 @@ "message": "Data retrieval failed when trying to access the PlayStation Network." }, "group_invalid": { - "message": "Failed to send message to group {group_name}. The group is invalid or does not exist." + "message": "Failed to send message to group {name}. The group is invalid or does not exist." }, "send_message_forbidden": { - "message": "Failed to send message to group {group_name}. You are not allowed to send messages to this group." + "message": "Failed to send message to {name}. You are not allowed to send messages to this group or friend." }, "send_message_failed": { - "message": "Failed to send message to group {group_name}. Try again later." + "message": "Failed to send message to {name}. Try again later." }, "user_profile_private": { "message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity." @@ -158,10 +158,10 @@ }, "notify": { "group_message": { - "name": "Group: {group_name}" + "name": "Group: {name}" }, "direct_message": { - "name": "Direct message" + "name": "Direct message: {name}" } } }, diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py index bfbdc9a72bdd48..f81f3842d806c9 100644 --- a/tests/components/playstation_network/conftest.py +++ b/tests/components/playstation_network/conftest.py @@ -184,6 +184,7 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]: fren = MagicMock( spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend" ) + fren.get_presence.return_value = mock_user.get_presence.return_value client.user.return_value.friends_list.return_value = [fren] diff --git a/tests/components/playstation_network/snapshots/test_notify.ambr b/tests/components/playstation_network/snapshots/test_notify.ambr index d8c329184337de..416b1da46cadfa 100644 --- a/tests/components/playstation_network/snapshots/test_notify.ambr +++ b/tests/components/playstation_network/snapshots/test_notify.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_notify_platform[notify.testuser_direct_message-entry] +# name: test_notify_platform[notify.testuser_direct_message_publicuniversalfriend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'notify', 'entity_category': None, - 'entity_id': 'notify.testuser_direct_message', + 'entity_id': 'notify.testuser_direct_message_publicuniversalfriend', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -24,24 +24,24 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Direct message', + 'original_name': 'Direct message: PublicUniversalFriend', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , - 'unique_id': 'fren-psn-id_direct_message', + 'unique_id': 'my-psn-id_fren-psn-id_direct_message', 'unit_of_measurement': None, }) # --- -# name: test_notify_platform[notify.testuser_direct_message-state] +# name: test_notify_platform[notify.testuser_direct_message_publicuniversalfriend-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Direct message', + 'friendly_name': 'testuser Direct message: PublicUniversalFriend', 'supported_features': , }), 'context': , - 'entity_id': 'notify.testuser_direct_message', + 'entity_id': 'notify.testuser_direct_message_publicuniversalfriend', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/playstation_network/snapshots/test_sensor.ambr b/tests/components/playstation_network/snapshots/test_sensor.ambr index 046989cebe6495..9d550e546b0402 100644 --- a/tests/components/playstation_network/snapshots/test_sensor.ambr +++ b/tests/components/playstation_network/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensors[sensor.testuser_bronze_trophies-entry] +# name: test_sensors[sensor.publicuniversalfriend_last_online-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_bronze_trophies', + 'entity_id': 'sensor.publicuniversalfriend_last_online', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -22,33 +22,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Bronze trophies', + 'original_name': 'Last online', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_earned_trophies_bronze', - 'unit_of_measurement': 'trophies', + 'translation_key': , + 'unique_id': 'fren-psn-id_last_online', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_bronze_trophies-state] +# name: test_sensors[sensor.publicuniversalfriend_last_online-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Bronze trophies', - 'unit_of_measurement': 'trophies', + 'device_class': 'timestamp', + 'friendly_name': 'PublicUniversalFriend Last online', }), 'context': , - 'entity_id': 'sensor.testuser_bronze_trophies', + 'entity_id': 'sensor.publicuniversalfriend_last_online', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '14450', + 'state': '2025-06-30T01:42:15+00:00', }) # --- -# name: test_sensors[sensor.testuser_gold_trophies-entry] +# name: test_sensors[sensor.publicuniversalfriend_now_playing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_gold_trophies', + 'entity_id': 'sensor.publicuniversalfriend_now_playing', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -73,31 +73,30 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Gold trophies', + 'original_name': 'Now playing', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_earned_trophies_gold', - 'unit_of_measurement': 'trophies', + 'translation_key': , + 'unique_id': 'fren-psn-id_now_playing', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_gold_trophies-state] +# name: test_sensors[sensor.publicuniversalfriend_now_playing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Gold trophies', - 'unit_of_measurement': 'trophies', + 'friendly_name': 'PublicUniversalFriend Now playing', }), 'context': , - 'entity_id': 'sensor.testuser_gold_trophies', + 'entity_id': 'sensor.publicuniversalfriend_now_playing', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11754', + 'state': 'STAR WARS Jedi: Survivor™', }) # --- -# name: test_sensors[sensor.testuser_last_online-entry] +# name: test_sensors[sensor.publicuniversalfriend_online_id-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,7 +109,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_last_online', + 'entity_id': 'sensor.publicuniversalfriend_online_id', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -120,38 +119,44 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Last online', + 'original_name': 'Online ID', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_last_online', + 'translation_key': , + 'unique_id': 'fren-psn-id_online_id', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_last_online-state] +# name: test_sensors[sensor.publicuniversalfriend_online_id-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'timestamp', - 'friendly_name': 'testuser Last online', + 'friendly_name': 'PublicUniversalFriend Online ID', }), 'context': , - 'entity_id': 'sensor.testuser_last_online', + 'entity_id': 'sensor.publicuniversalfriend_online_id', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2025-06-30T01:42:15+00:00', + 'state': 'PublicUniversalFriend', }) # --- -# name: test_sensors[sensor.testuser_last_online_2-entry] +# name: test_sensors[sensor.publicuniversalfriend_online_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -159,7 +164,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_last_online_2', + 'entity_id': 'sensor.publicuniversalfriend_online_status', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -169,33 +174,39 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Last online', + 'original_name': 'Online status', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'fren-psn-id_last_online', + 'translation_key': , + 'unique_id': 'fren-psn-id_online_status', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_last_online_2-state] +# name: test_sensors[sensor.publicuniversalfriend_online_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'timestamp', - 'friendly_name': 'testuser Last online', + 'device_class': 'enum', + 'friendly_name': 'PublicUniversalFriend Online status', + 'options': list([ + 'offline', + 'availabletoplay', + 'availabletocommunicate', + 'busy', + ]), }), 'context': , - 'entity_id': 'sensor.testuser_last_online_2', + 'entity_id': 'sensor.publicuniversalfriend_online_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2025-06-30T01:42:15+00:00', + 'state': 'availabletoplay', }) # --- -# name: test_sensors[sensor.testuser_next_level-entry] +# name: test_sensors[sensor.testuser_bronze_trophies-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -208,7 +219,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_next_level', + 'entity_id': 'sensor.testuser_bronze_trophies', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -220,31 +231,31 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Next level', + 'original_name': 'Bronze trophies', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_trophy_level_progress', - 'unit_of_measurement': '%', + 'translation_key': , + 'unique_id': 'my-psn-id_earned_trophies_bronze', + 'unit_of_measurement': 'trophies', }) # --- -# name: test_sensors[sensor.testuser_next_level-state] +# name: test_sensors[sensor.testuser_bronze_trophies-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Next level', - 'unit_of_measurement': '%', + 'friendly_name': 'testuser Bronze trophies', + 'unit_of_measurement': 'trophies', }), 'context': , - 'entity_id': 'sensor.testuser_next_level', + 'entity_id': 'sensor.testuser_bronze_trophies', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '19', + 'state': '14450', }) # --- -# name: test_sensors[sensor.testuser_now_playing-entry] +# name: test_sensors[sensor.testuser_gold_trophies-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -257,7 +268,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_now_playing', + 'entity_id': 'sensor.testuser_gold_trophies', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -269,30 +280,31 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Now playing', + 'original_name': 'Gold trophies', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_now_playing', - 'unit_of_measurement': None, + 'translation_key': , + 'unique_id': 'my-psn-id_earned_trophies_gold', + 'unit_of_measurement': 'trophies', }) # --- -# name: test_sensors[sensor.testuser_now_playing-state] +# name: test_sensors[sensor.testuser_gold_trophies-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Now playing', + 'friendly_name': 'testuser Gold trophies', + 'unit_of_measurement': 'trophies', }), 'context': , - 'entity_id': 'sensor.testuser_now_playing', + 'entity_id': 'sensor.testuser_gold_trophies', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'STAR WARS Jedi: Survivor™', + 'state': '11754', }) # --- -# name: test_sensors[sensor.testuser_now_playing_2-entry] +# name: test_sensors[sensor.testuser_last_online-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -305,7 +317,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_now_playing_2', + 'entity_id': 'sensor.testuser_last_online', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -315,32 +327,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Now playing', + 'original_name': 'Last online', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'fren-psn-id_now_playing', + 'translation_key': , + 'unique_id': 'my-psn-id_last_online', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_now_playing_2-state] +# name: test_sensors[sensor.testuser_last_online-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'testuser Now playing', + 'device_class': 'timestamp', + 'friendly_name': 'testuser Last online', }), 'context': , - 'entity_id': 'sensor.testuser_now_playing_2', + 'entity_id': 'sensor.testuser_last_online', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'STAR WARS Jedi: Survivor™', + 'state': '2025-06-30T01:42:15+00:00', }) # --- -# name: test_sensors[sensor.testuser_online_id-entry] +# name: test_sensors[sensor.testuser_next_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -353,7 +366,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_online_id', + 'entity_id': 'sensor.testuser_next_level', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -365,31 +378,31 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Online ID', + 'original_name': 'Next level', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_online_id', - 'unit_of_measurement': None, + 'translation_key': , + 'unique_id': 'my-psn-id_trophy_level_progress', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.testuser_online_id-state] +# name: test_sensors[sensor.testuser_next_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png', - 'friendly_name': 'testuser Online ID', + 'friendly_name': 'testuser Next level', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.testuser_online_id', + 'entity_id': 'sensor.testuser_next_level', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'testuser', + 'state': '19', }) # --- -# name: test_sensors[sensor.testuser_online_id_2-entry] +# name: test_sensors[sensor.testuser_now_playing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -402,7 +415,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_online_id_2', + 'entity_id': 'sensor.testuser_now_playing', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -414,43 +427,35 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Online ID', + 'original_name': 'Now playing', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'fren-psn-id_online_id', + 'translation_key': , + 'unique_id': 'my-psn-id_now_playing', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_online_id_2-state] +# name: test_sensors[sensor.testuser_now_playing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png', - 'friendly_name': 'testuser Online ID', + 'friendly_name': 'testuser Now playing', }), 'context': , - 'entity_id': 'sensor.testuser_online_id_2', + 'entity_id': 'sensor.testuser_now_playing', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'testuser', + 'state': 'STAR WARS Jedi: Survivor™', }) # --- -# name: test_sensors[sensor.testuser_online_status-entry] +# name: test_sensors[sensor.testuser_online_id-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'offline', - 'availabletoplay', - 'availabletocommunicate', - 'busy', - ]), - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -458,7 +463,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_online_status', + 'entity_id': 'sensor.testuser_online_id', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -468,39 +473,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Online status', + 'original_name': 'Online ID', 'platform': 'playstation_network', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': , - 'unique_id': 'my-psn-id_online_status', + 'translation_key': , + 'unique_id': 'my-psn-id_online_id', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_online_status-state] +# name: test_sensors[sensor.testuser_online_id-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'testuser Online status', - 'options': list([ - 'offline', - 'availabletoplay', - 'availabletocommunicate', - 'busy', - ]), + 'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png', + 'friendly_name': 'testuser Online ID', }), 'context': , - 'entity_id': 'sensor.testuser_online_status', + 'entity_id': 'sensor.testuser_online_id', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'availabletoplay', + 'state': 'testuser', }) # --- -# name: test_sensors[sensor.testuser_online_status_2-entry] +# name: test_sensors[sensor.testuser_online_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -520,7 +519,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.testuser_online_status_2', + 'entity_id': 'sensor.testuser_online_status', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -538,11 +537,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , - 'unique_id': 'fren-psn-id_online_status', + 'unique_id': 'my-psn-id_online_status', 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.testuser_online_status_2-state] +# name: test_sensors[sensor.testuser_online_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'enum', @@ -555,7 +554,7 @@ ]), }), 'context': , - 'entity_id': 'sensor.testuser_online_status_2', + 'entity_id': 'sensor.testuser_online_status', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/playstation_network/test_config_flow.py b/tests/components/playstation_network/test_config_flow.py index 0cd94fe153ab1d..14c5633d3848f1 100644 --- a/tests/components/playstation_network/test_config_flow.py +++ b/tests/components/playstation_network/test_config_flow.py @@ -501,6 +501,7 @@ async def test_add_friend_flow_no_friends( mock_psnawpapi: MagicMock, ) -> None: """Test we abort add friend subentry flow when the user has no friends.""" + mock_psnawpapi.user.return_value.friends_list.return_value = [] config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -508,8 +509,6 @@ async def test_add_friend_flow_no_friends( assert config_entry.state is ConfigEntryState.LOADED - mock_psnawpapi.user.return_value.friends_list.return_value = [] - result = await hass.config_entries.subentries.async_init( (config_entry.entry_id, "friend"), context={"source": SOURCE_USER}, diff --git a/tests/components/playstation_network/test_init.py b/tests/components/playstation_network/test_init.py index 6db4cb6ab6a696..e5a361a3cfbe9c 100644 --- a/tests/components/playstation_network/test_init.py +++ b/tests/components/playstation_network/test_init.py @@ -278,10 +278,9 @@ async def test_friends_coordinator_update_data_failed( ) -> None: """Test friends coordinator setup fails in _update_data.""" - mock_psnawpapi.user.return_value.get_presence.side_effect = [ - mock_psnawpapi.user.return_value.get_presence.return_value, - exception, - ] + mock = mock_psnawpapi.user.return_value.friends_list.return_value[0] + mock.get_presence.side_effect = exception + config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -306,11 +305,9 @@ async def test_friends_coordinator_setup_failed( state: ConfigEntryState, ) -> None: """Test friends coordinator setup fails in _async_setup.""" + mock = mock_psnawpapi.user.return_value.friends_list.return_value[0] + mock.profile.side_effect = exception - mock_psnawpapi.user.side_effect = [ - mock_psnawpapi.user.return_value, - exception, - ] config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -324,10 +321,10 @@ async def test_friends_coordinator_auth_failed( mock_psnawpapi: MagicMock, ) -> None: """Test friends coordinator starts reauth on authentication error.""" - mock_psnawpapi.user.side_effect = [ - mock_psnawpapi.user.return_value, - PSNAWPAuthenticationError, - ] + + mock = mock_psnawpapi.user.return_value.friends_list.return_value[0] + mock.profile.side_effect = PSNAWPAuthenticationError + config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/playstation_network/test_notify.py b/tests/components/playstation_network/test_notify.py index 4d5ad7df7d486b..a4ef6584a6ee79 100644 --- a/tests/components/playstation_network/test_notify.py +++ b/tests/components/playstation_network/test_notify.py @@ -38,7 +38,7 @@ async def notify_only() -> AsyncGenerator[None]: yield -@pytest.mark.usefixtures("mock_psnawpapi") +@pytest.mark.usefixtures("mock_psnawpapi", "entity_registry_enabled_by_default") async def test_notify_platform( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -58,9 +58,13 @@ async def test_notify_platform( @pytest.mark.parametrize( "entity_id", - ["notify.testuser_group_publicuniversalfriend", "notify.testuser_direct_message"], + [ + "notify.testuser_group_publicuniversalfriend", + "notify.testuser_direct_message_publicuniversalfriend", + ], ) @freeze_time("2025-07-28T00:00:00+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_send_message( hass: HomeAssistant, config_entry: MockConfigEntry, From e2ca439a3a16f49ee302d7381c6fe3feeedd3fe0 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Wed, 27 Aug 2025 15:05:15 -0700 Subject: [PATCH 05/95] Iaqualink: create parent device manually and link entities (#151215) --- homeassistant/components/iaqualink/__init__.py | 10 ++++++++++ homeassistant/components/iaqualink/entity.py | 1 + tests/components/iaqualink/conftest.py | 1 + 3 files changed, 12 insertions(+) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 68a8a093c093a9..88c7e97a8144f4 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -24,6 +24,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.httpx_client import get_async_client @@ -104,6 +105,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) -> f"Error while attempting to retrieve devices list: {svc_exception}" ) from svc_exception + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + name=system.name, + identifiers={(DOMAIN, system.serial)}, + manufacturer="Jandy", + serial_number=system.serial, + ) + for dev in devices.values(): if isinstance(dev, AqualinkThermostat): runtime_data.thermostats += [dev] diff --git a/homeassistant/components/iaqualink/entity.py b/homeassistant/components/iaqualink/entity.py index 0b3751e5fbccd8..c0f44946b7716b 100644 --- a/homeassistant/components/iaqualink/entity.py +++ b/homeassistant/components/iaqualink/entity.py @@ -29,6 +29,7 @@ def __init__(self, dev: AqualinkDeviceT) -> None: self._attr_unique_id = f"{dev.system.serial}_{dev.name}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, + via_device=(DOMAIN, dev.system.serial), manufacturer=dev.manufacturer, model=dev.model, name=dev.label, diff --git a/tests/components/iaqualink/conftest.py b/tests/components/iaqualink/conftest.py index c7e7373f4c2928..37e89e4fe52f88 100644 --- a/tests/components/iaqualink/conftest.py +++ b/tests/components/iaqualink/conftest.py @@ -43,6 +43,7 @@ def get_aqualink_system(aqualink, cls=None, data=None): data = {} num = random.randint(0, 99999) + data["name"] = "Pool" data["serial_number"] = f"SN{num:05}" return cls(aqualink=aqualink, data=data) From a57d77899a8f62a69b04bdae732f425c874ee9d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 27 Aug 2025 23:08:39 +0200 Subject: [PATCH 06/95] Fix spelling in bayesian strings (#151265) --- homeassistant/components/bayesian/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json index abf322a2b49c7f..2d296d549b82d7 100644 --- a/homeassistant/components/bayesian/strings.json +++ b/homeassistant/components/bayesian/strings.json @@ -61,7 +61,7 @@ }, "data_description": { "probability_threshold": "The probability above which the sensor will show as 'on'. 50% should produce the most accurate result. Use numbers greater than 50% if avoiding false positives is important, or vice-versa.", - "prior": "The baseline probabilty the sensor should be 'on', this is usually the percentage of time it is true. For example, for a sensor 'Everyone sleeping' it might be 8 hours a day, 33%.", + "prior": "The baseline probability the sensor should be 'on', this is usually the percentage of time it is true. For example, for a sensor 'Everyone sleeping' it might be 8 hours a day, 33%.", "device_class": "Choose the device class you would like the sensor to show as." } }, From ab7c5bf8d9393e2c77dcb27ea3f62ab5e1c73bf4 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Thu, 28 Aug 2025 10:59:37 +0100 Subject: [PATCH 07/95] Fix endpoint deprecation warning in Mastodon (#151275) --- homeassistant/components/mastodon/__init__.py | 15 +++- .../components/mastodon/config_flow.py | 9 +- .../components/mastodon/diagnostics.py | 11 ++- tests/components/mastodon/conftest.py | 6 +- .../mastodon/snapshots/test_diagnostics.ambr | 84 +++++++++++++++++++ tests/components/mastodon/test_config_flow.py | 46 +++++++++- tests/components/mastodon/test_diagnostics.py | 24 ++++++ tests/components/mastodon/test_init.py | 21 ++++- 8 files changed, 205 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index b6e0d863471b6b..6c8f53e4cb2ef6 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -2,7 +2,14 @@ from __future__ import annotations -from mastodon.Mastodon import Account, Instance, InstanceV2, Mastodon, MastodonError +from mastodon.Mastodon import ( + Account, + Instance, + InstanceV2, + Mastodon, + MastodonError, + MastodonNotFoundError, +) from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -105,7 +112,11 @@ def setup_mastodon( entry.data[CONF_ACCESS_TOKEN], ) - instance = client.instance() + try: + instance = client.instance_v2() + except MastodonNotFoundError: + instance = client.instance_v1() + account = client.account_verify_credentials() return client, instance, account diff --git a/homeassistant/components/mastodon/config_flow.py b/homeassistant/components/mastodon/config_flow.py index 1ae1e6b229eb93..dbd617eca5f236 100644 --- a/homeassistant/components/mastodon/config_flow.py +++ b/homeassistant/components/mastodon/config_flow.py @@ -7,7 +7,9 @@ from mastodon.Mastodon import ( Account, Instance, + InstanceV2, MastodonNetworkError, + MastodonNotFoundError, MastodonUnauthorizedError, ) import voluptuous as vol @@ -61,7 +63,7 @@ def check_connection( client_secret: str, access_token: str, ) -> tuple[ - Instance | None, + InstanceV2 | Instance | None, Account | None, dict[str, str], ]: @@ -73,7 +75,10 @@ def check_connection( client_secret, access_token, ) - instance = client.instance() + try: + instance = client.instance_v2() + except MastodonNotFoundError: + instance = client.instance_v1() account = client.account_verify_credentials() except MastodonNetworkError: diff --git a/homeassistant/components/mastodon/diagnostics.py b/homeassistant/components/mastodon/diagnostics.py index 31444413dfd18e..434f6c0acacc54 100644 --- a/homeassistant/components/mastodon/diagnostics.py +++ b/homeassistant/components/mastodon/diagnostics.py @@ -4,7 +4,7 @@ from typing import Any -from mastodon.Mastodon import Account, Instance +from mastodon.Mastodon import Account, Instance, InstanceV2, MastodonNotFoundError from homeassistant.core import HomeAssistant @@ -27,11 +27,16 @@ async def async_get_config_entry_diagnostics( } -def get_diagnostics(config_entry: MastodonConfigEntry) -> tuple[Instance, Account]: +def get_diagnostics( + config_entry: MastodonConfigEntry, +) -> tuple[InstanceV2 | Instance, Account]: """Get mastodon diagnostics.""" client = config_entry.runtime_data.client - instance = client.instance() + try: + instance = client.instance_v2() + except MastodonNotFoundError: + instance = client.instance_v1() account = client.account_verify_credentials() return instance, account diff --git a/tests/components/mastodon/conftest.py b/tests/components/mastodon/conftest.py index d8979083de9df1..0a0e203bf28178 100644 --- a/tests/components/mastodon/conftest.py +++ b/tests/components/mastodon/conftest.py @@ -32,12 +32,16 @@ def mock_mastodon_client() -> Generator[AsyncMock]: ) as mock_client, ): client = mock_client.return_value - client.instance.return_value = InstanceV2.from_json( + client.instance_v1.return_value = InstanceV2.from_json( + load_fixture("instance.json", DOMAIN) + ) + client.instance_v2.return_value = InstanceV2.from_json( load_fixture("instance.json", DOMAIN) ) client.account_verify_credentials.return_value = Account.from_json( load_fixture("account_verify_credentials.json", DOMAIN) ) + client.mastodon_api_version = 2 client.status_post.return_value = None yield client diff --git a/tests/components/mastodon/snapshots/test_diagnostics.ambr b/tests/components/mastodon/snapshots/test_diagnostics.ambr index ec9da1836bc637..81abc77e21f205 100644 --- a/tests/components/mastodon/snapshots/test_diagnostics.ambr +++ b/tests/components/mastodon/snapshots/test_diagnostics.ambr @@ -83,3 +83,87 @@ }), }) # --- +# name: test_entry_diagnostics_fallback_to_instance_v1 + dict({ + 'account': dict({ + 'acct': 'trwnh', + 'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png', + 'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png', + 'bot': True, + 'created_at': '2016-11-24T00:00:00+00:00', + 'discoverable': True, + 'display_name': 'infinite love ⴳ', + 'emojis': list([ + ]), + 'fields': list([ + dict({ + 'name': 'Website', + 'value': 'trwnh.com', + 'verified_at': '2019-08-29T04:14:55.571+00:00', + }), + dict({ + 'name': 'Portfolio', + 'value': 'abdullahtarawneh.com', + 'verified_at': '2021-02-11T20:34:13.574+00:00', + }), + dict({ + 'name': 'Fan of:', + 'value': 'Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)', + 'verified_at': None, + }), + dict({ + 'name': 'What to expect:', + 'value': 'talking about various things i find interesting, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people! and to spill my thoughts.', + 'verified_at': None, + }), + ]), + 'followers_count': 3169, + 'following_count': 328, + 'group': False, + 'header': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg', + 'header_static': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg', + 'hide_collections': True, + 'id': '14715', + 'indexable': False, + 'last_status_at': '2025-03-04T00:00:00', + 'limited': None, + 'locked': False, + 'memorial': None, + 'moved': None, + 'moved_to_account': None, + 'mute_expires_at': None, + 'noindex': False, + 'note': '

i have approximate knowledge of many things. perpetual student. (nb/ace/they)

xmpp/email: a@trwnh.com
trwnh.com
help me live:
- donate.stripe.com/4gwcPCaMpcQ1
- liberapay.com/trwnh

notes:
- my triggers are moths and glitter
- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise
- dm me if i did something wrong, so i can improve
- purest person on fedi, do not lewd in my presence

', + 'role': None, + 'roles': list([ + ]), + 'source': None, + 'statuses_count': 69523, + 'suspended': None, + 'uri': 'https://mastodon.social/users/trwnh', + 'url': 'https://mastodon.social/@trwnh', + 'username': 'trwnh', + }), + 'instance': dict({ + 'api_versions': None, + 'configuration': None, + 'contact': None, + 'description': 'The original server operated by the Mastodon gGmbH non-profit', + 'domain': 'mastodon.social', + 'icon': None, + 'languages': None, + 'registrations': None, + 'rules': None, + 'source_url': 'https://github.com/mastodon/mastodon', + 'thumbnail': None, + 'title': 'Mastodon', + 'uri': 'mastodon.social', + 'usage': dict({ + 'users': dict({ + 'active_month': 380143, + }), + }), + 'version': '4.4.0-nightly.2025-02-07', + }), + }) +# --- diff --git a/tests/components/mastodon/test_config_flow.py b/tests/components/mastodon/test_config_flow.py index 4b022df2ca2625..5f1014c31d3d41 100644 --- a/tests/components/mastodon/test_config_flow.py +++ b/tests/components/mastodon/test_config_flow.py @@ -2,7 +2,11 @@ from unittest.mock import AsyncMock -from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError +from mastodon.Mastodon import ( + MastodonNetworkError, + MastodonNotFoundError, + MastodonUnauthorizedError, +) import pytest from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN @@ -80,6 +84,46 @@ async def test_full_flow_with_path( assert result["result"].unique_id == "trwnh_mastodon_social" +async def test_full_flow_fallback_to_instance_v1( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow where instance_v2 fails and falls back to instance_v1.""" + mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError( + "Instance API v2 not found" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "@trwnh@mastodon.social" + assert result["data"] == { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + } + assert result["result"].unique_id == "trwnh_mastodon_social" + + mock_mastodon_client.instance_v2.assert_called_once() + mock_mastodon_client.instance_v1.assert_called_once() + + @pytest.mark.parametrize( ("exception", "error"), [ diff --git a/tests/components/mastodon/test_diagnostics.py b/tests/components/mastodon/test_diagnostics.py index 531543ee65d79b..a3ee1b8eea327c 100644 --- a/tests/components/mastodon/test_diagnostics.py +++ b/tests/components/mastodon/test_diagnostics.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +from mastodon.Mastodon import MastodonNotFoundError from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -26,3 +27,26 @@ async def test_entry_diagnostics( await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) == snapshot ) + + +async def test_entry_diagnostics_fallback_to_instance_v1( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics with fallback to instance_v1 when instance_v2 raises MastodonNotFoundError.""" + mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError( + "Instance v2 not found" + ) + + await setup_integration(hass, mock_config_entry) + + diagnostics_result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + mock_mastodon_client.instance_v1.assert_called() + + assert diagnostics_result == snapshot diff --git a/tests/components/mastodon/test_init.py b/tests/components/mastodon/test_init.py index c3d0728fe08d68..b4808792f66346 100644 --- a/tests/components/mastodon/test_init.py +++ b/tests/components/mastodon/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from mastodon.Mastodon import MastodonError +from mastodon.Mastodon import MastodonNotFoundError from syrupy.assertion import SnapshotAssertion from homeassistant.components.mastodon.config_flow import MastodonConfigFlow @@ -39,13 +39,30 @@ async def test_initialization_failure( mock_config_entry: MockConfigEntry, ) -> None: """Test initialization failure.""" - mock_mastodon_client.instance.side_effect = MastodonError + mock_mastodon_client.instance_v1.side_effect = MastodonNotFoundError + mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_setup_integration_fallback_to_instance_v1( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test full flow where instance_v2 fails and falls back to instance_v1.""" + mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError( + "Instance API v2 not found" + ) + + await setup_integration(hass, mock_config_entry) + + mock_mastodon_client.instance_v2.assert_called_once() + mock_mastodon_client.instance_v1.assert_called_once() + + async def test_migrate( hass: HomeAssistant, mock_mastodon_client: AsyncMock, From def27ab7054b45e1a0b70a736976ee430c45100d Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Thu, 28 Aug 2025 03:33:30 -0700 Subject: [PATCH 08/95] Remove `uv.lock` (#151282) --- uv.lock | 1919 ------------------------------------------------------- 1 file changed, 1919 deletions(-) delete mode 100644 uv.lock diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 9d6a7b046eff24..00000000000000 --- a/uv.lock +++ /dev/null @@ -1,1919 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13.2" - -[[package]] -name = "acme" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "josepy" }, - { name = "pyopenssl" }, - { name = "pyrfc3339" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/df/d006c4920fd04b843c21698bd038968cb9caa3315608f55abde0f8e4ad6b/acme-4.2.0.tar.gz", hash = "sha256:0df68c0e1acb3824a2100013f8cd51bda2e1a56aa23447449d14c942959f0c41", size = 96820, upload-time = "2025-08-05T19:19:08.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/26/9ff889b5d762616bf92ecbeb1ab93faddfd7bf6068146340359e9a6beb43/acme-4.2.0-py3-none-any.whl", hash = "sha256:6292011bbfa5f966521b2fb9469982c24ff4c58e240985f14564ccf35372e79a", size = 101573, upload-time = "2025-08-05T19:18:45.266Z" }, -] - -[[package]] -name = "aiodns" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycares" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload-time = "2025-06-13T16:21:53.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload-time = "2025-06-13T16:21:52.45Z" }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohasupervisor" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "mashumaro" }, - { name = "orjson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/c2/cd208f6b6bc78675130a4ed883bfd6de3e401131233ee85c4e3f6c231166/aiohasupervisor-0.3.1.tar.gz", hash = "sha256:6d88c32e640932855cf5d7ade573208a003527a9687129923a71e3ab0f0cdf26", size = 41261, upload-time = "2025-04-24T14:16:07.579Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/a3/f1d1e351c722f1a6343289b0aaff86391f3e4b2e2292760f9420f8a3628e/aiohasupervisor-0.3.1-py3-none-any.whl", hash = "sha256:d5fa5df20562177703c701e95889a52595788c5790a856f285474d68553346a3", size = 38803, upload-time = "2025-04-24T14:16:05.921Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.12.15" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, - { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, - { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, - { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, - { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, - { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, - { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, -] - -[[package]] -name = "aiohttp-asyncmdnsresolver" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiodns" }, - { name = "aiohttp" }, - { name = "zeroconf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/83/09fb97705e7308f94197a09b486669696ea20f28074c14b5811a38bdedc3/aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81", size = 36129, upload-time = "2025-02-14T14:46:44.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/d1/4f61508a43de82bb5c60cede3bb89cc57c5e8af7978d93ca03ad60b99368/aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e", size = 13582, upload-time = "2025-02-14T14:46:41.985Z" }, -] - -[[package]] -name = "aiohttp-cors" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, -] - -[[package]] -name = "aiohttp-fast-zlib" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/a6/982f3a013b42e914a2420631afcaecb729c49525cc6cc58e15d27ee4cb4b/aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28", size = 8770, upload-time = "2025-06-07T12:41:49.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/11/ea9ecbcd6cf68c5de690fd39b66341405ab091aa0c3598277e687aa65901/aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e", size = 8615, upload-time = "2025-06-07T12:41:47.454Z" }, -] - -[[package]] -name = "aiooui" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b7/ad0f86010bbabc4e556e98dd2921a923677188223cc524432695966f14fa/aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8", size = 369276, upload-time = "2025-01-19T00:12:44.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/fa/b1310457adbea7adb84d2c144159f3b41341c40c80df3c10ce6b266874b3/aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5", size = 367404, upload-time = "2025-01-19T00:12:42.57Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "aiozoneinfo" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/00/e437a179ab78ed24780ded10bbb5d7e10832c07f62eab1d44ee2f335c95c/aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3", size = 8381, upload-time = "2025-02-04T19:32:06.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" }, -] - -[[package]] -name = "annotatedyaml" -version = "0.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "propcache" }, - { name = "pyyaml" }, - { name = "voluptuous" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/b6/e24fb814108d0a708cc8b26d67e61d5fee0735373dcaa8cd61cb140caf02/annotatedyaml-0.4.5.tar.gz", hash = "sha256:e251929cd7e741fa2e9ece13e24e29bb8f1b5c6ca3a9ef7292a66a3ae8b9390f", size = 15321, upload-time = "2025-03-22T17:50:37.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/d4/262c3ebf8266595975f810998c6a82633eddc373764a927d919d33f3d3ce/annotatedyaml-0.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971293ef07be457554ee97bcd6f7b0cb13df1c8d8ab1a2554880d78d9dc5d27a", size = 60968, upload-time = "2025-03-22T17:54:21.021Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b2/fd26ed4aa50c8a6670ae0909f8075262d50fa959eeff2185074f00cdc8aa/annotatedyaml-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8100a47d37b766f850bf8659fc6f973b14633f5d4a1957195af0a0e36449ffbe", size = 60414, upload-time = "2025-03-22T17:54:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/f5/96/0c52b99fb8cf39b585fca4a4656b829c1b0eec38943eef40c97044ed114b/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51a053d426ce1d1d7a783cea5185f5f5b3a4c3c2f269cd9cd2dfb07bd6671ee0", size = 72011, upload-time = "2025-03-22T17:54:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a6/7a77d92db7df4f491f5a90218c1d327bf32d37bfa18c99d3a9588d219d0f/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2ca45e75b3091680553f21dca3f776075fb029f1a8499de61801cb0712f29de5", size = 77028, upload-time = "2025-03-22T17:54:24.433Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a0/bd6dc6eab687ab98a182cdf5fadb8a9456b6dab25cb1260857f324abcda0/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354a88931bc73e05d4e1b24dd6c26b8618ea6412553b4c8084a7481932482bc", size = 74145, upload-time = "2025-03-22T17:54:25.988Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e1/ad12626d5096835d583455a02165f1d0cabdfd1796f5b07854f86fc61083/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c3a91402dcfcf45967dcbbcd3ee151222c4881202be87f00c17cf0d627caae", size = 68149, upload-time = "2025-03-22T17:54:27.414Z" }, - { url = "https://files.pythonhosted.org/packages/25/48/a871c4c3c6e45b002a6f04a17b758e8db0120f79b43a494b298dff43ebfa/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:3d76ca28122fd063f27f298aa76f074f4bb8dd84501cf74cfec51931f0ed7ae0", size = 74388, upload-time = "2025-03-22T17:50:36.089Z" }, - { url = "https://files.pythonhosted.org/packages/03/b2/7ff9c2c479883a7f583ba5f0c380d937caf065eb994cbf671a656c6847b7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea47e128d2a8f549fad47b4a579f9d0a0e11733130419cb5071eb242caf5e66e", size = 73542, upload-time = "2025-03-22T17:54:28.527Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/a9cb90c65717226cf7eb3f5f0808befb9c80e05641c8857e305a02bc6393/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b0b21600607faea68a6a8e99fab7671119a672c454b153aec3fc3410347650ee", size = 69904, upload-time = "2025-03-22T17:54:29.694Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f0/a8d04e2cf8d743c5364af8a41dd2110a4fee70489142114f4f99a87124f7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:233864f23f89a43457759a526a01cccc9f60409b08070b806b5122ee5cc4cb9c", size = 80000, upload-time = "2025-03-22T17:54:30.826Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d6/24c949543c2378390856912ccf66d2b82b06ab68ec43ff8da48dd2e072e3/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:35e0be8088e81b60be70da401da23db5420795e1e3ba7451d232a02dd9a81f30", size = 76820, upload-time = "2025-03-22T17:54:31.967Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ca/8c85cf1f87234cf99a44ac2c9859e7446015932bcc205d06a95b0197739a/annotatedyaml-0.4.5-cp313-cp313-win32.whl", hash = "sha256:967fddfa8af4864f09190bde7905f05ab5bdd5f32fcca672e86033a39b0afbe8", size = 57338, upload-time = "2025-03-22T17:54:33.093Z" }, - { url = "https://files.pythonhosted.org/packages/78/57/2cb75df5189ee009278895afa77941ba701d4fc72f5b6ce44b6f97295159/annotatedyaml-0.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:f53f9f8e4ae92081653337be56265cf7085a5bc216f5e15c4531b36de5cba365", size = 62040, upload-time = "2025-03-22T17:54:34.617Z" }, -] - -[[package]] -name = "anyio" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, -] - -[[package]] -name = "astral" -version = "2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/c3/76dfe55a68c48a1a6f3d2eeab2793ebffa9db8adfba82774a7e0f5f43980/astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe", size = 578223, upload-time = "2020-05-20T14:23:17.602Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/60/7cc241b9c3710ebadddcb323e77dd422c693183aec92449a1cf1fb59e1ba/astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a", size = 30775, upload-time = "2020-05-20T14:23:14.866Z" }, -] - -[[package]] -name = "async-interrupt" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/79/732a581e3ceb09f938d33ad8ab3419856181d95bb621aa2441a10f281e10/async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7", size = 8484, upload-time = "2025-02-22T17:15:04.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/77/060b972fa7819fa9eea9a70acf8c7c0c58341a1e300ee5ccb063e757a4a7/async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c", size = 8907, upload-time = "2025-02-22T17:15:01.971Z" }, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - -[[package]] -name = "atomicwrites-homeassistant" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/10ff0fd9aa04f78a0b31bb617c8d29796a12bea33f1e48aa54687d635e44/atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f", size = 12223, upload-time = "2022-07-08T20:56:46.35Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/1b/872dd3b11939edb4c0a27d2569a9b7e77d3b88995a45a331f376e13528c0/atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d", size = 7128, upload-time = "2022-07-08T20:56:44.186Z" }, -] - -[[package]] -name = "attrs" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, -] - -[[package]] -name = "audioop-lts" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252, upload-time = "2024-08-04T21:13:56.209Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183, upload-time = "2024-08-04T21:13:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726, upload-time = "2024-08-04T21:14:00.846Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718, upload-time = "2024-08-04T21:14:01.989Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326, upload-time = "2024-08-04T21:14:03.509Z" }, - { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539, upload-time = "2024-08-04T21:14:04.679Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577, upload-time = "2024-08-04T21:14:09.038Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074, upload-time = "2024-08-04T21:14:09.99Z" }, - { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210, upload-time = "2024-08-04T21:14:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664, upload-time = "2024-08-04T21:14:12.394Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255, upload-time = "2024-08-04T21:14:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760, upload-time = "2024-08-04T21:14:14.74Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992, upload-time = "2024-08-04T21:14:19.155Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload-time = "2024-08-04T21:14:20.438Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload-time = "2024-08-04T21:14:21.342Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload-time = "2024-08-04T21:14:22.193Z" }, - { url = "https://files.pythonhosted.org/packages/7a/99/bb664a99561fd4266687e5cb8965e6ec31ba4ff7002c3fce3dc5ef2709db/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455", size = 46827, upload-time = "2024-08-04T21:14:23.034Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e3/f664171e867e0768ab982715e744430cf323f1282eb2e11ebfb6ee4c4551/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f", size = 27479, upload-time = "2024-08-04T21:14:23.922Z" }, - { url = "https://files.pythonhosted.org/packages/a6/0d/2a79231ff54eb20e83b47e7610462ad6a2bea4e113fae5aa91c6547e7764/audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf", size = 27056, upload-time = "2024-08-04T21:14:28.061Z" }, - { url = "https://files.pythonhosted.org/packages/86/46/342471398283bb0634f5a6df947806a423ba74b2e29e250c7ec0e3720e4f/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab", size = 87802, upload-time = "2024-08-04T21:14:29.586Z" }, - { url = "https://files.pythonhosted.org/packages/56/44/7a85b08d4ed55517634ff19ddfbd0af05bf8bfd39a204e4445cd0e6f0cc9/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6", size = 95016, upload-time = "2024-08-04T21:14:30.481Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2a/45edbca97ea9ee9e6bbbdb8d25613a36e16a4d1e14ae01557392f15cc8d3/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543", size = 87394, upload-time = "2024-08-04T21:14:31.883Z" }, - { url = "https://files.pythonhosted.org/packages/14/ae/832bcbbef2c510629593bf46739374174606e25ac7d106b08d396b74c964/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074", size = 84874, upload-time = "2024-08-04T21:14:32.751Z" }, - { url = "https://files.pythonhosted.org/packages/26/1c/8023c3490798ed2f90dfe58ec3b26d7520a243ae9c0fc751ed3c9d8dbb69/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78", size = 88698, upload-time = "2024-08-04T21:14:34.147Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/5379d953d4918278b1f04a5a64b2c112bd7aae8f81021009da0dcb77173c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb", size = 90401, upload-time = "2024-08-04T21:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/99/6e/3c45d316705ab1aec2e69543a5b5e458d0d112a93d08994347fafef03d50/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2", size = 91864, upload-time = "2024-08-04T21:14:36.158Z" }, - { url = "https://files.pythonhosted.org/packages/08/58/6a371d8fed4f34debdb532c0b00942a84ebf3e7ad368e5edc26931d0e251/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a", size = 98796, upload-time = "2024-08-04T21:14:37.185Z" }, - { url = "https://files.pythonhosted.org/packages/ee/77/d637aa35497e0034ff846fd3330d1db26bc6fd9dd79c406e1341188b06a2/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a", size = 94116, upload-time = "2024-08-04T21:14:38.145Z" }, - { url = "https://files.pythonhosted.org/packages/1a/60/7afc2abf46bbcf525a6ebc0305d85ab08dc2d1e2da72c48dbb35eee5b62c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63", size = 91520, upload-time = "2024-08-04T21:14:39.128Z" }, - { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482, upload-time = "2024-08-04T21:14:40.269Z" }, - { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780, upload-time = "2024-08-04T21:14:41.128Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, -] - -[[package]] -name = "awesomeversion" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/95/bd19ef0ef6735bd7131c0310f71432ea5fdf3dc2b3245a262d1f34bae55e/awesomeversion-25.5.0.tar.gz", hash = "sha256:d64c9f3579d2f60a5aa506a9dd0b38a74ab5f45e04800f943a547c1102280f31", size = 11693, upload-time = "2025-05-29T12:38:02.352Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/99/dc26ce0845a99f90fd99464a1d9124d5eacaa8bac92072af059cf002def4/awesomeversion-25.5.0-py3-none-any.whl", hash = "sha256:34a676ae10e10d3a96829fcc890a1d377fe1a7a2b98ee19951631951c2aebff6", size = 13998, upload-time = "2025-05-29T12:38:01.127Z" }, -] - -[[package]] -name = "bcrypt" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, - { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, - { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, - { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, - { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, - { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, - { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, - { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, - { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, - { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, - { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, - { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, - { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, - { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, -] - -[[package]] -name = "bleak" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dbus-fast", marker = "sys_platform == 'linux'" }, - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" }, - { name = "winrt-runtime", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-devices-bluetooth", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-devices-bluetooth-advertisement", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-devices-bluetooth-genericattributeprofile", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-devices-enumeration", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" }, - { name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/84/a7d5056e148b02b7a3398fe122eea5b1585f0439d95958f019867a2ec4b6/bleak-1.1.0.tar.gz", hash = "sha256:0ace59c8cf5a2d8aa66a2493419b59ac6a119c2f72f6e57be8dbdd3f2c0270e0", size = 116100, upload-time = "2025-08-10T22:50:23.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/7a/fbfffec2f7839fa779a11a3d1d46edcd6cf790c135ff3a2eaa3777906fea/bleak-1.1.0-py3-none-any.whl", hash = "sha256:174e7836e1ab0879860cd24ddd0ac604bd192bcc1acb978892e27359f3f18304", size = 136236, upload-time = "2025-08-10T22:50:21.74Z" }, -] - -[[package]] -name = "bleak-retry-connector" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bleak", marker = "python_full_version < '3.14'" }, - { name = "bluetooth-adapters", marker = "python_full_version < '3.14' and sys_platform == 'linux'" }, - { name = "dbus-fast", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/f1/9ba41e851e0b9cef32b0902fe835e04d6548ef193131212d47f0a39ad87b/bleak_retry_connector-4.0.0.tar.gz", hash = "sha256:2a20dcaee5aed6aada886565fcda0b59244fabbdba7781c139adac68422a50ae", size = 15854, upload-time = "2025-07-01T03:00:24.114Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/58/976e7a4c22853df08741525dbb7b3feb83737a645e841b48978e2c312bfa/bleak_retry_connector-4.0.0-py3-none-any.whl", hash = "sha256:b7712a10f80735eaa981549fa4f867418268cd32ab15d8ca4e0f6697bbe13f02", size = 16512, upload-time = "2025-07-01T03:00:22.886Z" }, -] - -[[package]] -name = "bluetooth-adapters" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiooui" }, - { name = "bleak" }, - { name = "dbus-fast", marker = "sys_platform == 'linux'" }, - { name = "uart-devices" }, - { name = "usb-devices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/be/1a3d598833270f1ad86a7ba27918a6377cb233ef468ab14e10c4b0838be5/bluetooth_adapters-2.0.0.tar.gz", hash = "sha256:ecdba203e806a90ea503cc32acfe11eafdc10813abac4591545d174da78d3c55", size = 17051, upload-time = "2025-07-01T00:40:08.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/0a/c30dd310acdfc117bee488d7f7374ae6e7f3d17d14c762a83be7b5177f63/bluetooth_adapters-2.0.0-py3-none-any.whl", hash = "sha256:7eff2c48dd3170e8ccf91888ddc97d847faa24cdd2678cf4b78166c1999171a8", size = 20077, upload-time = "2025-07-01T00:40:07.134Z" }, -] - -[[package]] -name = "bluetooth-auto-recovery" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bluetooth-adapters" }, - { name = "btsocket" }, - { name = "pyric" }, - { name = "usb-devices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/01/5c8214e36fdd6866b85d32d55eeeb57dec0d311536fbdcab314a8ab97c29/bluetooth_auto_recovery-1.5.2.tar.gz", hash = "sha256:f8decb4fd58c10eabec6ab7623a506be06f03e2cc26b6ce2726f72d8bce69296", size = 12570, upload-time = "2025-05-21T13:55:09.59Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/74/9274757a1efa31846f5674ecb80579eeccc3fde8d2ae89120e744f4afc96/bluetooth_auto_recovery-1.5.2-py3-none-any.whl", hash = "sha256:2748817403f43b4701ca3183a936159afe63857d996bd4b8e3186129f2c6b44a", size = 11499, upload-time = "2025-05-21T13:55:08.049Z" }, -] - -[[package]] -name = "bluetooth-data-tools" -version = "1.28.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/45/39aca7dcbeff6727af3d4675ad88a20b92390d72c1c291a870f9756ffdce/bluetooth_data_tools-1.28.2.tar.gz", hash = "sha256:2afa97695fc61c8d55d19ffa9485a498051410f399a183852d1bf29f675c3537", size = 16487, upload-time = "2025-07-02T03:15:08.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/f2/56cc5c23c95775b7d504ec03f3c06e487a48543710d94ea81da0a417b9ba/bluetooth_data_tools-1.28.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71df3e6221ee472cb38fd625cecc6e0a8733e093e40c08e80638e9387349b43b", size = 382151, upload-time = "2025-07-02T03:21:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3c/d6891ce258bfc9450d55d9c22f0572ae04f2f7fadbcfda5d592155f02bf5/bluetooth_data_tools-1.28.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b2925335caf40bb9872a8733d823bb8e97bac2bc7ce988a695452e4a39507e29", size = 378894, upload-time = "2025-07-02T03:21:35.987Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a0/95665da579b6186e8214e2fe37c8237837fb3f2d8840d87575171a0d070e/bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535c037b3ccd86a5df890b338b901eea3e974692ae07b591c1f99e787d629170", size = 404621, upload-time = "2025-07-02T03:21:37.335Z" }, - { url = "https://files.pythonhosted.org/packages/2f/95/ec11b451510b434eb150b502c425ed1a074182fc8adfbf164722901bd717/bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:080668765dc7d04d6b78a7bc0feaffd14b45ccee58b5c005a22b78e3730934fd", size = 413118, upload-time = "2025-07-02T03:21:38.579Z" }, - { url = "https://files.pythonhosted.org/packages/d7/00/e2498b28989ef7dc37c49ab8621d017d68340c522caf538e7fdf5fb5b389/bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c2947f86112fc308973df735f030ede800473dd61f9e32d62d55bfb5c00748", size = 408257, upload-time = "2025-07-02T03:21:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/5dd66f7e5fa342a12c150495d4adf3e7316c866ff03a6d3d78b769fc47d9/bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d74c6b9187b444e548cd01ce56c74eb0c1ba592043b9a1f48a9c2ed19a8a236a", size = 130448, upload-time = "2025-07-02T03:21:40.994Z" }, - { url = "https://files.pythonhosted.org/packages/38/6d/e11ac9d282342da12f1615e6814aada881866317811dc580305cd5db951e/bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ad09f0dbc343e51c34f32672aa877373d747eebe956c640117ce9472c86f1cb2", size = 140214, upload-time = "2025-07-02T03:15:06.927Z" }, - { url = "https://files.pythonhosted.org/packages/f6/07/a97ff62acf5d866e73b4c06366d1859f6340965d4f145287d2e5d2d8f5a3/bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c833481774fe319ef239351bb8a028cc2efe44ad7cf23681bd2cd2a4dfb71599", size = 410583, upload-time = "2025-07-02T03:21:42.149Z" }, - { url = "https://files.pythonhosted.org/packages/65/f0/f3868a755e88ff2f4371fa5f32b1637f00b048f0a0a5ccab9a828d7e1130/bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a989a4a5e8e4d70410fd9bba7b03f970bed7b8f79531087565931314437420be", size = 132702, upload-time = "2025-07-02T03:21:43.675Z" }, - { url = "https://files.pythonhosted.org/packages/05/82/0e9f383747557cdfec4f1f1fb0b2ee69931df28812eb0635cb53d6a37805/bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6f30e619ca3b46716a7f8c2bde35776d36e6b98e1922f0642034618e1056b3b3", size = 420685, upload-time = "2025-07-02T03:21:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/6e/25/a00ee7c9b38716480fd3a64e8100d5d5a6283f8513009958dcb221631007/bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cf3714c9e27aaa7db0800816bf766919cd1ac18080bac0102c2ad466db02f47a", size = 413573, upload-time = "2025-07-02T03:21:46.752Z" }, - { url = "https://files.pythonhosted.org/packages/09/e2/1c584a2107672670f3331ac781ebb5ddbae8f06b9461cb76794c1dc402e4/bluetooth_data_tools-1.28.2-cp313-cp313-win32.whl", hash = "sha256:8f28eeee5fecaebeb9fc1012e4220bc3c1ee6ee82bf8a17b9183995933f6d938", size = 285878, upload-time = "2025-07-02T03:21:48.11Z" }, - { url = "https://files.pythonhosted.org/packages/67/bb/19f2928dd9b4d27a74349edc687999c00d9694ff4ca19cf14f44f7548654/bluetooth_data_tools-1.28.2-cp313-cp313-win_amd64.whl", hash = "sha256:e748587be85a8133b0a43e34e2c6f65dbf5113765a03d4f89c26039b8289decb", size = 285881, upload-time = "2025-07-02T03:21:49.356Z" }, -] - -[[package]] -name = "boto3" -version = "1.40.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, - { name = "jmespath" }, - { name = "s3transfer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/5a/f31556d817e872c2723196a34b197d971d78297b22b8bae0ae6d93f7f9c1/boto3-1.40.7.tar.gz", hash = "sha256:61b15f70761f1eadd721c6ba41a92658f003eaaef09500ca7642f5ae68ec8945", size = 111989, upload-time = "2025-08-11T19:20:45.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/e3/f2a77f4809ffe4e896c2e6186db88333ae980f52a91b28e9fd068d8f5506/boto3-1.40.7-py3-none-any.whl", hash = "sha256:8727cac601a679d2885dc78b8119a0548bbbe04e49b72f7d94021a629154c080", size = 140061, upload-time = "2025-08-11T19:20:43.173Z" }, -] - -[[package]] -name = "botocore" -version = "1.40.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jmespath" }, - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/d7/5e559918410b259c1e54a4646ff39c56433e1c9cefa5e66ab0f06716cee8/botocore-1.40.7.tar.gz", hash = "sha256:33793696680cf3a0c4b5ace4f9070c67c4d4fcb19c999fd85cfee55de3dcf913", size = 14318282, upload-time = "2025-08-11T19:20:33.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fa/bb7ec68b24d1b4678d341a305cbfed78a593e6383c86a70727410e4d0e11/botocore-1.40.7-py3-none-any.whl", hash = "sha256:a06956f3d7222e80ef6ae193608f358c3b7898e1a2b88553479d8f9737fbb03e", size = 13981488, upload-time = "2025-08-11T19:20:27.303Z" }, -] - -[[package]] -name = "btsocket" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/b1/0ae262ecf936f5d2472ff7387087ca674e3b88d8c76b3e0e55fbc0c6e956/btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d", size = 19563, upload-time = "2024-06-10T07:05:27.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/2b/9bf3481131a24cb29350d69469448349362f6102bed9ae4a0a5bb228d731/btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c", size = 14807, upload-time = "2024-06-10T07:05:26.381Z" }, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, -] - -[[package]] -name = "ciso8601" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/e9/d83711081c997540aee59ad2f49d81f01d33e8551d766b0ebde346f605af/ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434", size = 28214, upload-time = "2024-12-09T12:26:40.768Z" } - -[[package]] -name = "cronsim" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz", hash = "sha256:5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d", size = 20315, upload-time = "2024-11-02T14:34:02.475Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/dd/9c40c4e0f4d3cb6cf52eb335e9cc1fa140c1f3a87146fb6987f465b069da/cronsim-2.6-py3-none-any.whl", hash = "sha256:5e153ff8ed64da7ee8d5caac470dbeda8024ab052c3010b1be149772b4801835", size = 13500, upload-time = "2024-12-04T12:53:57.443Z" }, -] - -[[package]] -name = "cryptography" -version = "45.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" }, - { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" }, - { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" }, - { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" }, - { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" }, - { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" }, - { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" }, - { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" }, - { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" }, - { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" }, - { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" }, - { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" }, - { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" }, - { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" }, -] - -[[package]] -name = "dbus-fast" -version = "2.44.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/f2/8a3f2345452f4aa8e9899544ba6dfdf699cef39ecfb04238fdad381451c8/dbus_fast-2.44.3.tar.gz", hash = "sha256:962b36abbe885159e31135c57a7d9659997c61a13d55ecb070a61dc502dbd87e", size = 72458, upload-time = "2025-08-04T00:42:18.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/cf/e4ae27e14e470b84827848694836e8fae0c386162d98e43f891783c0abc8/dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da0910f813350b951efe4964a19d7f4aaf253b6c1021b0d68340160a990dc2fc", size = 835165, upload-time = "2025-08-04T00:57:12.44Z" }, - { url = "https://files.pythonhosted.org/packages/ba/88/6d8b0d0d274fd944a5c9506e559a38b7020884fd4250ee31e9fdb279c80f/dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:253ad2417b0651ba32325661bb559228ceaedea9fb75d238972087a5f66551fd", size = 905750, upload-time = "2025-08-04T00:57:13.973Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/4306e52ea702fe79be160f333ed84af111d725c75605b1ca7286f7df69f8/dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb4c56bef8f69e4e2606eb29a5c137ba448cf7d6958f4f2fba263d74623bd06", size = 888637, upload-time = "2025-08-04T00:57:15.414Z" }, - { url = "https://files.pythonhosted.org/packages/78/c8/b45ff0a015f606c1998df2070967f016f873d4087845af14fd3d01303b0b/dbus_fast-2.44.3-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6e0a6a27a1f53b32259d0789bca6f53decd88dec52722cac9a93327f8b7670c3", size = 891773, upload-time = "2025-08-04T00:42:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4f/344bd7247b74b4af0562cf01be70832af62bd1495c6796125ea944d2a909/dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a990390c5d019e8e4d41268a3ead0eb6e48e977173d7685b0f5b5b3d0695c2f", size = 850429, upload-time = "2025-08-04T00:57:16.776Z" }, - { url = "https://files.pythonhosted.org/packages/43/26/ec514f6e882975d4c40e88cf88b0240952f9cf425aebdd59081afa7f6ad2/dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5aca3c940eddb99f19bd3f0c6c50cd566fd98396dd9516d35dbf12af25b7a2c6", size = 939261, upload-time = "2025-08-04T00:57:18.274Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/cb514104c0e98aa0514e4f09e5c16e78585e11dae392d501b742a92843c5/dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0046e74c25b79ffb6ea5b07f33b5da0bdc2a75ad6aede3f7836654485239121d", size = 916025, upload-time = "2025-08-04T00:57:19.939Z" }, -] - -[[package]] -name = "envs" -version = "1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/2098df91ff1499860935b4276ea0c27d3234170b03f803a8b9c97e42f0e9/envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398", size = 9230, upload-time = "2021-12-09T22:16:52.616Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" }, -] - -[[package]] -name = "fnv-hash-fast" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fnvhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/85/ebcbccceb212bdc9b0d964609e319469075df2a7393dcad7048a333507b6/fnv_hash_fast-1.5.0.tar.gz", hash = "sha256:c3f0d077a5e0eee6bc12938a6f560b6394b5736f3e30db83b2eca8e0fb948a74", size = 5670, upload-time = "2025-04-23T02:04:49.804Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/8e/eb6fcf4ff3d70919cc8eed1383c68682b5831b1e89d951e6922d650edeee/fnv_hash_fast-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0294a449e672583589e8e5cce9d60dfc5e29db3fb05737ccae98deba28b7d77f", size = 18597, upload-time = "2025-04-23T02:10:26.498Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f3/e5db61ba58224fd5a47fa7a16be8ee0ad1c09deadac2f73363aefa7342a9/fnv_hash_fast-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:643002874f4620c408fdf881041e7d8b23683e56b1d588604a3640758c4e6dfe", size = 18568, upload-time = "2025-04-23T02:10:27.508Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/8fe9a5237dd43a0a8f236413fe0e0e33b0f4f91170e6cf9f9242ff940855/fnv_hash_fast-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13904ceb14e09c5d6092eca8f6e1a65ea8bb606328b4b86d055365f23657ca58", size = 21736, upload-time = "2025-04-23T02:10:28.825Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d5/5629db362f2f515429228b564e51a404c0b7b6cad04f4896161bfb5bb974/fnv_hash_fast-1.5.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5747cc25ee940eaa70c05d0b3d0a49808e952b7dd8388453980b94ea9e95e837", size = 23091, upload-time = "2025-04-23T02:10:29.875Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0c/4ba49df5da5b345cb456ea1934569472555a9c4ead4a5ae899494b52e385/fnv_hash_fast-1.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9640989256fcb9e95a383ebde372b79bb4b7e14d296e5242fb32c422a6d83480", size = 22098, upload-time = "2025-04-23T02:10:31.066Z" }, - { url = "https://files.pythonhosted.org/packages/00/3d/99d8c58f550bff0da4e51f71643fa0b2b16ef47e4e8746b0698221e01451/fnv_hash_fast-1.5.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e3b79e3fada2925810efd1605f265f0335cafe48f1389c96c51261b3e2e05ff", size = 19733, upload-time = "2025-04-23T02:10:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/ee/00/20389a610628b5d294811fabe1bca408a4f5fe4cb5745ae05f52c77ef1b6/fnv_hash_fast-1.5.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ccd18302d1a2d800f6403be7d8cb02293f2e39363bc64cd843ed040396d36f1a", size = 21731, upload-time = "2025-04-23T02:04:48.356Z" }, - { url = "https://files.pythonhosted.org/packages/41/29/0c7a0c4bd2c06d7c917d38b81a084e53176ef514d5fd9d40163be1b78d78/fnv_hash_fast-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14c7672ae4cfaf8f88418dc23ef50977f4603c602932038ae52fae44b1b03aec", size = 22374, upload-time = "2025-04-23T02:10:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/ca/12/5efe53c767def55ab00ab184b4fe04591ddabffbe6daf08476dfe18dc8fb/fnv_hash_fast-1.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:90fff41560a95d5262f2237259a94d0c8c662e131b13540e9db51dbec1a14912", size = 20260, upload-time = "2025-04-23T02:10:34.943Z" }, - { url = "https://files.pythonhosted.org/packages/81/00/83261b804ee585ec1de0da3226185e2934ec7a1747b6a871bb2cbd777e51/fnv_hash_fast-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9b52650bd9107cfe8a81087b6bd9fa995f0ba23dafa1a7cb343aed99c136062", size = 23974, upload-time = "2025-04-23T02:10:35.943Z" }, - { url = "https://files.pythonhosted.org/packages/84/1a/72d8716adfe349eb3762e923df6e25346311469dfd3dbca4fc05d8176ced/fnv_hash_fast-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a4b3fa3e5e3273872d021bc2d6ef26db273bdd82a1bedd49b3f798dbcb34bba", size = 22844, upload-time = "2025-04-23T02:10:36.925Z" }, - { url = "https://files.pythonhosted.org/packages/8d/65/0dd16e6b1f6d163b56b34e8c6c1af41086e8d3e5fc3b77701d24c5f5cdde/fnv_hash_fast-1.5.0-cp313-cp313-win32.whl", hash = "sha256:381175ad08ee8b0c69c14283a60a20d953c24bc19e2d80e5932eb590211c50dc", size = 18983, upload-time = "2025-04-23T02:10:37.918Z" }, - { url = "https://files.pythonhosted.org/packages/8d/8d/179abdc6304491ea72f276e1c85f5c15269f680d1cfeda07cb9963e4a03c/fnv_hash_fast-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:db8e61e38d5eddf4a4115e82bbee35f0b1b1d5affe8736f78ffc833751746cf2", size = 20507, upload-time = "2025-04-23T02:10:38.967Z" }, -] - -[[package]] -name = "fnvhash" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/01/14ef74ea03ac12e8a80d43bbad5356ae809b125cd2072766e459bcc7d388/fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e", size = 1902, upload-time = "2015-11-28T12:21:00.722Z" } - -[[package]] -name = "frozenlist" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, -] - -[[package]] -name = "greenlet" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, - { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, - { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, - { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "habluetooth" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-interrupt" }, - { name = "bleak" }, - { name = "bleak-retry-connector" }, - { name = "bluetooth-adapters" }, - { name = "bluetooth-auto-recovery" }, - { name = "bluetooth-data-tools" }, - { name = "btsocket" }, - { name = "dbus-fast", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/60/2395a9b8c438fda49dba19c8d40a701a67c7c75640dd8f7a044a8c221eef/habluetooth-5.0.1.tar.gz", hash = "sha256:dfa720b0c2b03d6380ae3d474061c4fe78e58523f4baa208d0f8f5f8f3a8663c", size = 45433, upload-time = "2025-08-09T07:29:52.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/28/5a9676170a44c038ec6f93e51d330318de2139cae6d79067a1daae007bf3/habluetooth-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f6aac5b5d904ccf7a0cb8d2353ffbdcd9384e403c21a11d999e514f21d310bb", size = 607787, upload-time = "2025-08-09T07:42:40.332Z" }, - { url = "https://files.pythonhosted.org/packages/56/c7/094b571ea158c722275190fc91d1883642a5b245b73fc5635547db0c51d5/habluetooth-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95fca9eb3a8bcdbb86990228129f7cf2159d100b2cccd862a961f3f22c1e042c", size = 567320, upload-time = "2025-08-09T07:42:41.57Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9c/e7a901e265aa3c4afbaffa6b99b9c2436aa98352785ad3ca58e39740d8a6/habluetooth-5.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18ac447c09c0f2edcdd9152e15b707338ea3e6c903e35fee14a5f4820e6d64e1", size = 719517, upload-time = "2025-08-09T07:42:42.813Z" }, - { url = "https://files.pythonhosted.org/packages/77/60/ef1773b5412ca0ffcf2d9a25246644fc55dfdae0ba3131aa42a3cd384a13/habluetooth-5.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c55c6b7de8c64a2a23522d40fea7f60ccc0040d91b377e635f4ad4f26925ce49", size = 693819, upload-time = "2025-08-09T07:42:44.109Z" }, - { url = "https://files.pythonhosted.org/packages/0a/da/ef47d4adbfb9e894c9d8dde86ae8756609365bdb965deed473acb1712823/habluetooth-5.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62263daf0bed0c227bab14924e624f9ca8af483939a9be847844ea388fab971d", size = 779447, upload-time = "2025-08-09T07:42:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f5/55c2641f736d2d258526e2fd81584e7b3e9656bb7123ad6cc013597e4ce4/habluetooth-5.0.1-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ee08ae031f594683a236c359ed6d5fe2fa53fe1dca57229df5bd4b238cba61f3", size = 746598, upload-time = "2025-08-09T07:29:51.122Z" }, - { url = "https://files.pythonhosted.org/packages/bb/82/dd6ae16b920d6356c5a448c8e1a454570b391b470e09a0ecdd1c91d14ac7/habluetooth-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e8e45c746e31d86c93347054bd6a36d802ca873238b7f1da0a9a9830bc4caca7", size = 724755, upload-time = "2025-08-09T07:42:46.699Z" }, - { url = "https://files.pythonhosted.org/packages/d9/55/03d34af8b29508ed49dbd59eea46aac72247c874bf31e722b50fdc8d78c4/habluetooth-5.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7aa09c6252f5a1f2bcb94c22ec6c9ac5e3e25369a11674e43de60afe7b345568", size = 695255, upload-time = "2025-08-09T07:42:47.949Z" }, - { url = "https://files.pythonhosted.org/packages/21/96/b1ef001f97f0be242ca10f0c058093e8c6096d053bafd9bc4c5ca8105848/habluetooth-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fa229e2f0f09407f1471afecd4d318cfaf4e50c8f5d9bdc73a65226ab4810c6", size = 786896, upload-time = "2025-08-09T07:42:49.678Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/8d6fc5df88d6d71abaf9e6106189dacd4bbf6c48a5479b0676e4eb9ac7bf/habluetooth-5.0.1-cp313-cp313-win32.whl", hash = "sha256:173df6fb4cba6cef2605a1a6e178417143ecaf82ad7f3086693d13b0638743a0", size = 488999, upload-time = "2025-08-09T07:42:50.973Z" }, - { url = "https://files.pythonhosted.org/packages/44/1e/c377af6df7e88ecf5d0293d10b46d7da0cd9ac6076f14332799f27eeb48f/habluetooth-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:7690ea34c16ce37d9e7c9ad59c662d8f17d6069d235a72d323d6febe664ce764", size = 559081, upload-time = "2025-08-09T07:42:52.208Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c3/6714632a540f0cb130e8eacee92e29a732b90e5e6250f29933f691590e1b/habluetooth-5.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d46c67de552d3db96e000ce4031e388735681882a2d95a437b6e0138db918e9", size = 607802, upload-time = "2025-08-09T07:42:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b5/53fa82f71a6e74c6afcda9c92e16f3339af9a546ea17099edf0c17956111/habluetooth-5.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ddc5644c6c6b2a80ff9c826f901ca15748a020b8c7e162ab39fc35b49bbecf17", size = 570515, upload-time = "2025-08-09T07:42:54.785Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d2/00ea366636ba34ab338670026935db1d270d12c42649754eba402eb82fae/habluetooth-5.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86825b3c10e0fa43a469af6b5aad6dbfb012d90dcc039936ec441b9e908b70c1", size = 725920, upload-time = "2025-08-09T07:42:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/73/b5/d6838c17a2e52a90020ee807bfc9b06a7d95f2c011223b42b5a170b4d02c/habluetooth-5.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beda16e0c9272a077771c12f4b50cf43a3aa5173d71dbc4794ae68dc98aa3cad", size = 782391, upload-time = "2025-08-09T07:42:57.988Z" }, - { url = "https://files.pythonhosted.org/packages/fa/89/22d21a3450385a6cf725f8f9fe77b509008dc36e670af68f0af8ae5c3cbd/habluetooth-5.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30cbd5f37cc8aa2644db93c3a01c4f2843befc12962c2aa3f8b9aac8b6dfd3c2", size = 731907, upload-time = "2025-08-09T07:42:59.69Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9c/b85c14e38b64b58a480aca4392c39f52630e858a5730ac3ab50b6957e295/habluetooth-5.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1df1af9448deeead2b7ca9cfb89a5e44d6c5068a6a818211eaefb6a8a4ff808", size = 789648, upload-time = "2025-08-09T07:43:00.99Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a2/577339c3211512e41d4f5169616dc5a63d46599c8d75c2b0ce708d462deb/habluetooth-5.0.1-cp314-cp314-win32.whl", hash = "sha256:23740047240da1ebf5a1ba9f99d337310670ae5070c8f960c2bbc3aef061be95", size = 502235, upload-time = "2025-08-09T07:43:02.932Z" }, - { url = "https://files.pythonhosted.org/packages/67/f1/365da12d2c50a89c3fad1944cd045e8bb98d6f81d56e7c7f2765a66714c7/habluetooth-5.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:8327354cbb4a645b63595f8ae04767b97827376389a2c44bc0bfbc37c91f143e", size = 574802, upload-time = "2025-08-09T07:43:04.542Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a2/a785bc064de2e53f12658a371f7f99c3c60500a7cab86c844760dba71e92/habluetooth-5.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:50ee71822ab1bd6b3bbbe449c32701a9cbe5b224560ec8aa2cbde318bdcc51da", size = 607804, upload-time = "2025-08-09T07:43:06.006Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/22abdd1eda5d765c2d7518238dec2623728a170f8cbb0da1258272f97482/habluetooth-5.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:137484d72fd96829c5d16cf3f179ee218fc5155bda56d8c4563accda0094e816", size = 570516, upload-time = "2025-08-09T07:43:07.377Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/c9d3e38c4ac0347588e866bed030776e174021cd8398825caa7724c621f7/habluetooth-5.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0de147f3a393adee328459ee66663783a4b92e994789d37f594e415a047e07", size = 725922, upload-time = "2025-08-09T07:43:08.663Z" }, - { url = "https://files.pythonhosted.org/packages/28/1d/89396ac3cc36bf6b93b04982afb123adb46190a05607642e68f616cd9745/habluetooth-5.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:458ad7112caee189ef5ec22766ab1d9f788a0a6c02ef9a8507b344385a5802f0", size = 782393, upload-time = "2025-08-09T07:43:10.033Z" }, - { url = "https://files.pythonhosted.org/packages/50/de/fb6e0dda73f92010ce341abb6c4ac18a71225268867a27c424b78ab4bffb/habluetooth-5.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a838a76e71f7962c33865c6ed0990c6170def2a72de17d2f4986cc8064370a61", size = 731905, upload-time = "2025-08-09T07:43:11.381Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fc/df94f013a7b239a1c930e920c16a34c65fb827f1b26e3036d5fcb4b6e4f7/habluetooth-5.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d7557cbbb53a3b40fa626eca475c3d95a7fee43d90357655cbad15e7fc3a759d", size = 789649, upload-time = "2025-08-09T07:43:13.024Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d2/403fd160b7d6b6fdb88452daa2185dc90af102c8b5a88028c6de97295fe1/habluetooth-5.0.1-cp314-cp314t-win32.whl", hash = "sha256:b7f96471c2ea4949300fa4abcda3a35a6d7132634fe93378c6a9b9d45cc32c90", size = 502237, upload-time = "2025-08-09T07:43:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/a1/04/13539b05982e20e568aac9850c84712060f395f9cbf22bfccfff17757437/habluetooth-5.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2d9a13a13b105ee3712bdfbec3ac17baffd311c24d5a29c8e9c129eb362252e", size = 574803, upload-time = "2025-08-09T07:43:15.564Z" }, -] - -[[package]] -name = "hass-nabucasa" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "acme" }, - { name = "aiohttp" }, - { name = "async-timeout" }, - { name = "atomicwrites-homeassistant" }, - { name = "attrs" }, - { name = "ciso8601" }, - { name = "cryptography" }, - { name = "josepy" }, - { name = "pycognito" }, - { name = "pyjwt" }, - { name = "sentence-stream" }, - { name = "snitun" }, - { name = "webrtc-models" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ea/95/0c5bb462371581c3d347ff0db7a6f20ec61b678d29db453a0d14c9294e79/hass_nabucasa-1.0.0.tar.gz", hash = "sha256:7c379e9abc8c535e20538cb203827e3273e2ec2288da9505e67a92bc81e631dc", size = 91313, upload-time = "2025-08-14T07:43:02.43Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/63/2ac25cc20d66b3a4f0a0f3c7bb10dc57af5f3382a6349930a8c67f536d38/hass_nabucasa-1.0.0-py3-none-any.whl", hash = "sha256:b4d44c3de5ce370be2d8df881fc3654330faeb055ac09a3fb87b4b08cbd0c0d1", size = 73078, upload-time = "2025-08-14T07:43:00.696Z" }, -] - -[[package]] -name = "home-assistant-bluetooth" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "habluetooth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/0e/c05ee603cab1adb847a305bc8f1034cbdbc0a5d15169fcf68c0d6d21e33f/home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e", size = 7607, upload-time = "2025-02-04T16:11:15.259Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/9b/9904cec885cc32c45e8c22cd7e19d9c342e30074fdb7c58f3d5b33ea1adb/home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7", size = 7915, upload-time = "2025-02-04T16:11:13.163Z" }, -] - -[[package]] -name = "homeassistant" -version = "2025.9.0.dev0" -source = { editable = "." } -dependencies = [ - { name = "aiodns" }, - { name = "aiohasupervisor" }, - { name = "aiohttp" }, - { name = "aiohttp-asyncmdnsresolver" }, - { name = "aiohttp-cors" }, - { name = "aiohttp-fast-zlib" }, - { name = "aiozoneinfo" }, - { name = "annotatedyaml" }, - { name = "astral" }, - { name = "async-interrupt" }, - { name = "atomicwrites-homeassistant" }, - { name = "attrs" }, - { name = "audioop-lts" }, - { name = "awesomeversion" }, - { name = "bcrypt" }, - { name = "certifi" }, - { name = "ciso8601" }, - { name = "cronsim" }, - { name = "cryptography" }, - { name = "fnv-hash-fast" }, - { name = "hass-nabucasa" }, - { name = "home-assistant-bluetooth" }, - { name = "httpx" }, - { name = "ifaddr" }, - { name = "jinja2" }, - { name = "lru-dict" }, - { name = "orjson" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "propcache" }, - { name = "psutil-home-assistant" }, - { name = "pyjwt" }, - { name = "pyopenssl" }, - { name = "python-slugify" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "securetar" }, - { name = "sqlalchemy" }, - { name = "standard-aifc" }, - { name = "standard-telnetlib" }, - { name = "typing-extensions" }, - { name = "ulid-transform" }, - { name = "urllib3" }, - { name = "uv" }, - { name = "voluptuous" }, - { name = "voluptuous-openapi" }, - { name = "voluptuous-serialize" }, - { name = "webrtc-models" }, - { name = "yarl" }, - { name = "zeroconf" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiodns", specifier = "==3.5.0" }, - { name = "aiohasupervisor", specifier = "==0.3.1" }, - { name = "aiohttp", specifier = "==3.12.15" }, - { name = "aiohttp-asyncmdnsresolver", specifier = "==0.1.1" }, - { name = "aiohttp-cors", specifier = "==0.8.1" }, - { name = "aiohttp-fast-zlib", specifier = "==0.3.0" }, - { name = "aiozoneinfo", specifier = "==0.2.3" }, - { name = "annotatedyaml", specifier = "==0.4.5" }, - { name = "astral", specifier = "==2.2" }, - { name = "async-interrupt", specifier = "==1.2.2" }, - { name = "atomicwrites-homeassistant", specifier = "==1.4.1" }, - { name = "attrs", specifier = "==25.3.0" }, - { name = "audioop-lts", specifier = "==0.2.1" }, - { name = "awesomeversion", specifier = "==25.5.0" }, - { name = "bcrypt", specifier = "==4.3.0" }, - { name = "certifi", specifier = ">=2021.5.30" }, - { name = "ciso8601", specifier = "==2.3.2" }, - { name = "cronsim", specifier = "==2.6" }, - { name = "cryptography", specifier = "==45.0.3" }, - { name = "fnv-hash-fast", specifier = "==1.5.0" }, - { name = "hass-nabucasa", specifier = "==1.0.0" }, - { name = "home-assistant-bluetooth", specifier = "==1.13.1" }, - { name = "httpx", specifier = "==0.28.1" }, - { name = "ifaddr", specifier = "==0.2.0" }, - { name = "jinja2", specifier = "==3.1.6" }, - { name = "lru-dict", specifier = "==1.3.0" }, - { name = "orjson", specifier = "==3.11.2" }, - { name = "packaging", specifier = ">=23.1" }, - { name = "pillow", specifier = "==11.3.0" }, - { name = "propcache", specifier = "==0.3.2" }, - { name = "psutil-home-assistant", specifier = "==0.0.1" }, - { name = "pyjwt", specifier = "==2.10.1" }, - { name = "pyopenssl", specifier = "==25.1.0" }, - { name = "python-slugify", specifier = "==8.0.4" }, - { name = "pyyaml", specifier = "==6.0.2" }, - { name = "requests", specifier = "==2.32.4" }, - { name = "securetar", specifier = "==2025.2.1" }, - { name = "sqlalchemy", specifier = "==2.0.41" }, - { name = "standard-aifc", specifier = "==3.13.0" }, - { name = "standard-telnetlib", specifier = "==3.13.0" }, - { name = "typing-extensions", specifier = ">=4.14.0,<5.0" }, - { name = "ulid-transform", specifier = "==1.4.0" }, - { name = "urllib3", specifier = ">=2.0" }, - { name = "uv", specifier = "==0.8.9" }, - { name = "voluptuous", specifier = "==0.15.2" }, - { name = "voluptuous-openapi", specifier = "==0.1.0" }, - { name = "voluptuous-serialize", specifier = "==2.6.0" }, - { name = "webrtc-models", specifier = "==0.3.0" }, - { name = "yarl", specifier = "==1.20.1" }, - { name = "zeroconf", specifier = "==0.147.0" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "ifaddr" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jmespath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, -] - -[[package]] -name = "josepy" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/19/4ebe24c42c341c5868dff072b78d503fc1b0725d88ea619d2db68f5624a9/josepy-2.1.0.tar.gz", hash = "sha256:9beafbaa107ec7128e6c21d86b2bc2aea2f590158e50aca972dca3753046091f", size = 56189, upload-time = "2025-07-08T17:20:54.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/0e/e248f059986e376b87e546cd840d244370b9f7ef2b336456f0c2cfb2fef0/josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd", size = 29065, upload-time = "2025-07-08T17:20:53.504Z" }, -] - -[[package]] -name = "lru-dict" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, -] - -[[package]] -name = "mashumaro" -version = "3.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/92/4c1ac8d819fba3d6988876cadd922803818905a50d22d2027581366e8142/mashumaro-3.16.tar.gz", hash = "sha256:3844137cf053bbac30c4cbd0ee9984e839a5731a0ef96fd3dd9388359af3f2e1", size = 189804, upload-time = "2025-05-20T18:50:50.407Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/25/2142964380b25340d52f6ba5db771625f36ea54118deb94267eecf6e45f1/mashumaro-3.16-py3-none-any.whl", hash = "sha256:d72782cdad5e164748ca883023bc5a214a80835cdca75826bf0bcbff827e0bd3", size = 93990, upload-time = "2025-05-20T18:50:48.494Z" }, -] - -[[package]] -name = "multidict" -version = "6.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, - { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, - { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, - { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, - { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, - { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, - { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, - { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, - { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, - { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, - { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, - { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739, upload-time = "2025-08-12T15:12:28.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419, upload-time = "2025-08-12T15:11:25.517Z" }, - { url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803, upload-time = "2025-08-12T15:11:27.357Z" }, - { url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337, upload-time = "2025-08-12T15:11:28.805Z" }, - { url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222, upload-time = "2025-08-12T15:11:30.18Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020, upload-time = "2025-08-12T15:11:31.59Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721, upload-time = "2025-08-12T15:11:33.035Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574, upload-time = "2025-08-12T15:11:34.433Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225, upload-time = "2025-08-12T15:11:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201, upload-time = "2025-08-12T15:11:37.642Z" }, - { url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193, upload-time = "2025-08-12T15:11:39.153Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548, upload-time = "2025-08-12T15:11:40.768Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798, upload-time = "2025-08-12T15:11:42.164Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402, upload-time = "2025-08-12T15:11:44.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498, upload-time = "2025-08-12T15:11:45.864Z" }, - { url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051, upload-time = "2025-08-12T15:11:47.555Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406, upload-time = "2025-08-12T15:11:49.445Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788, upload-time = "2025-08-12T15:11:50.992Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318, upload-time = "2025-08-12T15:11:52.495Z" }, - { url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231, upload-time = "2025-08-12T15:11:53.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204, upload-time = "2025-08-12T15:11:55.409Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237, upload-time = "2025-08-12T15:11:57.18Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578, upload-time = "2025-08-12T15:11:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799, upload-time = "2025-08-12T15:12:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461, upload-time = "2025-08-12T15:12:01.854Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494, upload-time = "2025-08-12T15:12:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046, upload-time = "2025-08-12T15:12:04.87Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, -] - -[[package]] -name = "propcache" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, -] - -[[package]] -name = "psutil" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, -] - -[[package]] -name = "psutil-home-assistant" -version = "0.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "psutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/4f/32a51f53d645044740d0513a6a029d782b35bdc51a55ea171ce85034f5b7/psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8", size = 6045, upload-time = "2022-08-25T14:28:39.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, -] - -[[package]] -name = "pycares" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/2f/5b46bb8e65070eb1f7f549d2f2e71db6b9899ef24ac9f82128014aeb1e25/pycares-4.10.0.tar.gz", hash = "sha256:9df70dce6e05afa5d477f48959170e569485e20dad1a089c4cf3b2d7ffbd8bf9", size = 654318, upload-time = "2025-08-05T22:35:34.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/bd/7a1448f5f0852628520dc9cdff21b4d6f01f4ab5faaf208d030fba28e0e2/pycares-4.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d4904ebd5e4d0c78e9fd56e6c974da005eaa721365961764922929e8e8f7dd0a", size = 145861, upload-time = "2025-08-05T22:35:00.01Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6d/0e436ddb540a06fa898b8b6cd135babe44893d31d439935eee42bcd4f07b/pycares-4.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7144676e54b0686605333ec62ffdb7bb2b6cb4a6c53eed3e35ae3249dc64676b", size = 140893, upload-time = "2025-08-05T22:35:01.128Z" }, - { url = "https://files.pythonhosted.org/packages/22/7a/ec4734c1274205d0ac1419310464bfa5e1a96924a77312e760790c02769c/pycares-4.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f9a259bf46cc51c51c7402a2bf32d1416f029b9a4af3de8b8973345520278092", size = 637754, upload-time = "2025-08-05T22:35:02.258Z" }, - { url = "https://files.pythonhosted.org/packages/12/1d/306d071837073eccff6efb93560fdb4e53d53ca0c1002260bb34e074f706/pycares-4.10.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1dcfdda868ad2cee8d171288a4cd725a9ad67498a2f679428874a917396d464e", size = 687690, upload-time = "2025-08-05T22:35:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e9/2b517302d42a9ff101201b58e9e2cbd2458c0a1ed68cca7d4dc1397ed246/pycares-4.10.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f2d57bb27c884d130ac62d8c0ac57a158d27f8d75011f8700c7d44601f093652", size = 678273, upload-time = "2025-08-05T22:35:04.794Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bd/de9ed896e752fb22141d6310f6680bcb62ea1d6aa07dc129d914377bd4b4/pycares-4.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:95f4d976bf2feb3f406aef6b1314845dc1384d2e4ea0c439c7d50631f2b6d166", size = 640968, upload-time = "2025-08-05T22:35:05.928Z" }, - { url = "https://files.pythonhosted.org/packages/07/9f/be45f60277a0825d03feed2378a283ce514b4feea64785e917b926b8441e/pycares-4.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f9eecd9e28e43254c6fb1c69518bd6b753bf18230579c23e7f272ac52036d41f", size = 622316, upload-time = "2025-08-05T22:35:07.058Z" }, - { url = "https://files.pythonhosted.org/packages/91/21/ca7bd328d07c560a1fe0ba29008c24a48e88184d3ade658946aeaef25992/pycares-4.10.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f4f8ec43ce0db38152cded6939a3fa4d8aba888e323803cda99f67fa3053fa15", size = 670246, upload-time = "2025-08-05T22:35:08.213Z" }, - { url = "https://files.pythonhosted.org/packages/01/56/47fda9dbc23c3acfe42fa6d57bb850db6ede65a2a9476641a54621166464/pycares-4.10.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef107d30a9d667c295db58897390c2d32c206eb1802b14d98ac643990be4e04f", size = 652930, upload-time = "2025-08-05T22:35:09.701Z" }, - { url = "https://files.pythonhosted.org/packages/86/30/cc865c630d5c9f72f488a89463aabfd33895984955c489f66b5a524f9573/pycares-4.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56c843e69aad724dc5a795f32ebd6fec1d1592f58cabf89d2d148697c22c41be", size = 629187, upload-time = "2025-08-05T22:35:10.954Z" }, - { url = "https://files.pythonhosted.org/packages/92/88/3ff7be2a4bf5a400309d3ffaf9aa58596f7dc6f6fcb99f844fc5e4994a49/pycares-4.10.0-cp313-cp313-win32.whl", hash = "sha256:4310259be37b586ba8cd0b4983689e4c18e15e03709bd88b1076494e91ff424b", size = 118869, upload-time = "2025-08-05T22:35:12.375Z" }, - { url = "https://files.pythonhosted.org/packages/58/5f/cac05cee0556388cabd0abc332021ed01391d6be0685be7b5daff45088f6/pycares-4.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:893020d802afb54d929afda5289fe322b50110cd5386080178479a7381241f97", size = 144512, upload-time = "2025-08-05T22:35:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/45/2e/89b6e83a716935752d62a3c0622a077a9d28f7c2645b7f9b90d6951b37ba/pycares-4.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:ffa3e0f7a13f287b575e64413f2f9af6cf9096e383d1fd40f2870591628d843b", size = 115648, upload-time = "2025-08-05T22:35:15.891Z" }, -] - -[[package]] -name = "pycognito" -version = "2024.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "boto3" }, - { name = "envs" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/67/3975cf257fcc04903686ef87d39be386d894a0d8182f43d37e9cbfc9609f/pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4", size = 31182, upload-time = "2024-05-16T10:02:28.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/7a/f38dd351f47596b22ddbde1b8906e7f43d14be391dcdbd0c2daba886f26c/pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea", size = 26607, upload-time = "2024-05-16T10:02:27.3Z" }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pyobjc-core" -version = "11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/e9/0b85c81e2b441267bca707b5d89f56c2f02578ef8f3eafddf0e0c0b8848c/pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe", size = 974602, upload-time = "2025-06-14T20:56:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/24/12e4e2dae5f85fd0c0b696404ed3374ea6ca398e7db886d4f1322eb30799/pyobjc_core-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c", size = 676431, upload-time = "2025-06-14T20:44:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/f7/79/031492497624de4c728f1857181b06ce8c56444db4d49418fa459cba217c/pyobjc_core-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2", size = 719330, upload-time = "2025-06-14T20:44:51.621Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7d/6169f16a0c7ec15b9381f8bf33872baf912de2ef68d96c798ca4c6ee641f/pyobjc_core-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731", size = 667203, upload-time = "2025-06-14T20:44:53.262Z" }, - { url = "https://files.pythonhosted.org/packages/49/0f/f5ab2b0e57430a3bec9a62b6153c0e79c05a30d77b564efdb9f9446eeac5/pyobjc_core-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96", size = 708807, upload-time = "2025-06-14T20:44:54.851Z" }, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/0b/a01477cde2a040f97e226f3e15e5ffd1268fcb6d1d664885a95ba592eca9/pyobjc_framework_cocoa-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da", size = 389049, upload-time = "2025-06-14T20:46:53.757Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/64cf2661f6ab7c124d0486ec6d1d01a9bb2838a0d2a46006457d8c5e6845/pyobjc_framework_cocoa-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350", size = 393110, upload-time = "2025-06-14T20:46:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/33/87/01e35c5a3c5bbdc93d5925366421e10835fcd7b23347b6c267df1b16d0b3/pyobjc_framework_cocoa-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0", size = 392644, upload-time = "2025-06-14T20:46:56.503Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7c/54afe9ffee547c41e1161691e72067a37ed27466ac71c089bfdcd07ca70d/pyobjc_framework_cocoa-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71", size = 396742, upload-time = "2025-06-14T20:46:57.64Z" }, -] - -[[package]] -name = "pyobjc-framework-corebluetooth" -version = "11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fe/2081dfd9413b7b4d719935c33762fbed9cce9dc06430f322d1e2c9dbcd91/pyobjc_framework_corebluetooth-11.1.tar.gz", hash = "sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190", size = 60337, upload-time = "2025-06-14T20:57:05.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/b5/d07cfa229e3fa0cd1cdaa385774c41907941d25b693cf55ad92e8584a3b3/pyobjc_framework_corebluetooth-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:992404b03033ecf637e9174caed70cb22fd1be2a98c16faa699217678e62a5c7", size = 13179, upload-time = "2025-06-14T20:47:30.376Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/476bca43002a6d009aed956d5ed3f3867c8d1dcd085dde8989be7020c495/pyobjc_framework_corebluetooth-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ebb8648f5e33d98446eb1d6c4654ba4fcc15d62bfcb47fa3bbd5596f6ecdb37c", size = 13358, upload-time = "2025-06-14T20:47:31.114Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/6c050dffb9acc49129da54718c545bc5062f61a389ebaa4727bc3ef0b5a9/pyobjc_framework_corebluetooth-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:e84cbf52006a93d937b90421ada0bc4a146d6d348eb40ae10d5bd2256cc92206", size = 13245, upload-time = "2025-06-14T20:47:31.939Z" }, - { url = "https://files.pythonhosted.org/packages/36/15/9068e8cb108e19e8e86cbf50026bb4c509d85a5d55e2d4c36e292be94337/pyobjc_framework_corebluetooth-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:4da1106265d7efd3f726bacdf13ba9528cc380fb534b5af38b22a397e6908291", size = 13439, upload-time = "2025-06-14T20:47:32.66Z" }, -] - -[[package]] -name = "pyobjc-framework-libdispatch" -version = "11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/89/7830c293ba71feb086cb1551455757f26a7e2abd12f360d375aae32a4d7d/pyobjc_framework_libdispatch-11.1.tar.gz", hash = "sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87", size = 53942, upload-time = "2025-06-14T20:57:45.903Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/10/5851b68cd85b475ff1da08e908693819fd9a4ff07c079da9b0b6dbdaca9c/pyobjc_framework_libdispatch-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c4e219849f5426745eb429f3aee58342a59f81e3144b37aa20e81dacc6177de1", size = 15648, upload-time = "2025-06-14T20:50:59.809Z" }, - { url = "https://files.pythonhosted.org/packages/1b/79/f905f22b976e222a50d49e85fbd7f32d97e8790dd80a55f3f0c305305c32/pyobjc_framework_libdispatch-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a9357736cb47b4a789f59f8fab9b0d10b0a9c84f9876367c398718d3de085888", size = 15912, upload-time = "2025-06-14T20:51:00.572Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/225a3645ba2711c3122eec3e857ea003646643b4122bd98db2a8831740ff/pyobjc_framework_libdispatch-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:cd08f32ea7724906ef504a0fd40a32e2a0be4d64b9239530a31767ca9ccfc921", size = 15655, upload-time = "2025-06-14T20:51:01.655Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b5/ff49fb81f13c7ec48cd7ccad66e1986ccc6aa1984e04f4a78074748f7926/pyobjc_framework_libdispatch-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:5d9985b0e050cae72bf2c6a1cc8180ff4fa3a812cd63b2dc59e09c6f7f6263a1", size = 15920, upload-time = "2025-06-14T20:51:02.407Z" }, -] - -[[package]] -name = "pyopenssl" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, -] - -[[package]] -name = "pyrfc3339" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/d2/6587e8ec3951cbd97c56333d11e0f8a3a4cb64c0d6ed101882b7b31c431f/pyrfc3339-2.0.1.tar.gz", hash = "sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b", size = 4573, upload-time = "2024-11-04T01:57:09.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/ba/d778be0f6d8d583307b47ba481c95f88a59d5c6dfd5d136bc56656d1d17f/pyRFC3339-2.0.1-py3-none-any.whl", hash = "sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d", size = 5777, upload-time = "2024-11-04T01:57:08.185Z" }, -] - -[[package]] -name = "pyric" -version = "0.1.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" } - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-slugify" -version = "8.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "text-unidecode" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, -] - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "s3transfer" -version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, -] - -[[package]] -name = "securetar" -version = "2025.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/5b/da5f56ad39cbb1ca49bd0d4cccde7e97ea7d01fa724fa953746fa2b32ee6/securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1", size = 16124, upload-time = "2025-02-25T14:17:51.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/e0/b93a18e9bb7f7d2573a9c6819d42d996851edde0b0406d017067d7d23a0a/securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4", size = 11545, upload-time = "2025-02-25T14:17:50.832Z" }, -] - -[[package]] -name = "sentence-stream" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/61/51918209769d7373c9bcaecac6222fb494b1d1f272e818e515e5129ef89c/sentence_stream-1.1.0.tar.gz", hash = "sha256:a512604a9f43d4132e29ad04664e8b1778f4a20265799ac86e8d62d181009483", size = 9262, upload-time = "2025-07-24T15:37:37.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/c8/8e39ad90b52372ed3bd1254450ef69f55f7920a838f906e29a414ffcf4b2/sentence_stream-1.1.0-py3-none-any.whl", hash = "sha256:3fceb47673ff16f5e301d7d0935db18413f8f1143ba4aea7ea2d9f808c5f1436", size = 7989, upload-time = "2025-07-24T15:37:36.606Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "snitun" -version = "0.44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/83/acef455bd45428b512148db8c67ffdbb5e3460ab4e036dd896de15db0e7b/snitun-0.44.0.tar.gz", hash = "sha256:b9f693568ea6a7da6a9fa459597a404c1657bfb9259eb076005a8eb1247df087", size = 41098, upload-time = "2025-07-22T21:42:19.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/77/6b58e87ea1ced25cd90bb90e1def088485fae8e35771255943a4bd9c72ab/snitun-0.44.0-py3-none-any.whl", hash = "sha256:8c351ed936c9768d68b1dc5a33ad91c1b8d57cad09f29e73e0b19df0e573c08b", size = 48365, upload-time = "2025-07-22T21:42:18.013Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.41" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, - { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, - { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, -] - -[[package]] -name = "standard-aifc" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "audioop-lts" }, - { name = "standard-chunk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, -] - -[[package]] -name = "standard-chunk" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, -] - -[[package]] -name = "standard-telnetlib" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, -] - -[[package]] -name = "text-unidecode" -version = "1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, -] - -[[package]] -name = "uart-devices" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/08/a8fd6b3dd2cb92344fb4239d4e81ee121767430d7ce71f3f41282f7334e0/uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34", size = 5167, upload-time = "2025-02-22T16:47:05.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/64/edf33c2d7fba7d6bf057c9dc4235bfc699517ea4c996240a1a9c2bf51c29/uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123", size = 4827, upload-time = "2025-02-22T16:47:04.286Z" }, -] - -[[package]] -name = "ulid-transform" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/f2/16c8e6f3d82debedeb1b09bec889ad4a1ca8a71d2d269c156dd80d049c2e/ulid_transform-1.4.0.tar.gz", hash = "sha256:5914a3c4277b0d25ebb67f47bfee2167ac858d970249ea275221fb3e5d91c9a0", size = 16023, upload-time = "2025-03-07T10:44:02.653Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/1d/c43d3e1bda52a321f6cde3526b3634602958dc8ccf1f20fd6616767fd1a1/ulid_transform-1.4.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:9b1429ca7403696b290e4e97ffadbf8ed0b7470a97ad7e273372c3deae5bfb2f", size = 51566, upload-time = "2025-03-07T10:44:00.79Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "usb-devices" -version = "0.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/48/dbe6c4c559950ebebd413e8c40a8a60bfd47ddd79cb61b598a5987e03aad/usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d", size = 5421, upload-time = "2023-12-16T19:59:53.295Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c9/26171ae5b78d72dd006bbc51ca9baa2cbb889ae8e91608910207482108fd/usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf", size = 5349, upload-time = "2023-12-16T19:59:51.604Z" }, -] - -[[package]] -name = "uv" -version = "0.8.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/a1/4dea87c10875b441d906f82df42d725a4a04c2e8ae720d9fa01e1f75e3dc/uv-0.8.9.tar.gz", hash = "sha256:54d76faf5338d1e5643a32b048c600de0cdaa7084e5909106103df04f3306615", size = 3478291, upload-time = "2025-08-12T02:32:37.187Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/d8/a2a24d30660b5f05f86699f86b642b1193bea1017e77e5e5d3e1c64f7bcc/uv-0.8.9-py3-none-linux_armv6l.whl", hash = "sha256:4633c693c79c57a77c52608cbca8a6bb17801bfa223326fbc5c5142654c23cc3", size = 18477020, upload-time = "2025-08-12T02:31:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/4d/21/937e590fb08ce4c82503fddb08b54613c0d42dd06c660460f8f0552dd3a7/uv-0.8.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cdc11cbc81824e51ebb1bac35745a79048557e869ef9da458e99f1c3a96c7f9", size = 18486975, upload-time = "2025-08-12T02:31:54.804Z" }, - { url = "https://files.pythonhosted.org/packages/60/a8/e6fc3e204731aa26b09934bbdecc8d6baa58a2d9e55b59b13130bacf8e52/uv-0.8.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b20ee83e3bf294e0b1347d0b27c56ea1a4fa7eeff4361fbf1f39587d4273059", size = 17178749, upload-time = "2025-08-12T02:31:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3e/3104a054bb6e866503a13114ee969d4b66227ebab19a38e3468f36c03a87/uv-0.8.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3418315e624f60a1c4ed37987b35d5ff0d03961d380e7e7946a3378499d5d779", size = 17790897, upload-time = "2025-08-12T02:31:59.451Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/ab64cca644f40bf85fb9b3a9050aad25af7882a1d774a384fc473ef9c697/uv-0.8.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7efe01b3ed9816e07e6cd4e088472a558a1d2946177f31002b4c42cd55cb4604", size = 18124831, upload-time = "2025-08-12T02:32:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/08/d1/68a001e3ad5d0601ea9ff348b54a78c8ba87fd2a6b6b5e27b379f6f3dff0/uv-0.8.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e571132495d7ab24d2f0270c559d6facd4224745d9db7dff8c20ec0c71ae105a", size = 18924774, upload-time = "2025-08-12T02:32:04.479Z" }, - { url = "https://files.pythonhosted.org/packages/ed/71/1b252e523eb875aa4ac8d06d5f8df175fa2d29e13da347d5d4823bce6c47/uv-0.8.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:67507c66837d8465daaad9f2ccd7da7af981d8c94eb8e32798f62a98c28de82d", size = 20256335, upload-time = "2025-08-12T02:32:07.12Z" }, - { url = "https://files.pythonhosted.org/packages/30/fc/062a25088b30a0fd27e4cc46baa272dd816acdec252b120d05a16d63170a/uv-0.8.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3162f495805a26fba5aacbee49c8650e1e74313c7a2e6df6aec5de9d1299087", size = 19920018, upload-time = "2025-08-12T02:32:10.041Z" }, - { url = "https://files.pythonhosted.org/packages/d8/55/90a0dc35938e68509ff8e8a49ff45b0fd13f3a44752e37d8967cd9d19316/uv-0.8.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60eb70afeb1c66180e12a15afd706bcc0968dbefccf7ef6e5d27a1aaa765419b", size = 19235553, upload-time = "2025-08-12T02:32:12.361Z" }, - { url = "https://files.pythonhosted.org/packages/ae/a4/2db5939a3a993a06bca0a42e2120b4385bf1a4ff54242780701759252052/uv-0.8.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011d2b2d4781555f7f7d29d2f0d6b2638fc60eeff479406ed570052664589e6a", size = 19259174, upload-time = "2025-08-12T02:32:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c9/c52249b5f40f8eb2157587ae4b997942335e4df312dfb83b16b5ebdecc61/uv-0.8.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:97621843e087a68c0b4969676367d757e1de43c00a9f554eb7da35641bdff8a2", size = 18048069, upload-time = "2025-08-12T02:32:16.955Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ca/524137719fb09477e57c5983fa8864f824f5858b29fc679c0416634b79f0/uv-0.8.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b1be6a7b49d23b75d598691cc5c065a9e3cdf5e6e75d7b7f42f24d758ceef3c4", size = 18943440, upload-time = "2025-08-12T02:32:19.212Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b8/877bf9a52207023a8bf9b762bed3853697ed71c5c9911a4e31231de49a23/uv-0.8.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:91598361309c3601382c552dc22256f70b2491ad03357b66caa4be6fdf1111dd", size = 18075581, upload-time = "2025-08-12T02:32:21.732Z" }, - { url = "https://files.pythonhosted.org/packages/96/de/272d4111ff71765bcbfd3ecb4d4fff4073f08cc38b3ecdb7272518c3fe93/uv-0.8.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc81df9dd7571756e34255592caab92821652face35c3f52ad05efaa4bcc39d3", size = 18420275, upload-time = "2025-08-12T02:32:24.488Z" }, - { url = "https://files.pythonhosted.org/packages/90/15/fecfc6665d1bfc5c7dbd32ff1d63413ac43d7f6d16d76fdc4d2513cbe807/uv-0.8.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9ef728e0a5caa2bb129c009a68b30819552e7addf934916a466116e302748bed", size = 19354288, upload-time = "2025-08-12T02:32:27.714Z" }, - { url = "https://files.pythonhosted.org/packages/52/b5/9fef88ac0cc3ca71ff718fa7d7e90c1b3a8639b041c674825aae00d24bf5/uv-0.8.9-py3-none-win32.whl", hash = "sha256:a347c2f2630a45a3b7ceae28a78a528137edfec4847bb29da1561bd8d1f7d254", size = 18197270, upload-time = "2025-08-12T02:32:30.288Z" }, - { url = "https://files.pythonhosted.org/packages/04/0a/dacd483c9726d2b74e42ee1f186aabab508222114f3099a7610ad0f78004/uv-0.8.9-py3-none-win_amd64.whl", hash = "sha256:dc12048cdb53210d0c7218bb403ad30118b1fe8eeff3fbcc184c13c26fcc47d4", size = 20221458, upload-time = "2025-08-12T02:32:32.706Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7e/f2b35278304673dcf9e8fe84b6d15531d91c59530dcf7919111f39a8d28f/uv-0.8.9-py3-none-win_arm64.whl", hash = "sha256:53332de28e9ee00effb695a15cdc70b2455d6b5f6b596d556076b5dd1fd3aa26", size = 18805689, upload-time = "2025-08-12T02:32:35.036Z" }, -] - -[[package]] -name = "voluptuous" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, -] - -[[package]] -name = "voluptuous-openapi" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "voluptuous" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/20/ed87b130ae62076b731521b3c4bc502e6ba8cc92def09954e4e755934804/voluptuous_openapi-0.1.0.tar.gz", hash = "sha256:84bc44107c472ba8782f7a4cb342d19d155d5fe7f92367f092cd96cc850ff1b7", size = 14656, upload-time = "2025-05-11T21:10:14.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/3b/9e689d9fc68f0032bf5b7cbf767fc8bd4771d75cddaf01267fcc05490061/voluptuous_openapi-0.1.0-py3-none-any.whl", hash = "sha256:c3aac740286d368c90a99e007d55ddca7fcddf790d218c60ee0eeec2fcd3db2b", size = 9967, upload-time = "2025-05-11T21:10:13.647Z" }, -] - -[[package]] -name = "voluptuous-serialize" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "voluptuous" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/09/c26b38ab35d9f61e9bf5c3e805215db1316dd73c77569b47ab36a40d19b1/voluptuous-serialize-2.6.0.tar.gz", hash = "sha256:79acdc58239582a393144402d827fa8efd6df0f5350cdc606d9242f6f9bca7c4", size = 7562, upload-time = "2023-02-15T21:09:08.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/86/355e1c65934760e2fb037219f1f360562567cf6731d281440c1d57d36856/voluptuous_serialize-2.6.0-py3-none-any.whl", hash = "sha256:85a5c8d4d829cb49186c1b5396a8a517413cc5938e1bb0e374350190cd139616", size = 6819, upload-time = "2023-02-15T21:09:06.512Z" }, -] - -[[package]] -name = "webrtc-models" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mashumaro" }, - { name = "orjson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/e8/050ffe3b71ff44d3885eee2bed763ca937e2a30bc950d866f22ba657776b/webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526", size = 9411, upload-time = "2024-11-18T17:43:45.682Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/e7/62f29980c9e8d75af93b642a0c37aa8e201fd5268ba3a7179c172549bac3/webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea", size = 7476, upload-time = "2024-11-18T17:43:44.165Z" }, -] - -[[package]] -name = "winrt-runtime" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/d4/1a555d8bdcb8b920f8e896232c82901cc0cda6d3e4f92842199ae7dff70a/winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1", size = 210022, upload-time = "2025-06-06T06:44:11.767Z" }, - { url = "https://files.pythonhosted.org/packages/aa/24/2b6e536ca7745d788dfd17a2ec376fa03a8c7116dc638bb39b035635484f/winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d", size = 241349, upload-time = "2025-06-06T06:44:12.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7f/6d72973279e2929b2a71ed94198ad4a5d63ee2936e91a11860bf7b431410/winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159", size = 415126, upload-time = "2025-06-06T06:44:13.702Z" }, -] - -[[package]] -name = "winrt-windows-devices-bluetooth" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/cc/797516c5c0f8d7f5b680862e0ed7c1087c58aec0bcf57a417fa90f7eb983/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4", size = 105757, upload-time = "2025-06-06T07:00:13.269Z" }, - { url = "https://files.pythonhosted.org/packages/05/6d/f60588846a065e69a2ec5e67c5f85eb45cb7edef2ee8974cd52fa8504de6/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3", size = 113363, upload-time = "2025-06-06T07:00:14.135Z" }, - { url = "https://files.pythonhosted.org/packages/2c/13/2d3c4762018b26a9f66879676ea15d7551cdbf339c8e8e0c56ea05ea31ef/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2", size = 104722, upload-time = "2025-06-06T07:00:14.999Z" }, -] - -[[package]] -name = "winrt-windows-devices-bluetooth-advertisement" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/01/8fc8e57605ea08dd0723c035ed0c2d0435dace2bc80a66d33aecfea49a56/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90", size = 90037, upload-time = "2025-06-06T07:00:25.818Z" }, - { url = "https://files.pythonhosted.org/packages/86/83/503cf815d84c5ba8c8bc61480f32e55579ebf76630163405f7df39aa297b/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943", size = 95822, upload-time = "2025-06-06T07:00:26.666Z" }, - { url = "https://files.pythonhosted.org/packages/32/13/052be8b6642e6f509b30c194312b37bfee8b6b60ac3bd5ca2968c3ea5b80/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d", size = 89326, upload-time = "2025-06-06T07:00:27.477Z" }, -] - -[[package]] -name = "winrt-windows-devices-bluetooth-genericattributeprofile" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/93/30b45ce473d1a604908221a1fa035fe8d5e4bb9008e820ae671a21dab94c/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0", size = 183342, upload-time = "2025-06-06T07:00:56.16Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3b/eb9d99b82a36002d7885206d00ea34f4a23db69c16c94816434ded728fa3/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30", size = 187844, upload-time = "2025-06-06T07:00:57.134Z" }, - { url = "https://files.pythonhosted.org/packages/84/9b/ebbbe9be9a3e640dcfc5f166eb48f2f9d8ce42553f83aa9f4c5dcd9eb5f5/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383", size = 184540, upload-time = "2025-06-06T07:00:58.081Z" }, -] - -[[package]] -name = "winrt-windows-devices-enumeration" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/7d/ebd712ab8ccd599c593796fbcd606abe22b5a8e20db134aa87987d67ac0e/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9", size = 130276, upload-time = "2025-06-06T07:02:05.178Z" }, - { url = "https://files.pythonhosted.org/packages/70/de/f30daaaa0e6f4edb6bd7ddb3e058bd453c9ad90c032a4545c4d4639338aa/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015", size = 141536, upload-time = "2025-06-06T07:02:06.067Z" }, - { url = "https://files.pythonhosted.org/packages/75/4b/9a6aafdc74a085c550641a325be463bf4b811f6f605766c9cd4f4b5c19d2/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4", size = 135362, upload-time = "2025-06-06T07:02:06.997Z" }, -] - -[[package]] -name = "winrt-windows-foundation" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/71/5e87131e4aecc8546c76b9e190bfe4e1292d028bda3f9dd03b005d19c76c/winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46", size = 112184, upload-time = "2025-06-06T07:11:04.459Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7f/8d5108461351d4f6017f550af8874e90c14007f9122fa2eab9f9e0e9b4e1/winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479", size = 118672, upload-time = "2025-06-06T07:11:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/44/f5/2edf70922a3d03500dab17121b90d368979bd30016f6dbca0d043f0c71f1/winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4", size = 109673, upload-time = "2025-06-06T07:11:06.398Z" }, -] - -[[package]] -name = "winrt-windows-foundation-collections" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/cd/99ef050d80bea2922fa1ded93e5c250732634095d8bd3595dd808083e5ca/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9", size = 60063, upload-time = "2025-06-06T07:11:18.65Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/4f75fd6a4c96f1e9bee198c5dc9a9b57e87a9c38117e1b5e423401886353/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10", size = 69057, upload-time = "2025-06-06T07:11:19.446Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/de47ccc390017ec5575e7e7fd9f659ee3747c52049cdb2969b1b538ce947/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2", size = 58792, upload-time = "2025-06-06T07:11:20.24Z" }, -] - -[[package]] -name = "winrt-windows-storage-streams" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "winrt-runtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/d2/24d9f59bdc05e741261d5bec3bcea9a848d57714126a263df840e2b515a8/winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163", size = 127774, upload-time = "2025-06-06T14:02:04.752Z" }, - { url = "https://files.pythonhosted.org/packages/15/59/601724453b885265c7779d5f8025b043a68447cbc64ceb9149d674d5b724/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915", size = 131827, upload-time = "2025-06-06T14:02:05.601Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/a419675a6087c9ea496968c9b7805ef234afa585b7483e2269608a12b044/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d", size = 128180, upload-time = "2025-06-06T14:02:06.759Z" }, -] - -[[package]] -name = "yarl" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, -] - -[[package]] -name = "zeroconf" -version = "0.147.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ifaddr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/78/f681afade2a4e7a9ade696cf3d3dcd9905e28720d74c16cafb83b5dd5c0a/zeroconf-0.147.0.tar.gz", hash = "sha256:f517375de6bf2041df826130da41dc7a3e8772176d3076a5da58854c7d2e8d7a", size = 163958, upload-time = "2025-05-03T16:24:54.207Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/83/c6ee14c962b79f616f8f987a52244e877647db3846007fc167f481a81b7d/zeroconf-0.147.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1deedbedea7402754b3a1a05a2a1c881443451ccd600b2a7f979e97dd9fcbe6d", size = 1841229, upload-time = "2025-05-03T16:59:17.783Z" }, - { url = "https://files.pythonhosted.org/packages/91/c0/42c08a8b2c5b6052d48a5517a5d05076b8ee2c0a458ea9bd5e0e2be38c01/zeroconf-0.147.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c57d551e65a2a9b6333b685e3b074601f6e85762e4b4a490c663f1f2e215b24", size = 1697806, upload-time = "2025-05-03T16:59:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/bf/79/d9b440786d62626f2ca4bd692b6c2bbd1e70e1124c56321bac6a2212a5eb/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:095bdb0cd369355ff919e3be930991b38557baaa8292d82f4a4a8567a3944f05", size = 2141482, upload-time = "2025-05-03T16:59:22.067Z" }, - { url = "https://files.pythonhosted.org/packages/48/12/ab7d31620892a7f4d446a3f0261ddb1198318348c039b4a5ec7d9d09579c/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8ae0fe0bb947b3a128af586c76a16b5a7d027daa65e67637b042c745f9b136c4", size = 2315614, upload-time = "2025-05-03T16:59:24.091Z" }, - { url = "https://files.pythonhosted.org/packages/7b/48/2de072ee42e36328e1d80408b70eddf3df0a5b9640db188caa363b3e120f/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cbeea4d8d0c4f6eb5a82099d53f5729b628685039a44c1a84422080f8ec5b0d", size = 2259809, upload-time = "2025-05-03T16:59:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/02/ec/3344b1ed4e60b36dd73cb66c36299c83a356e853e728c68314061498e9cd/zeroconf-0.147.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:728f82417800c5c5dd3298f65cf7a8fef1707123b457d3832dbdf17d38f68840", size = 2096364, upload-time = "2025-05-03T16:59:27.786Z" }, - { url = "https://files.pythonhosted.org/packages/cd/30/5f34363e2d3c25a78fc925edcc5d45d332296a756d698ccfc060bba8a7aa/zeroconf-0.147.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:a2dc9ae96cd49b50d651a78204aafe9f41e907122dc98e719be5376b4dddec6f", size = 2307868, upload-time = "2025-05-03T16:24:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a8/9b4242ae78bd271520e019faf47d8a2b36242b3b1a7fd47ee7510d380734/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9570ab3203cc4bd3ad023737ef4339558cdf1f33a5d45d76ed3fe77e5fa5f57", size = 2295063, upload-time = "2025-05-03T16:59:29.695Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e6/b63e4e09d71e94bfe0d30c6fc80b0e67e3845eb630bcfb056626db070776/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:fd783d9bac258e79d07e2bd164c1962b8f248579392b5078fd607e7bb6760b53", size = 2152284, upload-time = "2025-05-03T16:59:31.598Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/42b990cb7ad997eb9f9fff15c61abff022adc44f5d1e96bd712ed6cd85ab/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b4acc76063cc379774db407dce0263616518bb5135057eb5eeafc447b3c05a81", size = 2498559, upload-time = "2025-05-03T16:59:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/080619bfcfc353deeb8cf7e813eaf73e8e28ff9a8ca7b97b9f0ecbf4d1d6/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:acc5334cb6cb98db3917bf9a3d6b6b7fdd205f8a74fd6f4b885abb4f61098857", size = 2456548, upload-time = "2025-05-03T16:59:35.721Z" }, - { url = "https://files.pythonhosted.org/packages/35/b6/a25b703f418200edd6932d56bbd32cbd087b828579cf223348fa778fb1ff/zeroconf-0.147.0-cp313-cp313-win32.whl", hash = "sha256:7c52c523aa756e67bf18d46db298a5964291f7d868b4a970163432e7d745b992", size = 1427188, upload-time = "2025-05-03T16:59:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ba463435cdb0b38088eae56d508ec6128b9012f58cedab145b1b77e51316/zeroconf-0.147.0-cp313-cp313-win_amd64.whl", hash = "sha256:60f623af0e45fba69f5fe80d7b300c913afe7928fb43f4b9757f0f76f80f0d82", size = 1655531, upload-time = "2025-05-03T16:59:40.65Z" }, -] From 2de572ea11954dc3122ccd8d75c7c674f1de0c9a Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Thu, 28 Aug 2025 06:58:13 -0300 Subject: [PATCH 09/95] Fix ONVIF not displaying sensor and binary_sensor entity names (#151285) --- homeassistant/components/onvif/binary_sensor.py | 2 +- homeassistant/components/onvif/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index d29f732ef67357..7fb27cc7b8078a 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -74,7 +74,7 @@ def __init__( BinarySensorDeviceClass, entry.original_device_class ) self._attr_entity_category = entry.entity_category - self._attr_name = entry.name + self._attr_name = entry.name or entry.original_name else: event = device.events.get_uid(uid) assert event diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index a0162a05f767a7..f6387de009cfef 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -70,7 +70,7 @@ def __init__( SensorDeviceClass, entry.original_device_class ) self._attr_entity_category = entry.entity_category - self._attr_name = entry.name + self._attr_name = entry.name or entry.original_name self._attr_native_unit_of_measurement = entry.unit_of_measurement else: event = device.events.get_uid(uid) From d5e9d2b9dc3d310eee66932ab699c4eb3e706832 Mon Sep 17 00:00:00 2001 From: Arjan <44190435+vingerha@users.noreply.github.com> Date: Thu, 28 Aug 2025 07:50:48 +0200 Subject: [PATCH 10/95] =?UTF-8?q?Adding=20missing:=20Averses=20de=20gr?= =?UTF-8?q?=C3=A8le=20(#151288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/meteo_france/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index cde2812b0595f9..13c52f04a06027 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -49,7 +49,7 @@ "Bancs de Brouillard", "Brouillard dense", ], - ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle"], + ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle", "Averses de grèle"], ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages", "Orage avec grêle"], ATTR_CONDITION_LIGHTNING_RAINY: [ "Pluie orageuses", From 894fb6ee66ddab2408b45578414c8cd2315256de Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 28 Aug 2025 11:57:32 +0200 Subject: [PATCH 11/95] Fix exception countries migration for Alexa Devices (#151292) --- .../components/alexa_devices/__init__.py | 2 +- .../components/alexa_devices/const.py | 18 +++++++++--------- tests/components/alexa_devices/const.py | 1 - tests/components/alexa_devices/test_init.py | 9 +++------ 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 7a4641bc51f961..9407a2d8987fdc 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -48,7 +48,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> ) # Convert country in domain - country = entry.data[CONF_COUNTRY] + country = entry.data[CONF_COUNTRY].lower() domain = COUNTRY_DOMAINS.get(country, country) # Add site to login data diff --git a/homeassistant/components/alexa_devices/const.py b/homeassistant/components/alexa_devices/const.py index 3ade3ad3ecdf80..c60096bae57133 100644 --- a/homeassistant/components/alexa_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -7,21 +7,21 @@ DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" -DEFAULT_DOMAIN = {"domain": "com"} +DEFAULT_DOMAIN = "com" COUNTRY_DOMAINS = { "ar": DEFAULT_DOMAIN, "at": DEFAULT_DOMAIN, - "au": {"domain": "com.au"}, - "be": {"domain": "com.be"}, + "au": "com.au", + "be": "com.be", "br": DEFAULT_DOMAIN, - "gb": {"domain": "co.uk"}, + "gb": "co.uk", "il": DEFAULT_DOMAIN, - "jp": {"domain": "co.jp"}, - "mx": {"domain": "com.mx"}, + "jp": "co.jp", + "mx": "com.mx", "no": DEFAULT_DOMAIN, - "nz": {"domain": "com.au"}, + "nz": "com.au", "pl": DEFAULT_DOMAIN, - "tr": {"domain": "com.tr"}, + "tr": "com.tr", "us": DEFAULT_DOMAIN, - "za": {"domain": "co.za"}, + "za": "co.za", } diff --git a/tests/components/alexa_devices/const.py b/tests/components/alexa_devices/const.py index 6a4dff1c38d005..ca701cd46e84df 100644 --- a/tests/components/alexa_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -1,7 +1,6 @@ """Alexa Devices tests const.""" TEST_CODE = "023123" -TEST_COUNTRY = "IT" TEST_PASSWORD = "fake_password" TEST_SERIAL_NUMBER = "echo_test_serial_number" TEST_USERNAME = "fake_email@gmail.com" diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 7055f8482ccc16..6c3faffd27b83c 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr from . import setup_integration -from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME +from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -42,7 +42,7 @@ async def test_migrate_entry( domain=DOMAIN, title="Amazon Test Account", data={ - CONF_COUNTRY: TEST_COUNTRY, + CONF_COUNTRY: "US", # country should be in COUNTRY_DOMAINS exceptions CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: {"session": "test-session"}, @@ -58,7 +58,4 @@ async def test_migrate_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED assert config_entry.minor_version == 2 - assert ( - config_entry.data[CONF_LOGIN_DATA]["site"] - == f"https://www.amazon.{TEST_COUNTRY}" - ) + assert config_entry.data[CONF_LOGIN_DATA]["site"] == "https://www.amazon.com" From b23bf164f198ca4fe5dffd19915c1dbeccef0dc4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 28 Aug 2025 11:53:46 +0200 Subject: [PATCH 12/95] Add missing state class to Alexa Devices sensors (#151296) --- homeassistant/components/alexa_devices/sensor.py | 3 +++ tests/components/alexa_devices/snapshots/test_sensor.ambr | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa_devices/sensor.py b/homeassistant/components/alexa_devices/sensor.py index 89c2bdce9b7bef..738e0ac2de575f 100644 --- a/homeassistant/components/alexa_devices/sensor.py +++ b/homeassistant/components/alexa_devices/sensor.py @@ -12,6 +12,7 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import LIGHT_LUX, UnitOfTemperature from homeassistant.core import HomeAssistant @@ -41,11 +42,13 @@ class AmazonSensorEntityDescription(SensorEntityDescription): if device.sensors[_key].scale == "CELSIUS" else UnitOfTemperature.FAHRENHEIT ), + state_class=SensorStateClass.MEASUREMENT, ), AmazonSensorEntityDescription( key="illuminance", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/tests/components/alexa_devices/snapshots/test_sensor.ambr b/tests/components/alexa_devices/snapshots/test_sensor.ambr index ae245b5c463800..646119331006ae 100644 --- a/tests/components/alexa_devices/snapshots/test_sensor.ambr +++ b/tests/components/alexa_devices/snapshots/test_sensor.ambr @@ -4,7 +4,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -42,6 +44,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Echo Test Temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , From d0866704bade41bd26638ea0af8c01c09e1579e2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 28 Aug 2025 11:38:08 +0200 Subject: [PATCH 13/95] Fix Reolink duplicates due to wrong merge (#151298) --- homeassistant/components/reolink/number.py | 1 - homeassistant/components/reolink/select.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index 721b14e9daf405..cc2a7b420379f6 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -853,7 +853,6 @@ async def async_setup_entry( ReolinkChimeNumberEntity(reolink_data, chime, entity_description) for entity_description in CHIME_NUMBER_ENTITIES for chime in api.chime_list - for chime in api.chime_list if chime.channel is not None ) async_add_entities(entities) diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 35ed3dbb70e1ad..23510125570ad6 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -380,7 +380,6 @@ async def async_setup_entry( ReolinkChimeSelectEntity(reolink_data, chime, entity_description) for entity_description in CHIME_SELECT_ENTITIES for chime in reolink_data.host.api.chime_list - if entity_description.supported(chime) if entity_description.supported(chime) and chime.channel is not None ) async_add_entities(entities) From 12b161e1548463c3c8d42ae1c0ac584bd62e1c11 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 28 Aug 2025 20:46:43 +0200 Subject: [PATCH 14/95] Fix Z-Wave duplicate notification binary sensors (#151304) --- .../components/zwave_js/binary_sensor.py | 20 +++++++++++++------ tests/components/zwave_js/conftest.py | 7 +++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 1ce035c313df21..fcb62ba9a80297 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -122,6 +122,13 @@ class NewNotificationZWaveJSEntityDescription(BinarySensorEntityDescription): # - Replace water filter # - Sump pump failure + +# This set can be removed once all notification sensors have been migrated +# to use the new discovery schema and we've removed the old discovery code. +MIGRATED_NOTIFICATION_TYPES = { + NotificationType.SMOKE_ALARM, +} + NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = ( NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 @@ -402,6 +409,12 @@ def async_add_binary_sensor( # ensure the notification CC Value is valid as binary sensor if not is_valid_notification_binary_sensor(info): return + if ( + notification_type := info.primary_value.metadata.cc_specific[ + CC_SPECIFIC_NOTIFICATION_TYPE + ] + ) in MIGRATED_NOTIFICATION_TYPES: + return # Get all sensors from Notification CC states for state_key in info.primary_value.metadata.states: if TYPE_CHECKING: @@ -414,12 +427,7 @@ def async_add_binary_sensor( NotificationZWaveJSEntityDescription | None ) = None for description in NOTIFICATION_SENSOR_MAPPINGS: - if ( - int(description.key) - == info.primary_value.metadata.cc_specific[ - CC_SPECIFIC_NOTIFICATION_TYPE - ] - ) and ( + if (int(description.key) == notification_type) and ( not description.states or int(state_key) in description.states ): notification_description = description diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index f60c01690551b3..1a765288cc1a1e 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator import copy import io +import logging from typing import Any, cast from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch @@ -925,6 +926,7 @@ async def integration_fixture( hass: HomeAssistant, client: MagicMock, platforms: list[Platform], + caplog: pytest.LogCaptureFixture, ) -> MockConfigEntry: """Set up the zwave_js integration.""" entry = MockConfigEntry( @@ -939,6 +941,11 @@ async def integration_fixture( client.async_send_command.reset_mock() + # Make sure no errors logged during setup. + # Eg. unique id collisions are only logged as errors and not raised, + # and may not cause tests to fail otherwise. + assert not any(record.levelno == logging.ERROR for record in caplog.records) + return entry From 32cbd2a23948fd322522f5ea60d73761776a2248 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Aug 2025 18:52:51 +0200 Subject: [PATCH 15/95] Improve migration to entity registry version 1.18 (#151308) Co-authored-by: Martin Hjelmare --- homeassistant/helpers/entity_registry.py | 97 ++++-- tests/helpers/test_entity_registry.py | 392 ++++++++++++++++++++++- 2 files changed, 454 insertions(+), 35 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 571f914e9d3074..95aa153ff0090c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -16,7 +16,7 @@ from enum import StrEnum import logging import time -from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict +from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict import attr import voluptuous as vol @@ -85,6 +85,8 @@ CLEANUP_INTERVAL = 3600 * 24 ORPHANED_ENTITY_KEEP_SECONDS = 3600 * 24 * 30 +UNDEFINED_STR: Final = "UNDEFINED" + ENTITY_CATEGORY_VALUE_TO_INDEX: dict[EntityCategory | None, int] = { val: idx for idx, val in enumerate(EntityCategory) } @@ -164,6 +166,17 @@ def _protect_entity_options( return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) +def _protect_optional_entity_options( + data: EntityOptionsType | UndefinedType | None, +) -> ReadOnlyEntityOptionsType | UndefinedType: + """Protect entity options from being modified.""" + if data is UNDEFINED: + return UNDEFINED + if data is None: + return ReadOnlyDict({}) + return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) + + @attr.s(frozen=True, kw_only=True, slots=True) class RegistryEntry: """Entity Registry Entry.""" @@ -414,15 +427,17 @@ class DeletedRegistryEntry: config_subentry_id: str | None = attr.ib() created_at: datetime = attr.ib() device_class: str | None = attr.ib() - disabled_by: RegistryEntryDisabler | None = attr.ib() + disabled_by: RegistryEntryDisabler | UndefinedType | None = attr.ib() domain: str = attr.ib(init=False, repr=False) - hidden_by: RegistryEntryHider | None = attr.ib() + hidden_by: RegistryEntryHider | UndefinedType | None = attr.ib() icon: str | None = attr.ib() id: str = attr.ib() labels: set[str] = attr.ib() modified_at: datetime = attr.ib() name: str | None = attr.ib() - options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options) + options: ReadOnlyEntityOptionsType | UndefinedType = attr.ib( + converter=_protect_optional_entity_options + ) orphaned_timestamp: float | None = attr.ib() _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) @@ -445,15 +460,21 @@ def as_storage_fragment(self) -> json_fragment: "config_subentry_id": self.config_subentry_id, "created_at": self.created_at, "device_class": self.device_class, - "disabled_by": self.disabled_by, + "disabled_by": self.disabled_by + if self.disabled_by is not UNDEFINED + else UNDEFINED_STR, "entity_id": self.entity_id, - "hidden_by": self.hidden_by, + "hidden_by": self.hidden_by + if self.hidden_by is not UNDEFINED + else UNDEFINED_STR, "icon": self.icon, "id": self.id, "labels": list(self.labels), "modified_at": self.modified_at, "name": self.name, - "options": self.options, + "options": self.options + if self.options is not UNDEFINED + else UNDEFINED_STR, "orphaned_timestamp": self.orphaned_timestamp, "platform": self.platform, "unique_id": self.unique_id, @@ -584,12 +605,12 @@ async def _async_migrate_func( # noqa: C901 entity["area_id"] = None entity["categories"] = {} entity["device_class"] = None - entity["disabled_by"] = None - entity["hidden_by"] = None + entity["disabled_by"] = UNDEFINED_STR + entity["hidden_by"] = UNDEFINED_STR entity["icon"] = None entity["labels"] = [] entity["name"] = None - entity["options"] = {} + entity["options"] = UNDEFINED_STR if old_major_version > 1: raise NotImplementedError @@ -958,25 +979,30 @@ def async_get_or_create( categories = deleted_entity.categories created_at = deleted_entity.created_at device_class = deleted_entity.device_class - disabled_by = deleted_entity.disabled_by - # Adjust disabled_by based on config entry state - if config_entry and config_entry is not UNDEFINED: - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = RegistryEntryDisabler.CONFIG_ENTRY + if deleted_entity.disabled_by is not UNDEFINED: + disabled_by = deleted_entity.disabled_by + # Adjust disabled_by based on config entry state + if config_entry and config_entry is not UNDEFINED: + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = RegistryEntryDisabler.CONFIG_ENTRY + elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: + disabled_by = None elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: disabled_by = None - elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: - disabled_by = None # Restore entity_id if it's available if self._entity_id_available(deleted_entity.entity_id): entity_id = deleted_entity.entity_id entity_registry_id = deleted_entity.id - hidden_by = deleted_entity.hidden_by + if deleted_entity.hidden_by is not UNDEFINED: + hidden_by = deleted_entity.hidden_by icon = deleted_entity.icon labels = deleted_entity.labels name = deleted_entity.name - options = deleted_entity.options + if deleted_entity.options is not UNDEFINED: + options = deleted_entity.options + else: + options = get_initial_options() if get_initial_options else None else: aliases = set() area_id = None @@ -1529,6 +1555,20 @@ async def async_load(self) -> None: previous_unique_id=entity["previous_unique_id"], unit_of_measurement=entity["unit_of_measurement"], ) + + def get_optional_enum[_EnumT: StrEnum]( + cls: type[_EnumT], value: str | None + ) -> _EnumT | UndefinedType | None: + """Convert string to the passed enum, UNDEFINED or None.""" + if value is None: + return None + if value == UNDEFINED_STR: + return UNDEFINED + try: + return cls(value) + except ValueError: + return None + for entity in data["deleted_entities"]: try: domain = split_entity_id(entity["entity_id"])[0] @@ -1546,6 +1586,7 @@ async def async_load(self) -> None: entity["platform"], entity["unique_id"], ) + deleted_entities[key] = DeletedRegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], @@ -1554,23 +1595,21 @@ async def async_load(self) -> None: config_subentry_id=entity["config_subentry_id"], created_at=datetime.fromisoformat(entity["created_at"]), device_class=entity["device_class"], - disabled_by=( - RegistryEntryDisabler(entity["disabled_by"]) - if entity["disabled_by"] - else None + disabled_by=get_optional_enum( + RegistryEntryDisabler, entity["disabled_by"] ), entity_id=entity["entity_id"], - hidden_by=( - RegistryEntryHider(entity["hidden_by"]) - if entity["hidden_by"] - else None + hidden_by=get_optional_enum( + RegistryEntryHider, entity["hidden_by"] ), icon=entity["icon"], id=entity["id"], labels=set(entity["labels"]), modified_at=datetime.fromisoformat(entity["modified_at"]), name=entity["name"], - options=entity["options"], + options=entity["options"] + if entity["options"] is not UNDEFINED_STR + else UNDEFINED, orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index acbcb02a5ded8a..da6cdf806d7a82 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -20,6 +20,7 @@ from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.dt import utc_from_timestamp, utcnow from tests.common import ( @@ -962,9 +963,10 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) assert entry.device_class is None assert entry.original_device_class == "best_class" - # Check we store migrated data + # Check migrated data await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == { + migrated_data = hass_storage[er.STORAGE_KEY] + assert migrated_data == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1007,6 +1009,11 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) }, } + # Serialize the migrated data again + registry.async_schedule_save() + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == migrated_data + @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: @@ -1142,9 +1149,17 @@ async def test_migration_1_11( assert entry.device_class is None assert entry.original_device_class == "best_class" + deleted_entry = registry.deleted_entities[ + ("test", "super_duper_platform", "very_very_unique") + ] + assert deleted_entry.disabled_by is UNDEFINED + assert deleted_entry.hidden_by is UNDEFINED + assert deleted_entry.options is UNDEFINED + # Check migrated data await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == { + migrated_data = hass_storage[er.STORAGE_KEY] + assert migrated_data == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1192,15 +1207,15 @@ async def test_migration_1_11( "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", "device_class": None, - "disabled_by": None, + "disabled_by": "UNDEFINED", "entity_id": "test.deleted_entity", - "hidden_by": None, + "hidden_by": "UNDEFINED", "icon": None, "id": "23456", "labels": [], "modified_at": "1970-01-01T00:00:00+00:00", "name": None, - "options": {}, + "options": "UNDEFINED", "orphaned_timestamp": None, "platform": "super_duper_platform", "unique_id": "very_very_unique", @@ -1209,6 +1224,11 @@ async def test_migration_1_11( }, } + # Serialize the migrated data again + registry.async_schedule_save() + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == migrated_data + async def test_update_entity_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry @@ -3150,6 +3170,366 @@ async def test_restore_entity( assert update_events[16].data == {"action": "create", "entity_id": "light.hue_1234"} +@pytest.mark.parametrize( + ("entity_disabled_by"), + [ + None, + er.RegistryEntryDisabler.CONFIG_ENTRY, + er.RegistryEntryDisabler.DEVICE, + er.RegistryEntryDisabler.HASS, + er.RegistryEntryDisabler.INTEGRATION, + er.RegistryEntryDisabler.USER, + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_disabled_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + entity_disabled_by: er.RegistryEntryDisabler | None, +) -> None: + """Check how the disabled_by flag is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, disabled_by=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=entity_disabled_by, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=None, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=entity_disabled_by, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=er.RegistryEntryHider.INTEGRATION, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key1": "value1"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + +@pytest.mark.parametrize( + ("entity_hidden_by"), + [ + None, + er.RegistryEntryHider.INTEGRATION, + er.RegistryEntryHider.USER, + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_hidden_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + entity_hidden_by: er.RegistryEntryHider | None, +) -> None: + """Check how the hidden_by flag is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, hidden_by=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=entity_hidden_by, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=entity_hidden_by, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key1": "value1"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_initial_options( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Check how the initial options is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, options=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=None, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=er.RegistryEntryHider.INTEGRATION, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key2": "value2"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + @pytest.mark.parametrize( ( "config_entry_disabled_by", From 282ec58c4ecc04b87452d7c797b2e44e8d6dd299 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Fri, 29 Aug 2025 11:21:18 +0200 Subject: [PATCH 16/95] Bump asusrouter to 1.20.1 (#151311) --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index c5bdb9440f595b..0fcc6f2d3d0c8b 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioasuswrt", "asusrouter", "asyncssh"], - "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.0"] + "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index d445b533627a5f..8b4303d99fe1b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -528,7 +528,7 @@ arris-tg2492lg==2.2.0 asmog==0.0.6 # homeassistant.components.asuswrt -asusrouter==1.20.0 +asusrouter==1.20.1 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3d5d17bb90a14..0f0f5d52e051dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ aranet4==2.5.1 arcam-fmj==1.8.2 # homeassistant.components.asuswrt -asusrouter==1.20.0 +asusrouter==1.20.1 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms From f49ce2a77a95c77cf063ce32ddb6a7db72ec618d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Aug 2025 16:48:29 +0200 Subject: [PATCH 17/95] Improve migration to device registry version 1.11 (#151315) Co-authored-by: Franck Nijhof --- homeassistant/helpers/device_registry.py | 49 ++++++--- tests/helpers/test_device_registry.py | 131 ++++++++++++++++++++++- 2 files changed, 165 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5e5f50c96fc5ae..d07dfb2da647f5 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -9,7 +9,7 @@ from functools import lru_cache import logging import time -from typing import TYPE_CHECKING, Any, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict import attr from yarl import URL @@ -68,6 +68,8 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +UNDEFINED_STR: Final = "UNDEFINED" + # Can be removed when suggested_area is removed from DeviceEntry RUNTIME_ONLY_ATTRS = {"suggested_area"} @@ -465,7 +467,7 @@ class DeletedDeviceEntry: validator=_normalize_connections_validator ) created_at: datetime = attr.ib() - disabled_by: DeviceEntryDisabler | None = attr.ib() + disabled_by: DeviceEntryDisabler | UndefinedType | None = attr.ib() id: str = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() labels: set[str] = attr.ib() @@ -480,15 +482,19 @@ def to_device_entry( config_subentry_id: str | None, connections: set[tuple[str, str]], identifiers: set[tuple[str, str]], + disabled_by: DeviceEntryDisabler | UndefinedType | None, ) -> DeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" # Adjust disabled_by based on config entry state - disabled_by = self.disabled_by - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = DeviceEntryDisabler.CONFIG_ENTRY - elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: - disabled_by = None + if self.disabled_by is not UNDEFINED: + disabled_by = self.disabled_by + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = DeviceEntryDisabler.CONFIG_ENTRY + elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = disabled_by if disabled_by is not UNDEFINED else None return DeviceEntry( area_id=self.area_id, # type ignores: likely https://github.com/python/mypy/issues/8625 @@ -520,7 +526,9 @@ def as_storage_fragment(self) -> json_fragment: }, "connections": list(self.connections), "created_at": self.created_at, - "disabled_by": self.disabled_by, + "disabled_by": self.disabled_by + if self.disabled_by is not UNDEFINED + else UNDEFINED_STR, "identifiers": list(self.identifiers), "id": self.id, "labels": list(self.labels), @@ -608,7 +616,7 @@ async def _async_migrate_func( # noqa: C901 # Introduced in 2025.6 for device in old_data["deleted_devices"]: device["area_id"] = None - device["disabled_by"] = None + device["disabled_by"] = UNDEFINED_STR device["labels"] = [] device["name_by_user"] = None if old_minor_version < 11: @@ -934,6 +942,7 @@ def async_get_or_create( config_subentry_id if config_subentry_id is not UNDEFINED else None, connections, identifiers, + disabled_by, ) self.devices[device.id] = device # If creating a new device, default to the config entry name @@ -1442,7 +1451,21 @@ async def async_load(self) -> None: sw_version=device["sw_version"], via_device_id=device["via_device_id"], ) + # Introduced in 0.111 + def get_optional_enum[_EnumT: StrEnum]( + cls: type[_EnumT], value: str | None + ) -> _EnumT | UndefinedType | None: + """Convert string to the passed enum, UNDEFINED or None.""" + if value is None: + return None + if value == UNDEFINED_STR: + return UNDEFINED + try: + return cls(value) + except ValueError: + return None + for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( area_id=device["area_id"], @@ -1455,10 +1478,8 @@ async def async_load(self) -> None: }, connections={tuple(conn) for conn in device["connections"]}, created_at=datetime.fromisoformat(device["created_at"]), - disabled_by=( - DeviceEntryDisabler(device["disabled_by"]) - if device["disabled_by"] - else None + disabled_by=get_optional_enum( + DeviceEntryDisabler, device["disabled_by"] ), identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 80910d42630cb5..dfa96fa6051847 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import ANY, patch +import attr from freezegun.api import FrozenDateTimeFactory import pytest from yarl import URL @@ -21,6 +22,7 @@ device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_capture_events, flush_store @@ -508,6 +510,9 @@ async def test_migration_from_1_1( ) assert entry.id == "abcdefghijklm" + deleted_entry = registry.deleted_devices["deletedid"] + assert deleted_entry.disabled_by is UNDEFINED + # Update to trigger a store entry = registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, @@ -581,7 +586,7 @@ async def test_migration_from_1_1( "config_entries_subentries": {"123456": [None]}, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", - "disabled_by": None, + "disabled_by": "UNDEFINED", "id": "deletedid", "identifiers": [["serial", "123456ABCDFF"]], "labels": [], @@ -3833,6 +3838,130 @@ async def test_restore_device( } +@pytest.mark.parametrize( + ("device_disabled_by", "expected_disabled_by"), + [ + (None, None), + (dr.DeviceEntryDisabler.CONFIG_ENTRY, dr.DeviceEntryDisabler.CONFIG_ENTRY), + (dr.DeviceEntryDisabler.INTEGRATION, dr.DeviceEntryDisabler.INTEGRATION), + (dr.DeviceEntryDisabler.USER, dr.DeviceEntryDisabler.USER), + (UNDEFINED, None), + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_device_disabled_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + device_disabled_by: dr.DeviceEntryDisabler | UndefinedType | None, + expected_disabled_by: dr.DeviceEntryDisabler | None, +) -> None: + """Check how the disabled_by flag is treated when restoring a device.""" + entry_id = mock_config_entry.entry_id + update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) + entry = device_registry.async_get_or_create( + config_entry_id=entry_id, + config_subentry_id=None, + configuration_url="http://config_url_orig.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + disabled_by=None, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_orig", + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer_orig", + model="model_orig", + model_id="model_id_orig", + name="name_orig", + serial_number="serial_no_orig", + suggested_area="suggested_area_orig", + sw_version="version_orig", + via_device="via_device_id_orig", + ) + + assert len(device_registry.devices) == 1 + assert len(device_registry.deleted_devices) == 0 + + device_registry.async_remove_device(entry.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + + deleted_entry = device_registry.deleted_devices[entry.id] + device_registry.deleted_devices[entry.id] = attr.evolve( + deleted_entry, disabled_by=UNDEFINED + ) + + # This will restore the original device, user customizations of + # area_id, disabled_by, labels and name_by_user will be restored + entry3 = device_registry.async_get_or_create( + config_entry_id=entry_id, + config_subentry_id=None, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + disabled_by=device_disabled_by, + entry_type=None, + hw_version="hw_version_new", + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + name="name_new", + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", + via_device="via_device_id_new", + ) + assert entry3 == dr.DeviceEntry( + area_id="suggested_area_orig", + config_entries={entry_id}, + config_entries_subentries={entry_id: {None}}, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=expected_disabled_by, + entry_type=None, + hw_version="hw_version_new", + id=entry.id, + identifiers={("bridgeid", "0123")}, + labels=set(), + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + modified_at=utcnow(), + name_by_user=None, + name="name_new", + primary_config_entry=entry_id, + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", + ) + + assert entry.id == entry3.id + assert len(device_registry.devices) == 1 + assert len(device_registry.deleted_devices) == 0 + + assert isinstance(entry3.config_entries, set) + assert isinstance(entry3.connections, set) + assert isinstance(entry3.identifiers, set) + + await hass.async_block_till_done() + + assert len(update_events) == 3 + assert update_events[0].data == { + "action": "create", + "device_id": entry.id, + } + assert update_events[1].data == { + "action": "remove", + "device_id": entry.id, + "device": entry.dict_repr, + } + assert update_events[2].data == { + "action": "create", + "device_id": entry3.id, + } + + @pytest.mark.parametrize( ( "config_entry_disabled_by", From 6f17c1653c22d48b6fff4e677449dd92dc0bf7ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 14:48:41 -0500 Subject: [PATCH 18/95] Bump nexia to 2.11.0 (#151319) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 939b0b6228418b..e72c9170900a22 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.10.0"] + "requirements": ["nexia==2.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b4303d99fe1b6..b0bbd4515d9a92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1509,7 +1509,7 @@ nettigo-air-monitor==5.0.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.10.0 +nexia==2.11.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f0f5d52e051dd..726c44e3b7f82b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1292,7 +1292,7 @@ netmap==0.7.0.2 nettigo-air-monitor==5.0.0 # homeassistant.components.nexia -nexia==2.10.0 +nexia==2.11.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 From 24017a9555a05c4f5e777dd104f92132c627b556 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 28 Aug 2025 22:27:46 +0200 Subject: [PATCH 19/95] Update frontend to 20250828.0 (#151321) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 98840d3be54a11..4ffe4a41c60c0d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250827.0"] + "requirements": ["home-assistant-frontend==20250828.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index da064ae9d88057..6ba850ca474e63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.1.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250827.0 +home-assistant-frontend==20250828.0 home-assistant-intents==2025.8.27 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b0bbd4515d9a92..c58051759a269c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250827.0 +home-assistant-frontend==20250828.0 # homeassistant.components.conversation home-assistant-intents==2025.8.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 726c44e3b7f82b..121ad0cf5a9d6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250827.0 +home-assistant-frontend==20250828.0 # homeassistant.components.conversation home-assistant-intents==2025.8.27 From 68ec41c43ab0d3201af650b4344a33bd7f48c873 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 28 Aug 2025 22:23:46 +0200 Subject: [PATCH 20/95] Bump deebot-client to 13.7.0 (#151327) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index ddd464bdc6a57c..b45c06062eebc0 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c58051759a269c..9ab37b08b1350c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ decora-wifi==1.4 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.6.0 +deebot-client==13.7.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 121ad0cf5a9d6b..b941c2a93237fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,7 +677,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.6.0 +deebot-client==13.7.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 1ffc0560c50386cb4f6a1558d23d472a4020c911 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 04:23:50 -0500 Subject: [PATCH 21/95] Bump habluetooth to 5.2.0 (#151333) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index d29a2cd417ae32..48641131424e9e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==5.1.0" + "habluetooth==5.2.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6ba850ca474e63..f9c59cc22df163 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==5.1.0 +habluetooth==5.2.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9ab37b08b1350c..6865bd1110ae7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.1.0 +habluetooth==5.2.0 # homeassistant.components.cloud hass-nabucasa==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b941c2a93237fe..749ec874124351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.1.0 +habluetooth==5.2.0 # homeassistant.components.cloud hass-nabucasa==1.0.0 From da4ec7b3dd0799e1dc0f69dc500cadac22d2a546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 04:19:44 -0500 Subject: [PATCH 22/95] Bump bleak-retry-connector to 4.4.3 (#151341) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 48641131424e9e..cca12b4daf069e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.4.1", + "bleak-retry-connector==4.4.3", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f9c59cc22df163..a3ab1a54eea7db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.4.1 +bleak-retry-connector==4.4.3 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 diff --git a/requirements_all.txt b/requirements_all.txt index 6865bd1110ae7e..4d54b4ca7748da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.4.1 +bleak-retry-connector==4.4.3 # homeassistant.components.bluetooth bleak==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 749ec874124351..4b2cef6d64bfa2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.4.1 +bleak-retry-connector==4.4.3 # homeassistant.components.bluetooth bleak==1.0.1 From 2d62c5f8d6cb81218af062c445f3f715d17a4133 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 29 Aug 2025 11:20:20 +0200 Subject: [PATCH 23/95] Bump airOS to 0.4.4 (#151345) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 2a2a241aef0276..d08fa6fad2cdd5 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.4.3"] + "requirements": ["airos==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d54b4ca7748da..57d5d45a76ec9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.4.3 +airos==0.4.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b2cef6d64bfa2..0706bd2e8b868a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.4.3 +airos==0.4.4 # homeassistant.components.airthings_ble airthings-ble==0.9.2 From a938a33e987aaa67fe0214dd24b922b7ed8023ca Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 29 Aug 2025 16:51:20 +0200 Subject: [PATCH 24/95] Bump reolink-aio to 0.15.0 (#151367) Co-authored-by: Franck Nijhof --- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/number.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 754ed780cee874..52b46089537d23 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.14.7"] + "requirements": ["reolink-aio==0.15.0"] } diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index cc2a7b420379f6..1904cb7abbdc9e 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -158,9 +158,9 @@ class ReolinkChimeNumberEntityDescription( native_step=1, native_min_value=0, native_max_value=100, - supported=lambda api, ch: api.supported(ch, "volume_speek"), - value=lambda api, ch: api.volume_speek(ch), - method=lambda api, ch, value: api.set_volume(ch, volume_speek=int(value)), + supported=lambda api, ch: api.supported(ch, "volume_speak"), + value=lambda api, ch: api.volume_speak(ch), + method=lambda api, ch, value: api.set_volume(ch, volume_speak=int(value)), ), ReolinkNumberEntityDescription( key="volume_doorbell", diff --git a/requirements_all.txt b/requirements_all.txt index 57d5d45a76ec9a..1694f374212ccb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2667,7 +2667,7 @@ renault-api==0.4.0 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.7 +reolink-aio==0.15.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0706bd2e8b868a..6ed2eca495aac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2213,7 +2213,7 @@ renault-api==0.4.0 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.7 +reolink-aio==0.15.0 # homeassistant.components.rflink rflink==0.0.67 From c69c3e7d85b5c7a6ef3f29d8fd7dbf601745f4de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 09:49:57 -0500 Subject: [PATCH 25/95] Bump nexia to 2.11.1 (#151379) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e72c9170900a22..310091639c7125 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.11.0"] + "requirements": ["nexia==2.11.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1694f374212ccb..82261b072ce724 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1509,7 +1509,7 @@ nettigo-air-monitor==5.0.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.11.0 +nexia==2.11.1 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ed2eca495aac1..bf2872e7cd1c15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1292,7 +1292,7 @@ netmap==0.7.0.2 nettigo-air-monitor==5.0.0 # homeassistant.components.nexia -nexia==2.11.0 +nexia==2.11.1 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 From 0a9203e24145f63aa5276b4eddf3384dc9d47811 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 09:51:56 -0500 Subject: [PATCH 26/95] Bump bleak-esphome to 3.2.0 (#151380) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 472384fdf7d778..802ddae36e93c9 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.1.0"] + "requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.2.0"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ffb02571742743..c5841da4467d98 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==39.0.0", "esphome-dashboard-api==1.3.0", - "bleak-esphome==3.1.0" + "bleak-esphome==3.2.0" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 82261b072ce724..e1086fae83ad86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==3.1.0 +bleak-esphome==3.2.0 # homeassistant.components.bluetooth bleak-retry-connector==4.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf2872e7cd1c15..55ce98e11ee8df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==3.1.0 +bleak-esphome==3.2.0 # homeassistant.components.bluetooth bleak-retry-connector==4.4.3 From 900b59d14886a556a22d57872c97cc1b686d7b28 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:03:32 +0200 Subject: [PATCH 27/95] Pin pytest-rerunfailures to 15.1 (#151383) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3ab1a54eea7db..71990e7a19b691 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -223,3 +223,7 @@ pymodbus==3.11.1 # Some packages don't support gql 4.0.0 yet gql<4.0.0 + +# pytest-rerunfailures 16.0 breaks pytest, pin 15.1 until resolved +# https://github.com/pytest-dev/pytest-rerunfailures/issues/302 +pytest-rerunfailures==15.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9f65409b9be966..ba35a80da82983 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -249,6 +249,10 @@ # Some packages don't support gql 4.0.0 yet gql<4.0.0 + +# pytest-rerunfailures 16.0 breaks pytest, pin 15.1 until resolved +# https://github.com/pytest-dev/pytest-rerunfailures/issues/302 +pytest-rerunfailures==15.1 """ GENERATED_MESSAGE = ( From e21c2fa08b572e349717f663db6a2c2a9daeec97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 09:52:27 -0500 Subject: [PATCH 28/95] Bump aioesphomeapi to 39.0.1 (#151385) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c5841da4467d98..8dd198d1da1fff 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==39.0.0", + "aioesphomeapi==39.0.1", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.2.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index e1086fae83ad86..e3a584fa60fe95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==39.0.0 +aioesphomeapi==39.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55ce98e11ee8df..05a926ddd08284 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==39.0.0 +aioesphomeapi==39.0.1 # homeassistant.components.flo aioflo==2021.11.0 From bec8cf3ea87df885cbd824a102386d204571df49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Aug 2025 20:10:10 +0200 Subject: [PATCH 29/95] Fix restoring disabled_by flag of deleted devices (#151313) --- homeassistant/helpers/device_registry.py | 2 ++ tests/helpers/test_device_registry.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d07dfb2da647f5..222e13963808fc 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -944,6 +944,8 @@ def async_get_or_create( identifiers, disabled_by, ) + disabled_by = UNDEFINED + self.devices[device.id] = device # If creating a new device, default to the config entry name if device_info_type == "primary" and (not name or name is UNDEFINED): diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index dfa96fa6051847..8cfd3c66ad9be4 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -4066,6 +4066,7 @@ async def test_restore_disabled_by( config_subentry_id=None, configuration_url="http://config_url_new.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + disabled_by=None, entry_type=None, hw_version="hw_version_new", identifiers={("bridgeid", "0123")}, From 3ea0e9ee88b25e183693a60774fedd75005f9c21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Aug 2025 15:17:32 +0000 Subject: [PATCH 30/95] Bump version to 2025.9.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 492e4b9b1a36dd..5e2cceed75a800 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 66415bf6deec2b..72d73618629450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b0" +version = "2025.9.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From afdb004aa00ee66d5bc06e1f23f795ee1e877a9d Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Mon, 1 Sep 2025 10:30:41 +0200 Subject: [PATCH 31/95] Fix bug with the wrong temperature scale on new router firmware (asuswrt) (#151011) --- homeassistant/components/asuswrt/bridge.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 6e33f3a0b43ba6..3e3e372108b90e 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -12,6 +12,7 @@ from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aiohttp import ClientSession from asusrouter import AsusRouter, AsusRouterError +from asusrouter.config import ARConfigKey from asusrouter.modules.client import AsusClient from asusrouter.modules.data import AsusData from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors @@ -314,10 +315,14 @@ class AsusWrtHttpBridge(AsusWrtBridge): def __init__(self, conf: dict[str, Any], session: ClientSession) -> None: """Initialize Bridge that use HTTP library.""" super().__init__(conf[CONF_HOST]) - self._api = self._get_api(conf, session) + # Get API configuration + config = self._get_api_config() + self._api = self._get_api(conf, session, config) @staticmethod - def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusRouter: + def _get_api( + conf: dict[str, Any], session: ClientSession, config: dict[ARConfigKey, Any] + ) -> AsusRouter: """Get the AsusRouter API.""" return AsusRouter( hostname=conf[CONF_HOST], @@ -326,8 +331,19 @@ def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusRouter: use_ssl=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS, port=conf.get(CONF_PORT), session=session, + config=config, ) + def _get_api_config(self) -> dict[ARConfigKey, Any]: + """Get configuration for the API.""" + return { + # Enable automatic temperature data correction in the library + ARConfigKey.OPTIMISTIC_TEMPERATURE: True, + # Disable `warning`-level log message when temperature + # is corrected by setting it to already notified. + ARConfigKey.NOTIFIED_OPTIMISTIC_TEMPERATURE: True, + } + @property def is_connected(self) -> bool: """Get connected status.""" From 12c9f6bea99e91d1320df8f91a0d01451c18b70f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 1 Sep 2025 11:48:19 +0200 Subject: [PATCH 32/95] modbus: Do not modify registers (return wrong data). (#151131) --- homeassistant/components/modbus/entity.py | 5 ++- tests/components/modbus/test_sensor.py | 40 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 180495bd226ae2..d6101681d3f17a 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -4,6 +4,7 @@ from abc import abstractmethod from collections.abc import Callable +import copy from datetime import datetime, timedelta import struct from typing import Any, cast @@ -280,7 +281,9 @@ def unpack_structure_result(self, registers: list[int]) -> str | None: """Convert registers to proper result.""" if self._swap: - registers = self._swap_registers(registers, self._slave_count) + registers = self._swap_registers( + copy.deepcopy(registers), self._slave_count + ) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DataType.STRING: return byte_string.decode() diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4910b4df065402..868e8a8baada7e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1357,6 +1357,46 @@ async def test_wrap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 201, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + ("config_addon", "register_words", "expected"), + [ + ( + { + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + "50594050", + ), + ], +) +async def test_wrap_regs_ok_sensor( + hass: HomeAssistant, mock_modbus_ha, mock_do_cycle, expected +) -> None: + """Run test for sensor struct.""" + assert hass.states.get(ENTITY_ID).state == expected + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert hass.states.get(ENTITY_ID).state == expected + + @pytest.fixture(name="mock_restore") async def mock_restore(hass: HomeAssistant) -> None: """Mock restore cache.""" From aea39133d0e13d1d9040e8d5d76497c469c4ec3a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 1 Sep 2025 09:50:57 +0200 Subject: [PATCH 33/95] Change sounds list source for Alexa Devices (#151317) --- .../components/alexa_devices/manifest.json | 2 +- .../components/alexa_devices/services.py | 11 +- .../components/alexa_devices/services.yaml | 517 ++---------------- .../components/alexa_devices/strings.json | 511 ++--------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_services.ambr | 2 +- .../components/alexa_devices/test_services.py | 11 +- 8 files changed, 93 insertions(+), 965 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 231bbb711129af..824f735b184aa8 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==5.0.1"] + "requirements": ["aioamazondevices==6.0.0"] } diff --git a/homeassistant/components/alexa_devices/services.py b/homeassistant/components/alexa_devices/services.py index 5463c7a4319219..9d225a7beac9b5 100644 --- a/homeassistant/components/alexa_devices/services.py +++ b/homeassistant/components/alexa_devices/services.py @@ -14,14 +14,12 @@ ATTR_TEXT_COMMAND = "text_command" ATTR_SOUND = "sound" -ATTR_SOUND_VARIANT = "sound_variant" SERVICE_TEXT_COMMAND = "send_text_command" SERVICE_SOUND_NOTIFICATION = "send_sound" SCHEMA_SOUND_SERVICE = vol.Schema( { vol.Required(ATTR_SOUND): cv.string, - vol.Required(ATTR_SOUND_VARIANT): cv.positive_int, vol.Required(ATTR_DEVICE_ID): cv.string, }, ) @@ -75,17 +73,14 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None: coordinator = config_entry.runtime_data if attribute == ATTR_SOUND: - variant: int = call.data[ATTR_SOUND_VARIANT] - pad = "_" if variant > 10 else "_0" - file = f"{value}{pad}{variant!s}" - if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]: + if value not in SOUNDS_LIST: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="invalid_sound_value", - translation_placeholders={"sound": value, "variant": str(variant)}, + translation_placeholders={"sound": value}, ) await coordinator.api.call_alexa_sound( - coordinator.data[device.serial_number], file + coordinator.data[device.serial_number], value ) elif attribute == ATTR_TEXT_COMMAND: await coordinator.api.call_alexa_text_command( diff --git a/homeassistant/components/alexa_devices/services.yaml b/homeassistant/components/alexa_devices/services.yaml index d9eef28aea216b..8194e75a8d675d 100644 --- a/homeassistant/components/alexa_devices/services.yaml +++ b/homeassistant/components/alexa_devices/services.yaml @@ -18,14 +18,6 @@ send_sound: selector: device: integration: alexa_devices - sound_variant: - required: true - example: 1 - default: 1 - selector: - number: - min: 1 - max: 50 sound: required: true example: amzn_sfx_doorbell_chime @@ -33,472 +25,45 @@ send_sound: selector: select: options: - - air_horn - - air_horns - - airboat - - airport - - aliens - - amzn_sfx_airplane_takeoff_whoosh - - amzn_sfx_army_march_clank_7x - - amzn_sfx_army_march_large_8x - - amzn_sfx_army_march_small_8x - - amzn_sfx_baby_big_cry - - amzn_sfx_baby_cry - - amzn_sfx_baby_fuss - - amzn_sfx_battle_group_clanks - - amzn_sfx_battle_man_grunts - - amzn_sfx_battle_men_grunts - - amzn_sfx_battle_men_horses - - amzn_sfx_battle_noisy_clanks - - amzn_sfx_battle_yells_men - - amzn_sfx_battle_yells_men_run - - amzn_sfx_bear_groan_roar - - amzn_sfx_bear_roar_grumble - - amzn_sfx_bear_roar_small - - amzn_sfx_beep_1x - - amzn_sfx_bell_med_chime - - amzn_sfx_bell_short_chime - - amzn_sfx_bell_timer - - amzn_sfx_bicycle_bell_ring - - amzn_sfx_bird_chickadee_chirp_1x - - amzn_sfx_bird_chickadee_chirps - - amzn_sfx_bird_forest - - amzn_sfx_bird_forest_short - - amzn_sfx_bird_robin_chirp_1x - - amzn_sfx_boing_long_1x - - amzn_sfx_boing_med_1x - - amzn_sfx_boing_short_1x - - amzn_sfx_bus_drive_past - - amzn_sfx_buzz_electronic - - amzn_sfx_buzzer_loud_alarm - - amzn_sfx_buzzer_small - - amzn_sfx_car_accelerate - - amzn_sfx_car_accelerate_noisy - - amzn_sfx_car_click_seatbelt - - amzn_sfx_car_close_door_1x - - amzn_sfx_car_drive_past - - amzn_sfx_car_honk_1x - - amzn_sfx_car_honk_2x - - amzn_sfx_car_honk_3x - - amzn_sfx_car_honk_long_1x - - amzn_sfx_car_into_driveway - - amzn_sfx_car_into_driveway_fast - - amzn_sfx_car_slam_door_1x - - amzn_sfx_car_undo_seatbelt - - amzn_sfx_cat_angry_meow_1x - - amzn_sfx_cat_angry_screech_1x - - amzn_sfx_cat_long_meow_1x - - amzn_sfx_cat_meow_1x - - amzn_sfx_cat_purr - - amzn_sfx_cat_purr_meow - - amzn_sfx_chicken_cluck - - amzn_sfx_church_bell_1x - - amzn_sfx_church_bells_ringing - - amzn_sfx_clear_throat_ahem - - amzn_sfx_clock_ticking - - amzn_sfx_clock_ticking_long - - amzn_sfx_copy_machine - - amzn_sfx_cough - - amzn_sfx_crow_caw_1x - - amzn_sfx_crowd_applause - - amzn_sfx_crowd_bar - - amzn_sfx_crowd_bar_rowdy - - amzn_sfx_crowd_boo - - amzn_sfx_crowd_cheer_med - - amzn_sfx_crowd_excited_cheer - - amzn_sfx_dog_med_bark_1x - - amzn_sfx_dog_med_bark_2x - - amzn_sfx_dog_med_bark_growl - - amzn_sfx_dog_med_growl_1x - - amzn_sfx_dog_med_woof_1x - - amzn_sfx_dog_small_bark_2x - - amzn_sfx_door_open - - amzn_sfx_door_shut - - amzn_sfx_doorbell - - amzn_sfx_doorbell_buzz - - amzn_sfx_doorbell_chime - - amzn_sfx_drinking_slurp - - amzn_sfx_drum_and_cymbal - - amzn_sfx_drum_comedy - - amzn_sfx_earthquake_rumble - - amzn_sfx_electric_guitar - - amzn_sfx_electronic_beep - - amzn_sfx_electronic_major_chord - - amzn_sfx_elephant - - amzn_sfx_elevator_bell_1x - - amzn_sfx_elevator_open_bell - - amzn_sfx_fairy_melodic_chimes - - amzn_sfx_fairy_sparkle_chimes - - amzn_sfx_faucet_drip - - amzn_sfx_faucet_running - - amzn_sfx_fireplace_crackle - - amzn_sfx_fireworks - - amzn_sfx_fireworks_firecrackers - - amzn_sfx_fireworks_launch - - amzn_sfx_fireworks_whistles - - amzn_sfx_food_frying - - amzn_sfx_footsteps - - amzn_sfx_footsteps_muffled - - amzn_sfx_ghost_spooky - - amzn_sfx_glass_on_table - - amzn_sfx_glasses_clink - - amzn_sfx_horse_gallop_4x - - amzn_sfx_horse_huff_whinny - - amzn_sfx_horse_neigh - - amzn_sfx_horse_neigh_low - - amzn_sfx_horse_whinny - - amzn_sfx_human_walking - - amzn_sfx_jar_on_table_1x - - amzn_sfx_kitchen_ambience - - amzn_sfx_large_crowd_cheer - - amzn_sfx_large_fire_crackling - - amzn_sfx_laughter - - amzn_sfx_laughter_giggle - - amzn_sfx_lightning_strike - - amzn_sfx_lion_roar - - amzn_sfx_magic_blast_1x - - amzn_sfx_monkey_calls_3x - - amzn_sfx_monkey_chimp - - amzn_sfx_monkeys_chatter - - amzn_sfx_motorcycle_accelerate - - amzn_sfx_motorcycle_engine_idle - - amzn_sfx_motorcycle_engine_rev - - amzn_sfx_musical_drone_intro - - amzn_sfx_oars_splashing_rowboat - - amzn_sfx_object_on_table_2x - - amzn_sfx_ocean_wave_1x - - amzn_sfx_ocean_wave_on_rocks_1x - - amzn_sfx_ocean_wave_surf - - amzn_sfx_people_walking - - amzn_sfx_person_running - - amzn_sfx_piano_note_1x - - amzn_sfx_punch - - amzn_sfx_rain - - amzn_sfx_rain_on_roof - - amzn_sfx_rain_thunder - - amzn_sfx_rat_squeak_2x - - amzn_sfx_rat_squeaks - - amzn_sfx_raven_caw_1x - - amzn_sfx_raven_caw_2x - - amzn_sfx_restaurant_ambience - - amzn_sfx_rooster_crow - - amzn_sfx_scifi_air_escaping - - amzn_sfx_scifi_alarm - - amzn_sfx_scifi_alien_voice - - amzn_sfx_scifi_boots_walking - - amzn_sfx_scifi_close_large_explosion - - amzn_sfx_scifi_door_open - - amzn_sfx_scifi_engines_on - - amzn_sfx_scifi_engines_on_large - - amzn_sfx_scifi_engines_on_short_burst - - amzn_sfx_scifi_explosion - - amzn_sfx_scifi_explosion_2x - - amzn_sfx_scifi_incoming_explosion - - amzn_sfx_scifi_laser_gun_battle - - amzn_sfx_scifi_laser_gun_fires - - amzn_sfx_scifi_laser_gun_fires_large - - amzn_sfx_scifi_long_explosion_1x - - amzn_sfx_scifi_missile - - amzn_sfx_scifi_motor_short_1x - - amzn_sfx_scifi_open_airlock - - amzn_sfx_scifi_radar_high_ping - - amzn_sfx_scifi_radar_low - - amzn_sfx_scifi_radar_medium - - amzn_sfx_scifi_run_away - - amzn_sfx_scifi_sheilds_up - - amzn_sfx_scifi_short_low_explosion - - amzn_sfx_scifi_small_whoosh_flyby - - amzn_sfx_scifi_small_zoom_flyby - - amzn_sfx_scifi_sonar_ping_3x - - amzn_sfx_scifi_sonar_ping_4x - - amzn_sfx_scifi_spaceship_flyby - - amzn_sfx_scifi_timer_beep - - amzn_sfx_scifi_zap_backwards - - amzn_sfx_scifi_zap_electric - - amzn_sfx_sheep_baa - - amzn_sfx_sheep_bleat - - amzn_sfx_silverware_clank - - amzn_sfx_sirens - - amzn_sfx_sleigh_bells - - amzn_sfx_small_stream - - amzn_sfx_sneeze - - amzn_sfx_stream - - amzn_sfx_strong_wind_desert - - amzn_sfx_strong_wind_whistling - - amzn_sfx_subway_leaving - - amzn_sfx_subway_passing - - amzn_sfx_subway_stopping - - amzn_sfx_swoosh_cartoon_fast - - amzn_sfx_swoosh_fast_1x - - amzn_sfx_swoosh_fast_6x - - amzn_sfx_test_tone - - amzn_sfx_thunder_rumble - - amzn_sfx_toilet_flush - - amzn_sfx_trumpet_bugle - - amzn_sfx_turkey_gobbling - - amzn_sfx_typing_medium - - amzn_sfx_typing_short - - amzn_sfx_typing_typewriter - - amzn_sfx_vacuum_off - - amzn_sfx_vacuum_on - - amzn_sfx_walking_in_mud - - amzn_sfx_walking_in_snow - - amzn_sfx_walking_on_grass - - amzn_sfx_water_dripping - - amzn_sfx_water_droplets - - amzn_sfx_wind_strong_gusting - - amzn_sfx_wind_whistling_desert - - amzn_sfx_wings_flap_4x - - amzn_sfx_wings_flap_fast - - amzn_sfx_wolf_howl - - amzn_sfx_wolf_young_howl - - amzn_sfx_wooden_door - - amzn_sfx_wooden_door_creaks_long - - amzn_sfx_wooden_door_creaks_multiple - - amzn_sfx_wooden_door_creaks_open - - amzn_ui_sfx_gameshow_bridge - - amzn_ui_sfx_gameshow_countdown_loop_32s_full - - amzn_ui_sfx_gameshow_countdown_loop_64s_full - - amzn_ui_sfx_gameshow_countdown_loop_64s_minimal - - amzn_ui_sfx_gameshow_intro - - amzn_ui_sfx_gameshow_negative_response - - amzn_ui_sfx_gameshow_neutral_response - - amzn_ui_sfx_gameshow_outro - - amzn_ui_sfx_gameshow_player1 - - amzn_ui_sfx_gameshow_player2 - - amzn_ui_sfx_gameshow_player3 - - amzn_ui_sfx_gameshow_player4 - - amzn_ui_sfx_gameshow_positive_response - - amzn_ui_sfx_gameshow_tally_negative - - amzn_ui_sfx_gameshow_tally_positive - - amzn_ui_sfx_gameshow_waiting_loop_30s - - anchor - - answering_machines - - arcs_sparks - - arrows_bows - - baby - - back_up_beeps - - bars_restaurants - - baseball - - basketball - - battles - - beeps_tones - - bell - - bikes - - billiards - - board_games - - body - - boing - - books - - bow_wash - - box - - break_shatter_smash - - breaks - - brooms_mops - - bullets - - buses - - buzz - - buzz_hums - - buzzers - - buzzers_pistols - - cables_metal - - camera - - cannons - - car_alarm - - car_alarms - - car_cell_phones - - carnivals_fairs - - cars - - casino - - casinos - - cellar - - chimes - - chimes_bells - - chorus - - christmas - - church_bells - - clock - - cloth - - concrete - - construction - - construction_factory - - crashes - - crowds - - debris - - dining_kitchens - - dinosaurs - - dripping - - drops - - electric - - electrical - - elevator - - evolution_monsters - - explosions - - factory - - falls - - fax_scanner_copier - - feedback_mics - - fight - - fire - - fire_extinguisher - - fireballs - - fireworks - - fishing_pole - - flags - - football - - footsteps - - futuristic - - futuristic_ship - - gameshow - - gear - - ghosts_demons - - giant_monster - - glass - - glasses_clink - - golf - - gorilla - - grenade_lanucher - - griffen - - gyms_locker_rooms - - handgun_loading - - handgun_shot - - handle - - hands - - heartbeats_ekg - - helicopter - - high_tech - - hit_punch_slap - - hits - - horns - - horror - - hot_tub_filling_up - - human - - human_vocals - - hygene # codespell:ignore - - ice_skating - - ignitions - - infantry - - intro - - jet - - juggling - - key_lock - - kids - - knocks - - lab_equip - - lacrosse - - lamps_lanterns - - leather - - liquid_suction - - locker_doors - - machine_gun - - magic_spells - - medium_large_explosions - - metal - - modern_rings - - money_coins - - motorcycles - - movement - - moves - - nature - - oar_boat - - pagers - - paintball - - paper - - parachute - - pay_phones - - phone_beeps - - pigmy_bats - - pills - - pour_water - - power_up_down - - printers - - prison - - public_space - - racquetball - - radios_static - - rain - - rc_airplane - - rc_car - - refrigerators_freezers - - regular - - respirator - - rifle - - roller_coaster - - rollerskates_rollerblades - - room_tones - - ropes_climbing - - rotary_rings - - rowboat_canoe - - rubber - - running - - sails - - sand_gravel - - screen_doors - - screens - - seats_stools - - servos - - shoes_boots - - shotgun - - shower - - sink_faucet - - sink_filling_water - - sink_run_and_off - - sink_water_splatter - - sirens - - skateboards - - ski - - skids_tires - - sled - - slides - - small_explosions - - snow - - snowmobile - - soldiers - - splash_water - - splashes_sprays - - sports_whistles - - squeaks - - squeaky - - stairs - - steam - - submarine_diesel - - swing_doors - - switches_levers - - swords - - tape - - tape_machine - - televisions_shows - - tennis_pingpong - - textile - - throw - - thunder - - ticks - - timer - - toilet_flush - - tone - - tones_noises - - toys - - tractors - - traffic - - train - - trucks_vans - - turnstiles - - typing - - umbrella - - underwater - - vampires - - various - - video_tunes - - volcano_earthquake - - watches - - water - - water_running - - werewolves - - winches_gears - - wind - - wood - - wood_boat - - woosh - - zap - - zippers + - air_horn_03 + - amzn_sfx_cat_meow_1x_01 + - amzn_sfx_church_bell_1x_02 + - amzn_sfx_crowd_applause_01 + - amzn_sfx_dog_med_bark_1x_02 + - amzn_sfx_doorbell_01 + - amzn_sfx_doorbell_chime_01 + - amzn_sfx_doorbell_chime_02 + - amzn_sfx_large_crowd_cheer_01 + - amzn_sfx_lion_roar_02 + - amzn_sfx_rooster_crow_01 + - amzn_sfx_scifi_alarm_01 + - amzn_sfx_scifi_alarm_04 + - amzn_sfx_scifi_engines_on_02 + - amzn_sfx_scifi_sheilds_up_01 + - amzn_sfx_trumpet_bugle_04 + - amzn_sfx_wolf_howl_02 + - bell_02 + - boing_01 + - boing_03 + - buzzers_pistols_01 + - camera_01 + - christmas_05 + - clock_01 + - futuristic_10 + - halloween_bats + - halloween_crows + - halloween_footsteps + - halloween_wind + - halloween_wolf + - holiday_halloween_ghost + - horror_10 + - med_system_alerts_minimal_dragon_short + - med_system_alerts_minimal_owl_short + - med_system_alerts_minimals_blue_wave_small + - med_system_alerts_minimals_galaxy_short + - med_system_alerts_minimals_panda_short + - med_system_alerts_minimals_tiger_short + - med_ui_success_generic_1-1 + - squeaky_12 + - zap_01 translation_key: sound diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index b1e9027ca536ba..79774aa3b3b9f1 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -129,474 +129,47 @@ "selector": { "sound": { "options": { - "air_horn": "Air Horn", - "air_horns": "Air Horns", - "airboat": "Airboat", - "airport": "Airport", - "aliens": "Aliens", - "amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh", - "amzn_sfx_army_march_clank_7x": "Army March Clank 7x", - "amzn_sfx_army_march_large_8x": "Army March Large 8x", - "amzn_sfx_army_march_small_8x": "Army March Small 8x", - "amzn_sfx_baby_big_cry": "Baby Big Cry", - "amzn_sfx_baby_cry": "Baby Cry", - "amzn_sfx_baby_fuss": "Baby Fuss", - "amzn_sfx_battle_group_clanks": "Battle Group Clanks", - "amzn_sfx_battle_man_grunts": "Battle Man Grunts", - "amzn_sfx_battle_men_grunts": "Battle Men Grunts", - "amzn_sfx_battle_men_horses": "Battle Men Horses", - "amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks", - "amzn_sfx_battle_yells_men": "Battle Yells Men", - "amzn_sfx_battle_yells_men_run": "Battle Yells Men Run", - "amzn_sfx_bear_groan_roar": "Bear Groan Roar", - "amzn_sfx_bear_roar_grumble": "Bear Roar Grumble", - "amzn_sfx_bear_roar_small": "Bear Roar Small", - "amzn_sfx_beep_1x": "Beep 1x", - "amzn_sfx_bell_med_chime": "Bell Med Chime", - "amzn_sfx_bell_short_chime": "Bell Short Chime", - "amzn_sfx_bell_timer": "Bell Timer", - "amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring", - "amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x", - "amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps", - "amzn_sfx_bird_forest": "Bird Forest", - "amzn_sfx_bird_forest_short": "Bird Forest Short", - "amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x", - "amzn_sfx_boing_long_1x": "Boing Long 1x", - "amzn_sfx_boing_med_1x": "Boing Med 1x", - "amzn_sfx_boing_short_1x": "Boing Short 1x", - "amzn_sfx_bus_drive_past": "Bus Drive Past", - "amzn_sfx_buzz_electronic": "Buzz Electronic", - "amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm", - "amzn_sfx_buzzer_small": "Buzzer Small", - "amzn_sfx_car_accelerate": "Car Accelerate", - "amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy", - "amzn_sfx_car_click_seatbelt": "Car Click Seatbelt", - "amzn_sfx_car_close_door_1x": "Car Close Door 1x", - "amzn_sfx_car_drive_past": "Car Drive Past", - "amzn_sfx_car_honk_1x": "Car Honk 1x", - "amzn_sfx_car_honk_2x": "Car Honk 2x", - "amzn_sfx_car_honk_3x": "Car Honk 3x", - "amzn_sfx_car_honk_long_1x": "Car Honk Long 1x", - "amzn_sfx_car_into_driveway": "Car Into Driveway", - "amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast", - "amzn_sfx_car_slam_door_1x": "Car Slam Door 1x", - "amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt", - "amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x", - "amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x", - "amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x", - "amzn_sfx_cat_meow_1x": "Cat Meow 1x", - "amzn_sfx_cat_purr": "Cat Purr", - "amzn_sfx_cat_purr_meow": "Cat Purr Meow", - "amzn_sfx_chicken_cluck": "Chicken Cluck", - "amzn_sfx_church_bell_1x": "Church Bell 1x", - "amzn_sfx_church_bells_ringing": "Church Bells Ringing", - "amzn_sfx_clear_throat_ahem": "Clear Throat Ahem", - "amzn_sfx_clock_ticking": "Clock Ticking", - "amzn_sfx_clock_ticking_long": "Clock Ticking Long", - "amzn_sfx_copy_machine": "Copy Machine", - "amzn_sfx_cough": "Cough", - "amzn_sfx_crow_caw_1x": "Crow Caw 1x", - "amzn_sfx_crowd_applause": "Crowd Applause", - "amzn_sfx_crowd_bar": "Crowd Bar", - "amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy", - "amzn_sfx_crowd_boo": "Crowd Boo", - "amzn_sfx_crowd_cheer_med": "Crowd Cheer Med", - "amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer", - "amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x", - "amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x", - "amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl", - "amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x", - "amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x", - "amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x", - "amzn_sfx_door_open": "Door Open", - "amzn_sfx_door_shut": "Door Shut", - "amzn_sfx_doorbell": "Doorbell", - "amzn_sfx_doorbell_buzz": "Doorbell Buzz", - "amzn_sfx_doorbell_chime": "Doorbell Chime", - "amzn_sfx_drinking_slurp": "Drinking Slurp", - "amzn_sfx_drum_and_cymbal": "Drum And Cymbal", - "amzn_sfx_drum_comedy": "Drum Comedy", - "amzn_sfx_earthquake_rumble": "Earthquake Rumble", - "amzn_sfx_electric_guitar": "Electric Guitar", - "amzn_sfx_electronic_beep": "Electronic Beep", - "amzn_sfx_electronic_major_chord": "Electronic Major Chord", - "amzn_sfx_elephant": "Elephant", - "amzn_sfx_elevator_bell_1x": "Elevator Bell 1x", - "amzn_sfx_elevator_open_bell": "Elevator Open Bell", - "amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes", - "amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes", - "amzn_sfx_faucet_drip": "Faucet Drip", - "amzn_sfx_faucet_running": "Faucet Running", - "amzn_sfx_fireplace_crackle": "Fireplace Crackle", - "amzn_sfx_fireworks": "Fireworks", - "amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers", - "amzn_sfx_fireworks_launch": "Fireworks Launch", - "amzn_sfx_fireworks_whistles": "Fireworks Whistles", - "amzn_sfx_food_frying": "Food Frying", - "amzn_sfx_footsteps": "Footsteps", - "amzn_sfx_footsteps_muffled": "Footsteps Muffled", - "amzn_sfx_ghost_spooky": "Ghost Spooky", - "amzn_sfx_glass_on_table": "Glass On Table", - "amzn_sfx_glasses_clink": "Glasses Clink", - "amzn_sfx_horse_gallop_4x": "Horse Gallop 4x", - "amzn_sfx_horse_huff_whinny": "Horse Huff Whinny", - "amzn_sfx_horse_neigh": "Horse Neigh", - "amzn_sfx_horse_neigh_low": "Horse Neigh Low", - "amzn_sfx_horse_whinny": "Horse Whinny", - "amzn_sfx_human_walking": "Human Walking", - "amzn_sfx_jar_on_table_1x": "Jar On Table 1x", - "amzn_sfx_kitchen_ambience": "Kitchen Ambience", - "amzn_sfx_large_crowd_cheer": "Large Crowd Cheer", - "amzn_sfx_large_fire_crackling": "Large Fire Crackling", - "amzn_sfx_laughter": "Laughter", - "amzn_sfx_laughter_giggle": "Laughter Giggle", - "amzn_sfx_lightning_strike": "Lightning Strike", - "amzn_sfx_lion_roar": "Lion Roar", - "amzn_sfx_magic_blast_1x": "Magic Blast 1x", - "amzn_sfx_monkey_calls_3x": "Monkey Calls 3x", - "amzn_sfx_monkey_chimp": "Monkey Chimp", - "amzn_sfx_monkeys_chatter": "Monkeys Chatter", - "amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate", - "amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle", - "amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev", - "amzn_sfx_musical_drone_intro": "Musical Drone Intro", - "amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat", - "amzn_sfx_object_on_table_2x": "Object On Table 2x", - "amzn_sfx_ocean_wave_1x": "Ocean Wave 1x", - "amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x", - "amzn_sfx_ocean_wave_surf": "Ocean Wave Surf", - "amzn_sfx_people_walking": "People Walking", - "amzn_sfx_person_running": "Person Running", - "amzn_sfx_piano_note_1x": "Piano Note 1x", - "amzn_sfx_punch": "Punch", - "amzn_sfx_rain": "Rain", - "amzn_sfx_rain_on_roof": "Rain On Roof", - "amzn_sfx_rain_thunder": "Rain Thunder", - "amzn_sfx_rat_squeak_2x": "Rat Squeak 2x", - "amzn_sfx_rat_squeaks": "Rat Squeaks", - "amzn_sfx_raven_caw_1x": "Raven Caw 1x", - "amzn_sfx_raven_caw_2x": "Raven Caw 2x", - "amzn_sfx_restaurant_ambience": "Restaurant Ambience", - "amzn_sfx_rooster_crow": "Rooster Crow", - "amzn_sfx_scifi_air_escaping": "Scifi Air Escaping", - "amzn_sfx_scifi_alarm": "Scifi Alarm", - "amzn_sfx_scifi_alien_voice": "Scifi Alien Voice", - "amzn_sfx_scifi_boots_walking": "Scifi Boots Walking", - "amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion", - "amzn_sfx_scifi_door_open": "Scifi Door Open", - "amzn_sfx_scifi_engines_on": "Scifi Engines On", - "amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large", - "amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst", - "amzn_sfx_scifi_explosion": "Scifi Explosion", - "amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x", - "amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion", - "amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle", - "amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires", - "amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large", - "amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x", - "amzn_sfx_scifi_missile": "Scifi Missile", - "amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x", - "amzn_sfx_scifi_open_airlock": "Scifi Open Airlock", - "amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping", - "amzn_sfx_scifi_radar_low": "Scifi Radar Low", - "amzn_sfx_scifi_radar_medium": "Scifi Radar Medium", - "amzn_sfx_scifi_run_away": "Scifi Run Away", - "amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up", - "amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion", - "amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby", - "amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby", - "amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x", - "amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x", - "amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby", - "amzn_sfx_scifi_timer_beep": "Scifi Timer Beep", - "amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards", - "amzn_sfx_scifi_zap_electric": "Scifi Zap Electric", - "amzn_sfx_sheep_baa": "Sheep Baa", - "amzn_sfx_sheep_bleat": "Sheep Bleat", - "amzn_sfx_silverware_clank": "Silverware Clank", - "amzn_sfx_sirens": "Sirens", - "amzn_sfx_sleigh_bells": "Sleigh Bells", - "amzn_sfx_small_stream": "Small Stream", - "amzn_sfx_sneeze": "Sneeze", - "amzn_sfx_stream": "Stream", - "amzn_sfx_strong_wind_desert": "Strong Wind Desert", - "amzn_sfx_strong_wind_whistling": "Strong Wind Whistling", - "amzn_sfx_subway_leaving": "Subway Leaving", - "amzn_sfx_subway_passing": "Subway Passing", - "amzn_sfx_subway_stopping": "Subway Stopping", - "amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast", - "amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x", - "amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x", - "amzn_sfx_test_tone": "Test Tone", - "amzn_sfx_thunder_rumble": "Thunder Rumble", - "amzn_sfx_toilet_flush": "Toilet Flush", - "amzn_sfx_trumpet_bugle": "Trumpet Bugle", - "amzn_sfx_turkey_gobbling": "Turkey Gobbling", - "amzn_sfx_typing_medium": "Typing Medium", - "amzn_sfx_typing_short": "Typing Short", - "amzn_sfx_typing_typewriter": "Typing Typewriter", - "amzn_sfx_vacuum_off": "Vacuum Off", - "amzn_sfx_vacuum_on": "Vacuum On", - "amzn_sfx_walking_in_mud": "Walking In Mud", - "amzn_sfx_walking_in_snow": "Walking In Snow", - "amzn_sfx_walking_on_grass": "Walking On Grass", - "amzn_sfx_water_dripping": "Water Dripping", - "amzn_sfx_water_droplets": "Water Droplets", - "amzn_sfx_wind_strong_gusting": "Wind Strong Gusting", - "amzn_sfx_wind_whistling_desert": "Wind Whistling Desert", - "amzn_sfx_wings_flap_4x": "Wings Flap 4x", - "amzn_sfx_wings_flap_fast": "Wings Flap Fast", - "amzn_sfx_wolf_howl": "Wolf Howl", - "amzn_sfx_wolf_young_howl": "Wolf Young Howl", - "amzn_sfx_wooden_door": "Wooden Door", - "amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long", - "amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple", - "amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open", - "amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge", - "amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full", - "amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full", - "amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal", - "amzn_ui_sfx_gameshow_intro": "Gameshow Intro", - "amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response", - "amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response", - "amzn_ui_sfx_gameshow_outro": "Gameshow Outro", - "amzn_ui_sfx_gameshow_player1": "Gameshow Player1", - "amzn_ui_sfx_gameshow_player2": "Gameshow Player2", - "amzn_ui_sfx_gameshow_player3": "Gameshow Player3", - "amzn_ui_sfx_gameshow_player4": "Gameshow Player4", - "amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response", - "amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative", - "amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive", - "amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s", - "anchor": "Anchor", - "answering_machines": "Answering Machines", - "arcs_sparks": "Arcs Sparks", - "arrows_bows": "Arrows Bows", - "baby": "Baby", - "back_up_beeps": "Back Up Beeps", - "bars_restaurants": "Bars Restaurants", - "baseball": "Baseball", - "basketball": "Basketball", - "battles": "Battles", - "beeps_tones": "Beeps Tones", - "bell": "Bell", - "bikes": "Bikes", - "billiards": "Billiards", - "board_games": "Board Games", - "body": "Body", - "boing": "Boing", - "books": "Books", - "bow_wash": "Bow Wash", - "box": "Box", - "break_shatter_smash": "Break Shatter Smash", - "breaks": "Breaks", - "brooms_mops": "Brooms Mops", - "bullets": "Bullets", - "buses": "Buses", - "buzz": "Buzz", - "buzz_hums": "Buzz Hums", - "buzzers": "Buzzers", - "buzzers_pistols": "Buzzers Pistols", - "cables_metal": "Cables Metal", - "camera": "Camera", - "cannons": "Cannons", - "car_alarm": "Car Alarm", - "car_alarms": "Car Alarms", - "car_cell_phones": "Car Cell Phones", - "carnivals_fairs": "Carnivals Fairs", - "cars": "Cars", - "casino": "Casino", - "casinos": "Casinos", - "cellar": "Cellar", - "chimes": "Chimes", - "chimes_bells": "Chimes Bells", - "chorus": "Chorus", - "christmas": "Christmas", - "church_bells": "Church Bells", - "clock": "Clock", - "cloth": "Cloth", - "concrete": "Concrete", - "construction": "Construction", - "construction_factory": "Construction Factory", - "crashes": "Crashes", - "crowds": "Crowds", - "debris": "Debris", - "dining_kitchens": "Dining Kitchens", - "dinosaurs": "Dinosaurs", - "dripping": "Dripping", - "drops": "Drops", - "electric": "Electric", - "electrical": "Electrical", - "elevator": "Elevator", - "evolution_monsters": "Evolution Monsters", - "explosions": "Explosions", - "factory": "Factory", - "falls": "Falls", - "fax_scanner_copier": "Fax Scanner Copier", - "feedback_mics": "Feedback Mics", - "fight": "Fight", - "fire": "Fire", - "fire_extinguisher": "Fire Extinguisher", - "fireballs": "Fireballs", - "fireworks": "Fireworks", - "fishing_pole": "Fishing Pole", - "flags": "Flags", - "football": "Football", - "footsteps": "Footsteps", - "futuristic": "Futuristic", - "futuristic_ship": "Futuristic Ship", - "gameshow": "Gameshow", - "gear": "Gear", - "ghosts_demons": "Ghosts Demons", - "giant_monster": "Giant Monster", - "glass": "Glass", - "glasses_clink": "Glasses Clink", - "golf": "Golf", - "gorilla": "Gorilla", - "grenade_lanucher": "Grenade Lanucher", - "griffen": "Griffen", - "gyms_locker_rooms": "Gyms Locker Rooms", - "handgun_loading": "Handgun Loading", - "handgun_shot": "Handgun Shot", - "handle": "Handle", - "hands": "Hands", - "heartbeats_ekg": "Heartbeats EKG", - "helicopter": "Helicopter", - "high_tech": "High Tech", - "hit_punch_slap": "Hit Punch Slap", - "hits": "Hits", - "horns": "Horns", - "horror": "Horror", - "hot_tub_filling_up": "Hot Tub Filling Up", - "human": "Human", - "human_vocals": "Human Vocals", - "hygene": "Hygene", - "ice_skating": "Ice Skating", - "ignitions": "Ignitions", - "infantry": "Infantry", - "intro": "Intro", - "jet": "Jet", - "juggling": "Juggling", - "key_lock": "Key Lock", - "kids": "Kids", - "knocks": "Knocks", - "lab_equip": "Lab Equip", - "lacrosse": "Lacrosse", - "lamps_lanterns": "Lamps Lanterns", - "leather": "Leather", - "liquid_suction": "Liquid Suction", - "locker_doors": "Locker Doors", - "machine_gun": "Machine Gun", - "magic_spells": "Magic Spells", - "medium_large_explosions": "Medium Large Explosions", - "metal": "Metal", - "modern_rings": "Modern Rings", - "money_coins": "Money Coins", - "motorcycles": "Motorcycles", - "movement": "Movement", - "moves": "Moves", - "nature": "Nature", - "oar_boat": "Oar Boat", - "pagers": "Pagers", - "paintball": "Paintball", - "paper": "Paper", - "parachute": "Parachute", - "pay_phones": "Pay Phones", - "phone_beeps": "Phone Beeps", - "pigmy_bats": "Pigmy Bats", - "pills": "Pills", - "pour_water": "Pour Water", - "power_up_down": "Power Up Down", - "printers": "Printers", - "prison": "Prison", - "public_space": "Public Space", - "racquetball": "Racquetball", - "radios_static": "Radios Static", - "rain": "Rain", - "rc_airplane": "RC Airplane", - "rc_car": "RC Car", - "refrigerators_freezers": "Refrigerators Freezers", - "regular": "Regular", - "respirator": "Respirator", - "rifle": "Rifle", - "roller_coaster": "Roller Coaster", - "rollerskates_rollerblades": "RollerSkates RollerBlades", - "room_tones": "Room Tones", - "ropes_climbing": "Ropes Climbing", - "rotary_rings": "Rotary Rings", - "rowboat_canoe": "Rowboat Canoe", - "rubber": "Rubber", - "running": "Running", - "sails": "Sails", - "sand_gravel": "Sand Gravel", - "screen_doors": "Screen Doors", - "screens": "Screens", - "seats_stools": "Seats Stools", - "servos": "Servos", - "shoes_boots": "Shoes Boots", - "shotgun": "Shotgun", - "shower": "Shower", - "sink_faucet": "Sink Faucet", - "sink_filling_water": "Sink Filling Water", - "sink_run_and_off": "Sink Run And Off", - "sink_water_splatter": "Sink Water Splatter", - "sirens": "Sirens", - "skateboards": "Skateboards", - "ski": "Ski", - "skids_tires": "Skids Tires", - "sled": "Sled", - "slides": "Slides", - "small_explosions": "Small Explosions", - "snow": "Snow", - "snowmobile": "Snowmobile", - "soldiers": "Soldiers", - "splash_water": "Splash Water", - "splashes_sprays": "Splashes Sprays", - "sports_whistles": "Sports Whistles", - "squeaks": "Squeaks", - "squeaky": "Squeaky", - "stairs": "Stairs", - "steam": "Steam", - "submarine_diesel": "Submarine Diesel", - "swing_doors": "Swing Doors", - "switches_levers": "Switches Levers", - "swords": "Swords", - "tape": "Tape", - "tape_machine": "Tape Machine", - "televisions_shows": "Televisions Shows", - "tennis_pingpong": "Tennis PingPong", - "textile": "Textile", - "throw": "Throw", - "thunder": "Thunder", - "ticks": "Ticks", - "timer": "Timer", - "toilet_flush": "Toilet Flush", - "tone": "Tone", - "tones_noises": "Tones Noises", - "toys": "Toys", - "tractors": "Tractors", - "traffic": "Traffic", - "train": "Train", - "trucks_vans": "Trucks Vans", - "turnstiles": "Turnstiles", - "typing": "Typing", - "umbrella": "Umbrella", - "underwater": "Underwater", - "vampires": "Vampires", - "various": "Various", - "video_tunes": "Video Tunes", - "volcano_earthquake": "Volcano Earthquake", - "watches": "Watches", - "water": "Water", - "water_running": "Water Running", - "werewolves": "Werewolves", - "winches_gears": "Winches Gears", - "wind": "Wind", - "wood": "Wood", - "wood_boat": "Wood Boat", - "woosh": "Woosh", - "zap": "Zap", - "zippers": "Zippers" + "air_horn_03": "Air horn", + "amzn_sfx_cat_meow_1x_01": "Cat meow", + "amzn_sfx_church_bell_1x_02": "Church bell", + "amzn_sfx_crowd_applause_01": "Crowd applause", + "amzn_sfx_dog_med_bark_1x_02": "Dog bark", + "amzn_sfx_doorbell_01": "Doorbell 1", + "amzn_sfx_doorbell_chime_01": "Doorbell 2", + "amzn_sfx_doorbell_chime_02": "Doorbell 3", + "amzn_sfx_large_crowd_cheer_01": "Crowd cheers", + "amzn_sfx_lion_roar_02": "Lion roar", + "amzn_sfx_rooster_crow_01": "Rooster", + "amzn_sfx_scifi_alarm_01": "Sirens", + "amzn_sfx_scifi_alarm_04": "Red alert", + "amzn_sfx_scifi_engines_on_02": "Engines on", + "amzn_sfx_scifi_sheilds_up_01": "Shields up", + "amzn_sfx_trumpet_bugle_04": "Trumpet", + "amzn_sfx_wolf_howl_02": "Wolf howl", + "bell_02": "Bells", + "boing_01": "Boing 1", + "boing_03": "Boing 2", + "buzzers_pistols_01": "Buzzer", + "camera_01": "Camera", + "christmas_05": "Christmas bells", + "clock_01": "Ticking clock", + "futuristic_10": "Aircraft", + "halloween_bats": "Halloween bats", + "halloween_crows": "Halloween crows", + "halloween_footsteps": "Halloween spooky footsteps", + "halloween_wind": "Halloween wind", + "halloween_wolf": "Halloween wolf", + "holiday_halloween_ghost": "Halloween ghost", + "horror_10": "Halloween creepy door", + "med_system_alerts_minimal_dragon_short": "Friendly dragon", + "med_system_alerts_minimal_owl_short": "Happy owl", + "med_system_alerts_minimals_blue_wave_small": "Underwater World Sonata", + "med_system_alerts_minimals_galaxy_short": "Infinite Galaxy", + "med_system_alerts_minimals_panda_short": "Baby panda", + "med_system_alerts_minimals_tiger_short": "Playful tiger", + "med_ui_success_generic_1-1": "Success 1", + "squeaky_12": "Squeaky door", + "zap_01": "Zap" } } }, @@ -614,7 +187,7 @@ "message": "Invalid device ID specified: {device_id}" }, "invalid_sound_value": { - "message": "Invalid sound {sound} with variant {variant} specified" + "message": "Invalid sound {sound} specified" }, "entry_not_loaded": { "message": "Entry not loaded: {entry}" diff --git a/requirements_all.txt b/requirements_all.txt index e3a584fa60fe95..ab4186d3cf871d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==5.0.1 +aioamazondevices==6.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05a926ddd08284..4e76a87fe1dfdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==5.0.1 +aioamazondevices==6.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/alexa_devices/snapshots/test_services.ambr b/tests/components/alexa_devices/snapshots/test_services.ambr index 885c4456a1a1f1..12eab4a683bf90 100644 --- a/tests/components/alexa_devices/snapshots/test_services.ambr +++ b/tests/components/alexa_devices/snapshots/test_services.ambr @@ -30,7 +30,7 @@ 'serial_number': 'echo_test_serial_number', 'software_version': 'echo_test_software_version', }), - 'chimes_bells_01', + 'bell_02', ), dict({ }), diff --git a/tests/components/alexa_devices/test_services.py b/tests/components/alexa_devices/test_services.py index 914664199c241e..72cef62a96623c 100644 --- a/tests/components/alexa_devices/test_services.py +++ b/tests/components/alexa_devices/test_services.py @@ -8,7 +8,6 @@ from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.components.alexa_devices.services import ( ATTR_SOUND, - ATTR_SOUND_VARIANT, ATTR_TEXT_COMMAND, SERVICE_SOUND_NOTIFICATION, SERVICE_TEXT_COMMAND, @@ -58,8 +57,7 @@ async def test_send_sound_service( DOMAIN, SERVICE_SOUND_NOTIFICATION, { - ATTR_SOUND: "chimes_bells", - ATTR_SOUND_VARIANT: 1, + ATTR_SOUND: "bell_02", ATTR_DEVICE_ID: device_entry.id, }, blocking=True, @@ -103,7 +101,7 @@ async def test_send_text_service( ("sound", "device_id", "translation_key", "translation_placeholders"), [ ( - "chimes_bells", + "bell_02", "fake_device_id", "invalid_device_id", {"device_id": "fake_device_id"}, @@ -114,7 +112,6 @@ async def test_send_text_service( "invalid_sound_value", { "sound": "wrong_sound_name", - "variant": "1", }, ), ], @@ -146,7 +143,6 @@ async def test_invalid_parameters( SERVICE_SOUND_NOTIFICATION, { ATTR_SOUND: sound, - ATTR_SOUND_VARIANT: 1, ATTR_DEVICE_ID: device_id, }, blocking=True, @@ -183,8 +179,7 @@ async def test_config_entry_not_loaded( DOMAIN, SERVICE_SOUND_NOTIFICATION, { - ATTR_SOUND: "chimes_bells", - ATTR_SOUND_VARIANT: 1, + ATTR_SOUND: "bell_02", ATTR_DEVICE_ID: device_entry.id, }, blocking=True, From a972c1e0b077995ece8fdfc10c6e2ca7d8726f54 Mon Sep 17 00:00:00 2001 From: Arjan <44190435+vingerha@users.noreply.github.com> Date: Mon, 1 Sep 2025 09:46:21 +0200 Subject: [PATCH 34/95] Fix typo in Meteo France mappings (#151344) --- homeassistant/components/meteo_france/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 13c52f04a06027..285e508a66118b 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -49,7 +49,7 @@ "Bancs de Brouillard", "Brouillard dense", ], - ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle", "Averses de grèle"], + ATTR_CONDITION_HAIL: ["Risque de grêle", "Averses de grêle"], ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages", "Orage avec grêle"], ATTR_CONDITION_LIGHTNING_RAINY: [ "Pluie orageuses", From 9d0e222671ece2d8d58ed34825792f48b623e0f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Sep 2025 03:35:22 -0500 Subject: [PATCH 35/95] Reduce log spam from unauthenticated websocket connections (#151388) --- .../components/websocket_api/http.py | 15 +++++- tests/components/websocket_api/test_http.py | 49 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 4250da149aded3..0e9e0eb69330c9 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -37,6 +37,7 @@ from .util import describe_request CLOSE_MSG_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING} +AUTH_MESSAGE_TIMEOUT = 10 # seconds if TYPE_CHECKING: from .connection import ActiveConnection @@ -389,9 +390,11 @@ async def _async_handle_auth_phase( # Auth Phase try: - msg = await self._wsock.receive(10) + msg = await self._wsock.receive(AUTH_MESSAGE_TIMEOUT) except TimeoutError as err: - raise Disconnect("Did not receive auth message within 10 seconds") from err + raise Disconnect( + f"Did not receive auth message within {AUTH_MESSAGE_TIMEOUT} seconds" + ) from err if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): raise Disconnect("Received close message during auth phase") @@ -538,6 +541,14 @@ async def _async_cleanup_writer_and_close( finally: if disconnect_warn is None: logger.debug("%s: Disconnected", self.description) + elif connection is None: + # Auth phase disconnects (connection is None) should be logged at debug level + # as they can be from random port scanners or non-legitimate connections + logger.debug( + "%s: Disconnected during auth phase: %s", + self.description, + disconnect_warn, + ) else: logger.warning( "%s: Disconnected: %s", self.description, disconnect_warn diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index b4b11d9cf02d40..2e60e837976bac 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta +import logging from typing import Any, cast from unittest.mock import patch @@ -20,7 +21,11 @@ from homeassistant.util.dt import utcnow from tests.common import async_call_logger_set_level, async_fire_time_changed -from tests.typing import MockHAClientWebSocket, WebSocketGenerator +from tests.typing import ( + ClientSessionGenerator, + MockHAClientWebSocket, + WebSocketGenerator, +) @pytest.fixture @@ -400,6 +405,48 @@ async def test_prepare_fail_connection_reset( assert "Connection reset by peer while preparing WebSocket" in caplog.text +async def test_auth_timeout_logs_at_debug( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test auth timeout is logged at debug level not warning.""" + # Setup websocket API + assert await async_setup_component(hass, "websocket_api", {}) + + client = await hass_client() + + # Patch the auth timeout to be very short (0.001 seconds) + with ( + caplog.at_level(logging.DEBUG, "homeassistant.components.websocket_api"), + patch( + "homeassistant.components.websocket_api.http.AUTH_MESSAGE_TIMEOUT", 0.001 + ), + ): + # Try to connect - will timeout quickly since we don't send auth + ws = await client.ws_connect("/api/websocket") + # Wait a bit for the timeout to trigger and cleanup to complete + await asyncio.sleep(0.1) + await ws.close() + await asyncio.sleep(0.1) + + # Check that "Did not receive auth message" is logged at debug, not warning + debug_messages = [ + r.message for r in caplog.records if r.levelno == logging.DEBUG + ] + assert any( + "Disconnected during auth phase: Did not receive auth message" in msg + for msg in debug_messages + ) + + # Check it's NOT logged at warning level + warning_messages = [ + r.message for r in caplog.records if r.levelno >= logging.WARNING + ] + for msg in warning_messages: + assert "Did not receive auth message" not in msg + + async def test_enable_coalesce( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, From 737ee51b53e2415afc6c15a8f1e91b4adca027f0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 1 Sep 2025 10:20:03 +0200 Subject: [PATCH 36/95] Update frontend to 20250829.0 (#151390) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4ffe4a41c60c0d..8de9ccd4e5b3ef 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250828.0"] + "requirements": ["home-assistant-frontend==20250829.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 71990e7a19b691..ff567a94c0fcc8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.2.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250828.0 +home-assistant-frontend==20250829.0 home-assistant-intents==2025.8.27 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ab4186d3cf871d..d68b5290e01017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250828.0 +home-assistant-frontend==20250829.0 # homeassistant.components.conversation home-assistant-intents==2025.8.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e76a87fe1dfdc..7097289b0d6b77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250828.0 +home-assistant-frontend==20250829.0 # homeassistant.components.conversation home-assistant-intents==2025.8.27 From 5428c6fc237d90439eea3a22e9860b03e86c69fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Aug 2025 12:14:22 -0500 Subject: [PATCH 37/95] Bump habluetooth to 5.2.1 (#151391) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cca12b4daf069e..95bb58204233c7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==5.2.0" + "habluetooth==5.2.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ff567a94c0fcc8..83611156cecc69 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==5.2.0 +habluetooth==5.2.1 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d68b5290e01017..45cdadc41de02f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.2.0 +habluetooth==5.2.1 # homeassistant.components.cloud hass-nabucasa==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7097289b0d6b77..84f6b02ccacf6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.2.0 +habluetooth==5.2.1 # homeassistant.components.cloud hass-nabucasa==1.0.0 From 94081e011be761761766a7ad9ef85b975df0dc1a Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:37:41 -0700 Subject: [PATCH 38/95] Fix play media example data (#151394) --- homeassistant/components/media_player/services.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 24a04393d94663..26a2624a61c412 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -135,9 +135,7 @@ play_media: required: true selector: media: - example: - media_content_id: "https://home-assistant.io/images/cast/splash.png" - media_content_type: "music" + example: '{"media_content_id": "https://home-assistant.io/images/cast/splash.png", "media_content_type": "music"}' enqueue: filter: From b86c37f5561e7d8bc000acdcfb82a57de776656b Mon Sep 17 00:00:00 2001 From: Russell VanderMey Date: Mon, 1 Sep 2025 04:19:35 -0400 Subject: [PATCH 39/95] Avoid blocking IO in TRIGGERcmd (#151396) --- homeassistant/components/triggercmd/__init__.py | 8 ++++++-- homeassistant/components/triggercmd/config_flow.py | 4 +++- homeassistant/components/triggercmd/manifest.json | 2 +- homeassistant/components/triggercmd/switch.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/triggercmd/__init__.py b/homeassistant/components/triggercmd/__init__.py index f58b2b481d46fa..3c1a2c855d04a8 100644 --- a/homeassistant/components/triggercmd/__init__.py +++ b/homeassistant/components/triggercmd/__init__.py @@ -8,6 +8,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import httpx_client from .const import CONF_TOKEN @@ -20,9 +21,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: TriggercmdConfigEntry) -> bool: """Set up TRIGGERcmd from a config entry.""" + hass_client = httpx_client.get_async_client(hass) hub = ha.Hub(entry.data[CONF_TOKEN]) - - status_code = await client.async_connection_test(entry.data[CONF_TOKEN]) + await hub.async_init(hass_client) + status_code = await client.async_connection_test( + entry.data[CONF_TOKEN], hass_client + ) if status_code != 200: raise ConfigEntryNotReady diff --git a/homeassistant/components/triggercmd/config_flow.py b/homeassistant/components/triggercmd/config_flow.py index 48c4eacfd5a790..e796e836abf925 100644 --- a/homeassistant/components/triggercmd/config_flow.py +++ b/homeassistant/components/triggercmd/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import httpx_client from .const import CONF_TOKEN, DOMAIN @@ -32,8 +33,9 @@ async def validate_input(hass: HomeAssistant, data: dict) -> str: if not token_data["id"]: raise InvalidToken + hass_client = httpx_client.get_async_client(hass) try: - await client.async_connection_test(data[CONF_TOKEN]) + await client.async_connection_test(data[CONF_TOKEN], hass_client) except Exception as e: raise TRIGGERcmdConnectionError from e else: diff --git a/homeassistant/components/triggercmd/manifest.json b/homeassistant/components/triggercmd/manifest.json index a0ee4eaf63eab4..1083c82e5beac7 100644 --- a/homeassistant/components/triggercmd/manifest.json +++ b/homeassistant/components/triggercmd/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/triggercmd", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["triggercmd==0.0.27"] + "requirements": ["triggercmd==0.0.36"] } diff --git a/homeassistant/components/triggercmd/switch.py b/homeassistant/components/triggercmd/switch.py index e03ff333751a50..ae7b0d4beeccdf 100644 --- a/homeassistant/components/triggercmd/switch.py +++ b/homeassistant/components/triggercmd/switch.py @@ -82,5 +82,6 @@ async def trigger(self, params: str): "params": params, "sender": "Home Assistant", }, + self._switch.hub.httpx_client, ) _LOGGER.debug("TRIGGERcmd trigger response: %s", r.json()) diff --git a/requirements_all.txt b/requirements_all.txt index 45cdadc41de02f..a91a3c424295bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2978,7 +2978,7 @@ tplink-omada-client==1.4.4 transmission-rpc==7.0.3 # homeassistant.components.triggercmd -triggercmd==0.0.27 +triggercmd==0.0.36 # homeassistant.components.twinkly ttls==1.8.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84f6b02ccacf6d..7b16e93c15a8dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2452,7 +2452,7 @@ tplink-omada-client==1.4.4 transmission-rpc==7.0.3 # homeassistant.components.triggercmd -triggercmd==0.0.27 +triggercmd==0.0.36 # homeassistant.components.twinkly ttls==1.8.3 From d9af4f1b3c251ec69475bf9b0b74f11b9f70c0d3 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 29 Aug 2025 16:17:42 -0500 Subject: [PATCH 40/95] Bump intents to 2025.8.29 (#151397) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index a4c13f76efb893..f0fdfc49509148 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==3.2.0", "home-assistant-intents==2025.8.27"] + "requirements": ["hassil==3.2.0", "home-assistant-intents==2025.8.29"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 83611156cecc69..59f4185ff21349 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250829.0 -home-assistant-intents==2025.8.27 +home-assistant-intents==2025.8.29 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index a91a3c424295bd..0e2bb28b6c1668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ holidays==0.79 home-assistant-frontend==20250829.0 # homeassistant.components.conversation -home-assistant-intents==2025.8.27 +home-assistant-intents==2025.8.29 # homeassistant.components.homematicip_cloud homematicip==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b16e93c15a8dc..ed68c9890f44d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ holidays==0.79 home-assistant-frontend==20250829.0 # homeassistant.components.conversation -home-assistant-intents==2025.8.27 +home-assistant-intents==2025.8.29 # homeassistant.components.homematicip_cloud homematicip==2.3.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index c77745d04b1184..8cf40ae8c33e00 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==3.2.0 \ - home-assistant-intents==2025.8.27 \ + home-assistant-intents==2025.8.29 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 From 281bf2f3087f31380e626a04af652a2b8a452612 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Aug 2025 15:38:34 -0600 Subject: [PATCH 41/95] Bump `aiopurpleair` to 2025.08.1 (#151398) --- homeassistant/components/purpleair/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/purpleair/manifest.json b/homeassistant/components/purpleair/manifest.json index 87cb375c347b54..a1cebb289c9740 100644 --- a/homeassistant/components/purpleair/manifest.json +++ b/homeassistant/components/purpleair/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/purpleair", "iot_class": "cloud_polling", - "requirements": ["aiopurpleair==2023.12.0"] + "requirements": ["aiopurpleair==2025.08.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e2bb28b6c1668..6975ee3e73ef47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -346,7 +346,7 @@ aiopegelonline==0.1.1 aiopulse==0.4.6 # homeassistant.components.purpleair -aiopurpleair==2023.12.0 +aiopurpleair==2025.08.1 # homeassistant.components.hunterdouglas_powerview aiopvapi==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed68c9890f44d5..87a9a91a3d81a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ aiopegelonline==0.1.1 aiopulse==0.4.6 # homeassistant.components.purpleair -aiopurpleair==2023.12.0 +aiopurpleair==2025.08.1 # homeassistant.components.hunterdouglas_powerview aiopvapi==3.1.1 From 66442f1714bdffb146c145db824dc6563045e69c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 30 Aug 2025 22:06:01 +0200 Subject: [PATCH 42/95] Allow integration to initialize when BraviaTV is offline (#151415) --- homeassistant/components/braviatv/entity.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index e1c6260b070ef9..faeaed7a5d1766 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -1,5 +1,7 @@ """A entity class for Bravia TV integration.""" +from typing import TYPE_CHECKING + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -17,11 +19,15 @@ def __init__(self, coordinator: BraviaTVCoordinator, unique_id: str) -> None: super().__init__(coordinator) self._attr_unique_id = unique_id + + if TYPE_CHECKING: + assert coordinator.client.mac is not None + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, - connections={(CONNECTION_NETWORK_MAC, coordinator.system_info["macAddr"])}, + connections={(CONNECTION_NETWORK_MAC, coordinator.client.mac)}, manufacturer=ATTR_MANUFACTURER, - model_id=coordinator.system_info["model"], - hw_version=coordinator.system_info["generation"], - serial_number=coordinator.system_info["serial"], + model_id=coordinator.system_info.get("model"), + hw_version=coordinator.system_info.get("generation"), + serial_number=coordinator.system_info.get("serial"), ) From fbab53bd0c88ee3a6aa7ff7c7325643aa38e80e9 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:16:19 +0200 Subject: [PATCH 43/95] Bump aioautomower to 2.2.1 (#151427) --- .../components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/husqvarna_automower/test_init.py | 14 ++++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 60ac9fe4fa5531..03605cc738b0df 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2.2.0"] + "requirements": ["aioautomower==2.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6975ee3e73ef47..26a166992a45b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.2.0 +aioautomower==2.2.1 # homeassistant.components.azure_devops aioazuredevops==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87a9a91a3d81a8..35c665ecda5b68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2.2.0 +aioautomower==2.2.1 # homeassistant.components.azure_devops aioazuredevops==2.2.2 diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index a157380ab3c416..271b381d32f19c 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -525,10 +525,11 @@ def fake_register_websocket_response( mock_automower_client.register_data_callback.side_effect = ( fake_register_websocket_response ) + ws_ready_callbacks: list[Callable[[], None]] = [] @callback def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None: - callback_holder["ws_ready_cb"] = cb + ws_ready_callbacks.append(cb) mock_automower_client.register_ws_ready_callback.side_effect = ( fake_register_ws_ready_callback @@ -536,8 +537,8 @@ def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None: await setup_integration(hass, mock_config_entry) - assert "ws_ready_cb" in callback_holder, "ws_ready_callback was not registered" - callback_holder["ws_ready_cb"]() + for cb in ws_ready_callbacks: + cb() await hass.async_block_till_done() assert mock_automower_client.get_status.call_count == 1 @@ -631,10 +632,11 @@ def fake_register_websocket_response( mock_automower_client.register_data_callback.side_effect = ( fake_register_websocket_response ) + ws_ready_callbacks: list[Callable[[], None]] = [] @callback def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None: - callback_holder["ws_ready_cb"] = cb + ws_ready_callbacks.append(cb) mock_automower_client.register_ws_ready_callback.side_effect = ( fake_register_ws_ready_callback @@ -642,8 +644,8 @@ def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None: await setup_integration(hass, mock_config_entry) - assert "ws_ready_cb" in callback_holder, "ws_ready_callback was not registered" - callback_holder["ws_ready_cb"]() + for cb in ws_ready_callbacks: + cb() await hass.async_block_till_done() assert mock_automower_client.get_status.call_count == 1 From d00bf4b01407e749935ededb68bdc166eaec4855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Sep 2025 03:18:53 -0500 Subject: [PATCH 44/95] Fix Yale Access Bluetooth key discovery timing issues (#151433) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/yalexs_ble/__init__.py | 44 ++- .../components/yalexs_ble/config_cache.py | 31 ++ .../components/yalexs_ble/config_flow.py | 115 ++++-- .../components/yalexs_ble/strings.json | 15 +- .../components/yalexs_ble/test_config_flow.py | 338 +++++++++++++----- 5 files changed, 416 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/yalexs_ble/config_cache.py diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 68d64494e41810..82d029f33ccbbe 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -19,6 +19,7 @@ from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from .config_cache import async_get_validated_config from .const import ( CONF_ALWAYS_CONNECTED, CONF_KEY, @@ -96,13 +97,30 @@ def _async_shutdown(event: Event | None = None) -> None: ) try: - await push_lock.wait_for_first_update(DEVICE_TIMEOUT) - except AuthError as ex: - raise ConfigEntryAuthFailed(str(ex)) from ex - except (YaleXSBLEError, TimeoutError) as ex: - raise ConfigEntryNotReady( - f"{ex}; Try moving the Bluetooth adapter closer to {local_name}" - ) from ex + await _async_wait_for_first_update(push_lock, local_name) + except ConfigEntryAuthFailed: + # If key has rotated, try to fetch it from the cache + # and update + if (validated_config := async_get_validated_config(hass, address)) and ( + validated_config.key != entry.data[CONF_KEY] + or validated_config.slot != entry.data[CONF_SLOT] + ): + assert shutdown_callback is not None + shutdown_callback() + push_lock.set_lock_key(validated_config.key, validated_config.slot) + shutdown_callback = await push_lock.start() + await _async_wait_for_first_update(push_lock, local_name) + # If we can use the cached key and slot, update the entry. + hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_KEY: validated_config.key, + CONF_SLOT: validated_config.slot, + }, + ) + else: + raise entry.runtime_data = YaleXSBLEData(entry.title, push_lock, always_connected) @@ -147,6 +165,18 @@ async def _async_update_listener( await hass.config_entries.async_reload(entry.entry_id) +async def _async_wait_for_first_update(push_lock: PushLock, local_name: str) -> None: + """Wait for the first update from the push lock.""" + try: + await push_lock.wait_for_first_update(DEVICE_TIMEOUT) + except AuthError as ex: + raise ConfigEntryAuthFailed(str(ex)) from ex + except (YaleXSBLEError, TimeoutError) as ex: + raise ConfigEntryNotReady( + f"{ex}; Try moving the Bluetooth adapter closer to {local_name}" + ) from ex + + async def async_unload_entry(hass: HomeAssistant, entry: YALEXSBLEConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/yalexs_ble/config_cache.py b/homeassistant/components/yalexs_ble/config_cache.py new file mode 100644 index 00000000000000..eccfbf3ea9e092 --- /dev/null +++ b/homeassistant/components/yalexs_ble/config_cache.py @@ -0,0 +1,31 @@ +"""The Yale Access Bluetooth integration.""" + +from __future__ import annotations + +from yalexs_ble import ValidatedLockConfig + +from homeassistant.core import HomeAssistant, callback +from homeassistant.util.hass_dict import HassKey + +CONFIG_CACHE: HassKey[dict[str, ValidatedLockConfig]] = HassKey( + "yalexs_ble_config_cache" +) + + +@callback +def async_add_validated_config( + hass: HomeAssistant, + address: str, + config: ValidatedLockConfig, +) -> None: + """Add a validated config.""" + hass.data.setdefault(CONFIG_CACHE, {})[address] = config + + +@callback +def async_get_validated_config( + hass: HomeAssistant, + address: str, +) -> ValidatedLockConfig | None: + """Get the config for a specific address.""" + return hass.data.get(CONFIG_CACHE, {}).get(address) diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index 0e1eabdf6b215f..dbaf44bc6e6bd9 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.typing import DiscoveryInfoType +from .config_cache import async_add_validated_config, async_get_validated_config from .const import CONF_ALWAYS_CONNECTED, CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN from .util import async_find_existing_service_info, human_readable_name @@ -92,7 +93,10 @@ async def async_step_bluetooth( None, discovery_info.name, discovery_info.address ), } - return await self.async_step_user() + if lock_cfg := async_get_validated_config(self.hass, discovery_info.address): + self._lock_cfg = lock_cfg + return await self.async_step_integration_discovery_confirm() + return await self.async_step_key_slot() async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType @@ -105,6 +109,7 @@ async def async_step_integration_discovery( discovery_info["key"], discovery_info["slot"], ) + async_add_validated_config(self.hass, lock_cfg.address, lock_cfg) address = lock_cfg.address self.local_name = lock_cfg.local_name @@ -232,22 +237,28 @@ async def async_step_reauth_validate( errors=errors, ) - async def async_step_user( + async def async_step_key_slot( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the user step to pick discovered device.""" + """Handle the key and slot step.""" errors: dict[str, str] = {} + discovery_info = self._discovery_info + assert discovery_info is not None + address = discovery_info.address + validated_config = async_get_validated_config(self.hass, address) - if user_input is not None: - self.active = True - address = user_input[CONF_ADDRESS] - discovery_info = self._discovered_devices[address] + if user_input is not None or validated_config: local_name = discovery_info.name - key = user_input[CONF_KEY] - slot = user_input[CONF_SLOT] - await self.async_set_unique_id( - discovery_info.address, raise_on_progress=False - ) + if validated_config: + key = validated_config.key + slot = validated_config.slot + title = validated_config.name + else: + assert user_input is not None + key = user_input[CONF_KEY] + slot = user_input[CONF_SLOT] + title = human_readable_name(None, local_name, address) + await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() if not ( errors := await async_validate_lock_or_error( @@ -255,7 +266,7 @@ async def async_step_user( ) ): return self.async_create_entry( - title=local_name, + title=title, data={ CONF_LOCAL_NAME: discovery_info.name, CONF_ADDRESS: discovery_info.address, @@ -264,24 +275,48 @@ async def async_step_user( }, ) - if discovery := self._discovery_info: + return self.async_show_form( + step_id="key_slot", + data_schema=vol.Schema( + { + vol.Required(CONF_KEY): str, + vol.Required(CONF_SLOT): int, + } + ), + errors=errors, + description_placeholders={ + "address": address, + "title": self._async_get_name_from_address(address), + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + self.active = True + address = user_input[CONF_ADDRESS] + self._discovery_info = self._discovered_devices[address] + return await self.async_step_key_slot() + + current_addresses = self._async_current_ids(include_ignore=False) + current_unique_names = { + entry.data.get(CONF_LOCAL_NAME) + for entry in self._async_current_entries() + if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME)) + } + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.name in current_unique_names + or discovery.address in self._discovered_devices + or YALE_MFR_ID not in discovery.manufacturer_data + ): + continue self._discovered_devices[discovery.address] = discovery - else: - current_addresses = self._async_current_ids(include_ignore=False) - current_unique_names = { - entry.data.get(CONF_LOCAL_NAME) - for entry in self._async_current_entries() - if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME)) - } - for discovery in async_discovered_service_info(self.hass): - if ( - discovery.address in current_addresses - or discovery.name in current_unique_names - or discovery.address in self._discovered_devices - or YALE_MFR_ID not in discovery.manufacturer_data - ): - continue - self._discovered_devices[discovery.address] = discovery if not self._discovered_devices: return self.async_abort(reason="no_devices_found") @@ -290,14 +325,12 @@ async def async_step_user( { vol.Required(CONF_ADDRESS): vol.In( { - service_info.address: ( - f"{service_info.name} ({service_info.address})" + service_info.address: self._async_get_name_from_address( + service_info.address ) for service_info in self._discovered_devices.values() } - ), - vol.Required(CONF_KEY): str, - vol.Required(CONF_SLOT): int, + ) } ) return self.async_show_form( @@ -306,6 +339,18 @@ async def async_step_user( errors=errors, ) + @callback + def _async_get_name_from_address(self, address: str) -> str: + """Get the name of a device from its address.""" + if validated_config := async_get_validated_config(self.hass, address): + return f"{validated_config.name} ({address})" + if address in self._discovered_devices: + service_info = self._discovered_devices[address] + return f"{service_info.name} ({service_info.address})" + assert self._discovery_info is not None + assert self._discovery_info.address == address + return f"{self._discovery_info.name} ({address})" + @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json index 92d807d01f6f50..604ff34aa6f9cc 100644 --- a/homeassistant/components/yalexs_ble/strings.json +++ b/homeassistant/components/yalexs_ble/strings.json @@ -3,18 +3,23 @@ "flow_title": "{name}", "step": { "user": { - "description": "Check the documentation for how to find the offline key. If you are using the August cloud integration to obtain the key, you may need to reload the August cloud integration while the lock is in Bluetooth range.", + "description": "Select the device you want to set up over Bluetooth.", + "data": { + "address": "Bluetooth address" + } + }, + "key_slot": { + "description": "Enter the key for the {title} lock with address {address}. If you are using the August or Yale cloud integration to obtain the key, you may be able to avoid this manual setup by reloading the August or Yale cloud integration while the lock is in Bluetooth range.", "data": { - "address": "Bluetooth address", "key": "Offline Key (32-byte hex string)", "slot": "Offline Key Slot (Integer between 0 and 255)" } }, "reauth_validate": { - "description": "Enter the updated key for the {title} lock with address {address}. If you are using the August cloud integration to obtain the key, you may be able to avoid manual reauthentication by reloading the August cloud integration while the lock is in Bluetooth range.", + "description": "Enter the updated key for the {title} lock with address {address}. If you are using the August or Yale cloud integration to obtain the key, you may be able to avoid manual re-authentication by reloading the August or Yale cloud integration while the lock is in Bluetooth range.", "data": { - "key": "[%key:component::yalexs_ble::config::step::user::data::key%]", - "slot": "[%key:component::yalexs_ble::config::step::user::data::slot%]" + "key": "[%key:component::yalexs_ble::config::step::key_slot::data::key%]", + "slot": "[%key:component::yalexs_ble::config::step::key_slot::data::slot%]" } }, "integration_discovery_confirm": { diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index c272036097dfa1..1c518b9ce33f40 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -61,6 +61,16 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} + with ( patch( "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", @@ -70,25 +80,24 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: slot, }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result2["data"] == { + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: slot, } - assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -113,6 +122,16 @@ async def test_user_step_from_ignored(hass: HomeAssistant, slot: int) -> None: assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} + with ( patch( "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", @@ -122,25 +141,24 @@ async def test_user_step_from_ignored(hass: HomeAssistant, slot: int) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: slot, }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result2["data"] == { + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: slot, } - assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -198,37 +216,44 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: result["flow_id"], { CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, - CONF_KEY: "dog", - CONF_SLOT: 66, }, ) assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {CONF_KEY: "invalid_key_format"} + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, - CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f", + CONF_KEY: "dog", CONF_SLOT: 66, }, ) assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "user" + assert result3["step_id"] == "key_slot" assert result3["errors"] == {CONF_KEY: "invalid_key_format"} result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + assert result4["type"] is FlowResultType.FORM + assert result4["step_id"] == "key_slot" + assert result4["errors"] == {CONF_KEY: "invalid_key_format"} + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 999, }, ) - assert result4["type"] is FlowResultType.FORM - assert result4["step_id"] == "user" - assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} + assert result5["type"] is FlowResultType.FORM + assert result5["step_id"] == "key_slot" + assert result5["errors"] == {CONF_SLOT: "invalid_key_index"} with ( patch( @@ -239,25 +264,24 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result5 = await hass.config_entries.flow.async_configure( - result4["flow_id"], + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result5["type"] is FlowResultType.CREATE_ENTRY - assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result5["data"] == { + assert result6["type"] is FlowResultType.CREATE_ENTRY + assert result6["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result6["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, } - assert result5["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result6["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -274,23 +298,32 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} + with patch( "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", side_effect=BleakError, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result3["type"] is FlowResultType.FORM + assert result3["step_id"] == "key_slot" + assert result3["errors"] == {"base": "cannot_connect"} with ( patch( @@ -301,25 +334,24 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result3["data"] == { + assert result4["type"] is FlowResultType.CREATE_ENTRY + assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result4["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, } - assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -336,23 +368,32 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} + with patch( "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", side_effect=AuthError, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {CONF_KEY: "invalid_auth"} + assert result3["type"] is FlowResultType.FORM + assert result3["step_id"] == "key_slot" + assert result3["errors"] == {CONF_KEY: "invalid_auth"} with ( patch( @@ -363,25 +404,24 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result3["data"] == { + assert result4["type"] is FlowResultType.CREATE_ENTRY + assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result4["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, } - assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -398,23 +438,32 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + assert result2["errors"] == {} + with patch( "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", side_effect=RuntimeError, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result3["type"] is FlowResultType.FORM + assert result3["step_id"] == "key_slot" + assert result3["errors"] == {"base": "unknown"} with ( patch( @@ -425,25 +474,24 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, ) await hass.async_block_till_done() - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name - assert result3["data"] == { + assert result4["type"] is FlowResultType.CREATE_ENTRY + assert result4["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" + assert result4["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, } - assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert result4["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 @@ -455,7 +503,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "key_slot" assert result["errors"] == {} with ( @@ -470,7 +518,6 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, @@ -478,7 +525,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, @@ -563,7 +610,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "key_slot" assert result["errors"] == {} flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN]) assert len(flows) == 1 @@ -629,6 +676,60 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( assert len(flows) == 0 +async def test_bluetooth_discovery_with_cached_config( + hass: HomeAssistant, +) -> None: + """Test bluetooth discovery when validated config is already in cache.""" + # First, populate the cache via integration discovery + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + # Now do bluetooth discovery with the cached config + with patch( + "homeassistant.components.yalexs_ble.PushLock.validate", + return_value=None, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["description_placeholders"] == { + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + } + + # Confirm the discovery + with patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Front Door" + assert result["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + + async def test_integration_discovery_updates_key_unique_local_name( hass: HomeAssistant, ) -> None: @@ -774,7 +875,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres data=LOCK_DISCOVERY_INFO_UUID_ADDRESS, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "key_slot" assert result["errors"] == {} flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN]) assert len(flows) == 1 @@ -850,7 +951,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "key_slot" assert result["errors"] == {} flows = list(hass.config_entries.flow._handler_progress_index[DOMAIN]) assert len(flows) == 1 @@ -907,6 +1008,15 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( assert result["step_id"] == "user" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + user_flow_event = asyncio.Event() valdidate_started = asyncio.Event() @@ -926,9 +1036,8 @@ async def _wait_for_user_flow(): ): user_flow_task = asyncio.create_task( hass.config_entries.flow.async_configure( - result["flow_id"], + result2["flow_id"], { - CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", CONF_SLOT: 66, }, @@ -959,7 +1068,7 @@ async def _wait_for_user_flow(): user_flow_result = await user_flow_task assert user_flow_result["type"] is FlowResultType.CREATE_ENTRY - assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert user_flow_result["title"] == f"{YALE_ACCESS_LOCK_DISCOVERY_INFO.name} (EEFF)" assert user_flow_result["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, @@ -1033,6 +1142,75 @@ async def test_reauth(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_step_with_cached_config(hass: HomeAssistant) -> None: + """Test user step when config is already cached from integration discovery.""" + # First, simulate integration discovery to populate the cache + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "no_devices_found" + + # Now start a user flow - it should use the cached config + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # The dropdown should show "Front Door (AA:BB:CC:DD:EE:FF)" from cached config + # This is the line 346 case we're testing + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "key_slot" + + # The key_slot step should auto-complete with cached values + # When no user input is provided, it should use the cached config + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + # No user input triggers using cached config + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + None, # None triggers checking for cached config + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Front Door" # Uses the name from cached config + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_options(hass: HomeAssistant) -> None: """Test options.""" entry = MockConfigEntry( From 74c91e46f2f1ce6b7b7cb3a308ab130e3b889457 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 1 Sep 2025 17:48:49 +1000 Subject: [PATCH 45/95] Fix history startup failures (#151439) --- .../components/tesla_fleet/__init__.py | 1 - .../tesla_fleet/snapshots/test_sensor.ambr | 42 +++++++++---------- tests/components/tesla_fleet/test_init.py | 22 ++++++---- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 2642bd2f7d5589..8cf5f8b2b5843e 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -179,7 +179,6 @@ async def _refresh_token() -> str: ) await live_coordinator.async_config_entry_first_refresh() - await history_coordinator.async_config_entry_first_refresh() await info_coordinator.async_config_entry_first_refresh() # Create energy site model diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index f6268627be1b13..f7ac1ef8b6011c 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -55,7 +55,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_charged-statealt] @@ -130,7 +130,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_discharged-statealt] @@ -205,7 +205,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30.06', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_exported-statealt] @@ -280,7 +280,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_imported_from_generator-statealt] @@ -355,7 +355,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.08', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_imported_from_grid-statealt] @@ -430,7 +430,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '43.6', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_imported_from_solar-statealt] @@ -580,7 +580,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30.022', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_consumer_imported_from_battery-statealt] @@ -655,7 +655,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_consumer_imported_from_generator-statealt] @@ -730,7 +730,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.282', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_consumer_imported_from_grid-statealt] @@ -805,7 +805,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30.96', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_consumer_imported_from_solar-statealt] @@ -955,7 +955,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.001', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_generator_exported-statealt] @@ -1105,7 +1105,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_exported-statealt] @@ -1180,7 +1180,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.048', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_exported_from_battery-statealt] @@ -1255,7 +1255,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_exported_from_generator-statealt] @@ -1330,7 +1330,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '127.32', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_exported_from_solar-statealt] @@ -1405,7 +1405,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.542', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_imported-statealt] @@ -1555,7 +1555,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0106171875', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_services_exported-statealt] @@ -1630,7 +1630,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0450625', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_services_imported-statealt] @@ -1865,7 +1865,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_home_usage-statealt] @@ -2087,7 +2087,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '211.88', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_solar_exported-statealt] @@ -2162,7 +2162,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_solar_generated-statealt] diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 7bd90a3568cbfa..3645a0f434da8e 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -317,18 +317,26 @@ async def test_energy_site_refresh_error( # Test Energy History Coordinator -@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +@pytest.mark.parametrize(("side_effect"), [side_effect for side_effect, _ in ERRORS]) async def test_energy_history_refresh_error( hass: HomeAssistant, normal_config_entry: MockConfigEntry, mock_energy_history: AsyncMock, side_effect: TeslaFleetError, - state: ConfigEntryState, + freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh with an error.""" - mock_energy_history.side_effect = side_effect await setup_platform(hass, normal_config_entry) - assert normal_config_entry.state is state + assert normal_config_entry.state is ConfigEntryState.LOADED + + # Now test that the coordinator handles errors during refresh + mock_energy_history.side_effect = side_effect + freezer.tick(ENERGY_HISTORY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # The coordinator should handle the error gracefully + assert normal_config_entry.state is ConfigEntryState.LOADED async def test_energy_live_refresh_ratelimited( @@ -410,20 +418,20 @@ async def test_energy_history_refresh_ratelimited( async_fire_time_changed(hass) await hass.async_block_till_done() - assert mock_energy_history.call_count == 2 + assert mock_energy_history.call_count == 1 freezer.tick(ENERGY_HISTORY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() # Should not call for another 10 seconds - assert mock_energy_history.call_count == 2 + assert mock_energy_history.call_count == 1 freezer.tick(ENERGY_HISTORY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert mock_energy_history.call_count == 3 + assert mock_energy_history.call_count == 2 async def test_init_region_issue( From 0050626d8ce047b5071cab94ebc0dd2a363b65f8 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 31 Aug 2025 03:14:14 -0700 Subject: [PATCH 46/95] Bump opower to 0.15.4 (#151443) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index a3f29071ce9675..dc69c33cd5d467 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.15.3"] + "requirements": ["opower==0.15.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 26a166992a45b9..da8ea0b3410913 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.3 +opower==0.15.4 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35c665ecda5b68..33a6b44b6ee418 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1384,7 +1384,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.3 +opower==0.15.4 # homeassistant.components.oralb oralb-ble==0.17.6 From bfa3b534092283917fb473bc5a5dbc9d15ed36e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Aug 2025 17:13:03 -0500 Subject: [PATCH 47/95] Bump bluetooth-adapters to 2.1.0 and habluetooth to 5.3.0 (#151465) --- homeassistant/components/bluetooth/__init__.py | 2 +- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/bluetooth/test_diagnostics.py | 2 ++ tests/components/bluetooth/test_websocket_api.py | 5 +++++ 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index e3428eb9b869ed..8568724c0b137a 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -385,10 +385,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Bluetooth adapter {adapter} with address {address} not found" ) passive = entry.options.get(CONF_PASSIVE) + adapters = await manager.async_get_bluetooth_adapters() mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE scanner = HaScanner(mode, adapter, address) scanner.async_setup() - adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] if entry.title == address: hass.config_entries.async_update_entry( diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 95bb58204233c7..5559e5e8710300 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,10 +17,10 @@ "requirements": [ "bleak==1.0.1", "bleak-retry-connector==4.4.3", - "bluetooth-adapters==2.0.0", + "bluetooth-adapters==2.1.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==5.2.1" + "habluetooth==5.3.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 59f4185ff21349..21c72c8fbb5b44 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ awesomeversion==25.5.0 bcrypt==4.3.0 bleak-retry-connector==4.4.3 bleak==1.0.1 -bluetooth-adapters==2.0.0 +bluetooth-adapters==2.1.0 bluetooth-auto-recovery==1.5.2 bluetooth-data-tools==1.28.2 cached-ipaddress==0.10.0 @@ -34,7 +34,7 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==5.2.1 +habluetooth==5.3.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index da8ea0b3410913..b033c7eb053aba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ bluemaestro-ble==0.4.1 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==2.0.0 +bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.5.2 @@ -1133,7 +1133,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.2.1 +habluetooth==5.3.0 # homeassistant.components.cloud hass-nabucasa==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33a6b44b6ee418..992c5aaee1d204 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,7 +583,7 @@ bluemaestro-ble==0.4.1 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==2.0.0 +bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.5.2 @@ -994,7 +994,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==5.2.1 +habluetooth==5.3.0 # homeassistant.components.cloud hass-nabucasa==1.0.0 diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 5c4d8bda70d153..599d6833163944 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -297,6 +297,7 @@ async def test_diagnostics_macos( assert diag == { "adapters": { "Core Bluetooth": { + "adapter_type": None, "address": "00:00:00:00:00:00", "manufacturer": "Apple", "passive_scan": False, @@ -317,6 +318,7 @@ async def test_diagnostics_macos( }, "adapters": { "Core Bluetooth": { + "adapter_type": None, "address": "00:00:00:00:00:00", "manufacturer": "Apple", "passive_scan": False, diff --git a/tests/components/bluetooth/test_websocket_api.py b/tests/components/bluetooth/test_websocket_api.py index f12d77913a93a2..19693db40004e1 100644 --- a/tests/components/bluetooth/test_websocket_api.py +++ b/tests/components/bluetooth/test_websocket_api.py @@ -332,6 +332,7 @@ async def test_subscribe_scanner_details( "connectable": False, "name": "hci0 (00:00:00:00:00:01)", "source": "00:00:00:00:00:01", + "scanner_type": "unknown", } ] } @@ -349,6 +350,7 @@ async def test_subscribe_scanner_details( "connectable": False, "name": "hci3 (AA:BB:CC:DD:EE:33)", "source": "AA:BB:CC:DD:EE:33", + "scanner_type": "unknown", } ] } @@ -362,6 +364,7 @@ async def test_subscribe_scanner_details( "connectable": False, "name": "hci3 (AA:BB:CC:DD:EE:33)", "source": "AA:BB:CC:DD:EE:33", + "scanner_type": "unknown", } ] } @@ -399,6 +402,7 @@ async def test_subscribe_scanner_details_specific_scanner( "connectable": False, "name": "hci3 (AA:BB:CC:DD:EE:33)", "source": "AA:BB:CC:DD:EE:33", + "scanner_type": "unknown", } ] } @@ -412,6 +416,7 @@ async def test_subscribe_scanner_details_specific_scanner( "connectable": False, "name": "hci3 (AA:BB:CC:DD:EE:33)", "source": "AA:BB:CC:DD:EE:33", + "scanner_type": "unknown", } ] } From 5edb786aad6511e16729b0e8b6f86171c1d0f1ab Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 1 Sep 2025 13:06:09 +0300 Subject: [PATCH 48/95] Allow structure field of ai_task.generate_data for non-advanced users (#151481) --- homeassistant/components/ai_task/services.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/ai_task/services.yaml b/homeassistant/components/ai_task/services.yaml index 17a3b499bfe28b..8a37990a5d726d 100644 --- a/homeassistant/components/ai_task/services.yaml +++ b/homeassistant/components/ai_task/services.yaml @@ -20,7 +20,6 @@ generate_data: supported_features: - ai_task.AITaskEntityFeature.GENERATE_DATA structure: - advanced: true required: false example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }' selector: From a67919fd7c91589035df1c362f01d75e6a3ebf50 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:22:23 +0200 Subject: [PATCH 49/95] Fix backup manager delete backup error filter (#151490) --- homeassistant/components/backup/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index f1b2f7d5b976d3..863775a32ed34b 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -896,7 +896,8 @@ async def async_delete_filtered_backups( ) agent_errors = { backup_id: error - for backup_id, error in zip(backup_ids, delete_results, strict=True) + for backup_id, error_dict in zip(backup_ids, delete_results, strict=True) + for error in error_dict.values() if error and not isinstance(error, BackupNotFound) } if agent_errors: From e3d08d5f2654da5dc4cdb615bb0e4766901d57a2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Sep 2025 11:59:25 +0200 Subject: [PATCH 50/95] Set Aladdin Connect integration type to hub (#151491) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/generated/integrations.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index d6b4dd2625f238..67c755e29a8ecd 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "integration_type": "system", + "integration_type": "hub", "iot_class": "cloud_polling", "requirements": ["genie-partner-sdk==1.0.10"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f117008fedf99f..0df4cc993cd5c0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -187,6 +187,12 @@ } } }, + "aladdin_connect": { + "name": "Aladdin Connect", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "alarmdecoder": { "name": "AlarmDecoder", "integration_type": "device", From 8d1a45bb8b9e8952de2cda4e2fb938d1f3ae3280 Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Mon, 1 Sep 2025 12:34:27 +0200 Subject: [PATCH 51/95] Missing state for inverter state sensor in Imeon inverter (#151493) Co-authored-by: TheBushBoy --- homeassistant/components/imeon_inverter/const.py | 1 + homeassistant/components/imeon_inverter/strings.json | 1 + tests/components/imeon_inverter/snapshots/test_sensor.ambr | 2 ++ 3 files changed, 4 insertions(+) diff --git a/homeassistant/components/imeon_inverter/const.py b/homeassistant/components/imeon_inverter/const.py index 9cde40e01d79ce..45d43b1c1efd2d 100644 --- a/homeassistant/components/imeon_inverter/const.py +++ b/homeassistant/components/imeon_inverter/const.py @@ -9,6 +9,7 @@ ] ATTR_BATTERY_STATUS = ["charging", "discharging", "charged"] ATTR_INVERTER_STATE = [ + "not_connected", "unsynchronized", "grid_consumption", "grid_injection", diff --git a/homeassistant/components/imeon_inverter/strings.json b/homeassistant/components/imeon_inverter/strings.json index 66d0472b89ae9a..6e1e3bb69ff92d 100644 --- a/homeassistant/components/imeon_inverter/strings.json +++ b/homeassistant/components/imeon_inverter/strings.json @@ -91,6 +91,7 @@ "manager_inverter_state": { "name": "Inverter state", "state": { + "not_connected": "Not connected", "unsynchronized": "Unsynchronized", "grid_consumption": "Grid consumption", "grid_injection": "Grid injection", diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index 673f561d540879..b860566a516579 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -1351,6 +1351,7 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'not_connected', 'unsynchronized', 'grid_consumption', 'grid_injection', @@ -1392,6 +1393,7 @@ 'device_class': 'enum', 'friendly_name': 'Imeon inverter Inverter state', 'options': list([ + 'not_connected', 'unsynchronized', 'grid_consumption', 'grid_injection', From 249dbf976f2352d4b80c02719e14fd6ddf596473 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Sep 2025 10:36:21 +0000 Subject: [PATCH 52/95] Bump version to 2025.9.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5e2cceed75a800..864c18ae23c075 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 72d73618629450..a087ecfe6c4dbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b1" +version = "2025.9.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 9f4369dc8b25770033986133b0fb80b664891264 Mon Sep 17 00:00:00 2001 From: Phil Male Date: Mon, 1 Sep 2025 14:14:50 +0100 Subject: [PATCH 53/95] Use average color for Hue light group state (#149499) --- homeassistant/components/hue/v2/group.py | 79 ++++- tests/components/hue/test_light_v2.py | 361 ++++++++++++++++++++++- 2 files changed, 426 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 4db9bc16ca8997..41956824ab2221 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -226,15 +226,26 @@ def _update_values(self) -> None: lights_with_color_support = 0 lights_with_color_temp_support = 0 lights_with_dimming_support = 0 + lights_on_with_dimming_support = 0 total_brightness = 0 all_lights = self.controller.get_lights(self.resource.id) lights_in_colortemp_mode = 0 + lights_in_xy_mode = 0 lights_in_dynamic_mode = 0 + # accumulate color values + xy_total_x = 0.0 + xy_total_y = 0.0 + xy_count = 0 + temp_total = 0.0 + # loop through all lights to find capabilities for light in all_lights: + # reset per-light colortemp on flag + light_in_colortemp_mode = False + # check if light has color temperature if color_temp := light.color_temperature: lights_with_color_temp_support += 1 - # we assume mired values from the first capable light + # default to mired values from the last capable light self._attr_color_temp_kelvin = ( color_util.color_temperature_mired_to_kelvin(color_temp.mirek) if color_temp.mirek @@ -250,15 +261,39 @@ def _update_values(self) -> None: color_temp.mirek_schema.mirek_minimum ) ) - if color_temp.mirek is not None and color_temp.mirek_valid: + # counters for color mode vote and average temp + if ( + light.on.on + and color_temp.mirek is not None + and color_temp.mirek_valid + ): lights_in_colortemp_mode += 1 + light_in_colortemp_mode = True + temp_total += color_util.color_temperature_mired_to_kelvin( + color_temp.mirek + ) + # check if light has color xy if color := light.color: lights_with_color_support += 1 - # we assume xy values from the first capable light + # default to xy values from the last capable light self._attr_xy_color = (color.xy.x, color.xy.y) + # counters for color mode vote and average xy color + if light.on.on: + xy_total_x += color.xy.x + xy_total_y += color.xy.y + xy_count += 1 + # only count for colour mode vote if + # this light is not in colortemp mode + if not light_in_colortemp_mode: + lights_in_xy_mode += 1 + # check if light has dimming if dimming := light.dimming: lights_with_dimming_support += 1 - total_brightness += dimming.brightness + # accumulate brightness values + if light.on.on: + total_brightness += dimming.brightness + lights_on_with_dimming_support += 1 + # check if light is in dynamic mode if ( light.dynamics and light.dynamics.status == DynamicStatus.DYNAMIC_PALETTE @@ -266,10 +301,11 @@ def _update_values(self) -> None: lights_in_dynamic_mode += 1 # this is a bit hacky because light groups may contain lights - # of different capabilities. We set a colormode as supported - # if any of the lights support it + # of different capabilities # this means that the state is derived from only some of the lights # and will never be 100% accurate but it will be close + + # assign group color support modes based on light capabilities if lights_with_color_support > 0: supported_color_modes.add(ColorMode.XY) if lights_with_color_temp_support > 0: @@ -278,19 +314,38 @@ def _update_values(self) -> None: if len(supported_color_modes) == 0: # only add color mode brightness if no color variants supported_color_modes.add(ColorMode.BRIGHTNESS) - self._brightness_pct = total_brightness / lights_with_dimming_support - self._attr_brightness = round( - ((total_brightness / lights_with_dimming_support) / 100) * 255 - ) + # as we have brightness support, set group brightness values + if lights_on_with_dimming_support > 0: + self._brightness_pct = total_brightness / lights_on_with_dimming_support + self._attr_brightness = round( + ((total_brightness / lights_on_with_dimming_support) / 100) * 255 + ) else: supported_color_modes.add(ColorMode.ONOFF) self._dynamic_mode_active = lights_in_dynamic_mode > 0 self._attr_supported_color_modes = supported_color_modes - # pick a winner for the current colormode - if lights_with_color_temp_support > 0 and lights_in_colortemp_mode > 0: + # set the group color values if there are any color lights on + if xy_count > 0: + self._attr_xy_color = ( + round(xy_total_x / xy_count, 5), + round(xy_total_y / xy_count, 5), + ) + if lights_in_colortemp_mode > 0: + avg_temp = temp_total / lights_in_colortemp_mode + self._attr_color_temp_kelvin = round(avg_temp) + # pick a winner for the current color mode based on the majority of on lights + # if there is no winner pick the highest mode from group capabilities + if lights_in_xy_mode > 0 and lights_in_xy_mode >= lights_in_colortemp_mode: + self._attr_color_mode = ColorMode.XY + elif ( + lights_in_colortemp_mode > 0 + and lights_in_colortemp_mode > lights_in_xy_mode + ): self._attr_color_mode = ColorMode.COLOR_TEMP elif lights_with_color_support > 0: self._attr_color_mode = ColorMode.XY + elif lights_with_color_temp_support > 0: + self._attr_color_mode = ColorMode.COLOR_TEMP elif lights_with_dimming_support > 0: self._attr_color_mode = ColorMode.BRIGHTNESS else: diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 83b2bd48b3cd48..13cfe3995de9b0 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -518,9 +518,8 @@ async def test_grouped_lights( } mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() - await hass.async_block_till_done() - # the light should now be on and have the properties we've set + # The light should now be on and have the properties we've set test_light = hass.states.get(test_light_id) assert test_light is not None assert test_light.state == "on" @@ -528,6 +527,364 @@ async def test_grouped_lights( assert test_light.attributes["brightness"] == 255 assert test_light.attributes["xy_color"] == (0.123, 0.123) + # While we have a group on, test the color aggregation logic, XY first + + # Turn off one of the bulbs in the group + # "hue_light_with_color_and_color_temperature_1" corresponds to "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1" + mock_bridge_v2.mock_requests.clear() + single_light_id = "light.hue_light_with_color_and_color_temperature_1" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": single_light_id}, + blocking=True, + ) + event = { + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "type": "light", + "on": {"on": False}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # The group should still show the same XY color since other lights maintain their color + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["xy_color"] == (0.123, 0.123) + + # Turn the light back on with a white XY color (different from the rest of the group) + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": single_light_id, "xy_color": [0.3127, 0.3290]}, + blocking=True, + ) + event = { + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "type": "light", + "on": {"on": True}, + "color": {"xy": {"x": 0.3127, "y": 0.3290}}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # Now the group XY color should be the average of all three lights: + # Light 1: (0.3127, 0.3290) - white + # Light 2: (0.123, 0.123) + # Light 3: (0.123, 0.123) + # Average: ((0.3127 + 0.123 + 0.123) / 3, (0.3290 + 0.123 + 0.123) / 3) + # Average: (0.1862, 0.1917) rounded to 4 decimal places + expected_x = round((0.3127 + 0.123 + 0.123) / 3, 4) + expected_y = round((0.3290 + 0.123 + 0.123) / 3, 4) + + # Check that the group XY color is now the average of all lights + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + group_x, group_y = test_light.attributes["xy_color"] + assert abs(group_x - expected_x) < 0.001 # Allow small floating point differences + assert abs(group_y - expected_y) < 0.001 + + # Test turning off another light in the group, leaving only two lights on - one white and one original color + # "hue_light_with_color_and_color_temperature_2" corresponds to "b3fe71ef-d0ef-48de-9355-d9e604377df0" + second_light_id = "light.hue_light_with_color_and_color_temperature_2" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": second_light_id}, + blocking=True, + ) + + # Simulate the second light turning off + event = { + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "type": "light", + "on": {"on": False}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # Now only two lights are on: + # Light 1: (0.3127, 0.3290) - white + # Light 3: (0.123, 0.123) - original color + # Average of remaining lights: ((0.3127 + 0.123) / 2, (0.3290 + 0.123) / 2) + expected_x_two_lights = round((0.3127 + 0.123) / 2, 4) + expected_y_two_lights = round((0.3290 + 0.123) / 2, 4) + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + # Check that the group color is now the average of only the two remaining lights + group_x, group_y = test_light.attributes["xy_color"] + assert abs(group_x - expected_x_two_lights) < 0.001 + assert abs(group_y - expected_y_two_lights) < 0.001 + + # Test colour temperature aggregation + # Set all three lights to colour temperature mode with different mirek values + for mirek, light_name, light_id in zip( + [300, 250, 200], + [ + "light.hue_light_with_color_and_color_temperature_1", + "light.hue_light_with_color_and_color_temperature_2", + "light.hue_light_with_color_and_color_temperature_gradient", + ], + [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ], + strict=True, + ): + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": light_name, + "color_temp": mirek, + }, + blocking=True, + ) + # Emit update event with matching mirek value + mock_bridge_v2.api.emit_event( + "update", + { + "id": light_id, + "type": "light", + "on": {"on": True}, + "color_temperature": {"mirek": mirek, "mirek_valid": True}, + }, + ) + await hass.async_block_till_done() + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP + + # Expected average kelvin calculation: + # 300 mirek ≈ 3333K, 250 mirek ≈ 4000K, 200 mirek ≈ 5000K + expected_avg_kelvin = round((3333 + 4000 + 5000) / 3) + assert abs(test_light.attributes["color_temp_kelvin"] - expected_avg_kelvin) <= 5 + + # Switch light 3 off and check average kelvin temperature of remaining two lights + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.hue_light_with_color_and_color_temperature_gradient"}, + blocking=True, + ) + event = { + "id": "8015b17f-8336-415b-966a-b364bd082397", + "type": "light", + "on": {"on": False}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP + + # Expected average kelvin calculation: + # 300 mirek ≈ 3333K, 250 mirek ≈ 4000K + expected_avg_kelvin = round((3333 + 4000) / 2) + assert abs(test_light.attributes["color_temp_kelvin"] - expected_avg_kelvin) <= 5 + + # Turn light 3 back on in XY mode and verify majority still favours COLOR_TEMP + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.hue_light_with_color_and_color_temperature_gradient", + "xy_color": [0.123, 0.123], + }, + blocking=True, + ) + mock_bridge_v2.api.emit_event( + "update", + { + "id": "8015b17f-8336-415b-966a-b364bd082397", + "type": "light", + "on": {"on": True}, + "color": {"xy": {"x": 0.123, "y": 0.123}}, + "color_temperature": { + "mirek": None, + "mirek_valid": False, + }, + }, + ) + await hass.async_block_till_done() + + test_light = hass.states.get(test_light_id) + assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP + + # Switch light 2 to XY mode to flip the majority + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.hue_light_with_color_and_color_temperature_2", + "xy_color": [0.321, 0.321], + }, + blocking=True, + ) + mock_bridge_v2.api.emit_event( + "update", + { + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "type": "light", + "on": {"on": True}, + "color": {"xy": {"x": 0.321, "y": 0.321}}, + "color_temperature": { + "mirek": None, + "mirek_valid": False, + }, + }, + ) + await hass.async_block_till_done() + + test_light = hass.states.get(test_light_id) + assert test_light.attributes["color_mode"] == ColorMode.XY + + # Test brightness aggregation with different brightness levels + mock_bridge_v2.mock_requests.clear() + + # Set all three lights to different brightness levels + for brightness, light_name, light_id in zip( + [90.0, 60.0, 30.0], + [ + "light.hue_light_with_color_and_color_temperature_1", + "light.hue_light_with_color_and_color_temperature_2", + "light.hue_light_with_color_and_color_temperature_gradient", + ], + [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ], + strict=True, + ): + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": light_name, + "brightness": brightness, + }, + blocking=True, + ) + # Emit update event with matching brightness value + mock_bridge_v2.api.emit_event( + "update", + { + "id": light_id, + "type": "light", + "on": {"on": True}, + "dimming": {"brightness": brightness}, + }, + ) + await hass.async_block_till_done() + + # Check that the group brightness is the average of all three lights + # Expected average: (90 + 60 + 30) / 3 = 60% -> 153 (60% of 255) + expected_brightness = round(((90 + 60 + 30) / 3 / 100) * 255) + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["brightness"] == expected_brightness + + # Turn off the dimmest light 3 (30% brightness) while keeping the other two on + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.hue_light_with_color_and_color_temperature_gradient"}, + blocking=True, + ) + event = { + "id": "8015b17f-8336-415b-966a-b364bd082397", + "type": "light", + "on": {"on": False}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # Check that the group brightness is now the average of the two remaining lights + # Expected average: (90 + 60) / 2 = 75% -> 191 (75% of 255) + expected_brightness_two_lights = round(((90 + 60) / 2 / 100) * 255) + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["brightness"] == expected_brightness_two_lights + + # Turn off light 2 (60% brightness), leaving only the brightest one + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": "light.hue_light_with_color_and_color_temperature_2"}, + blocking=True, + ) + event = { + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "type": "light", + "on": {"on": False}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # Check that the group brightness is now just the remaining light's brightness + # Expected brightness: 90% -> 230 (round(90 / 100 * 255)) + expected_brightness_one_light = round((90 / 100) * 255) + + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["brightness"] == expected_brightness_one_light + + # Set all three lights back to 100% brightness for consistency with later tests + for light_name, light_id in zip( + [ + "light.hue_light_with_color_and_color_temperature_1", + "light.hue_light_with_color_and_color_temperature_2", + "light.hue_light_with_color_and_color_temperature_gradient", + ], + [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ], + strict=True, + ): + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": light_name, + "brightness": 100.0, + }, + blocking=True, + ) + # Emit update event with matching brightness value + mock_bridge_v2.api.emit_event( + "update", + { + "id": light_id, + "type": "light", + "on": {"on": True}, + "dimming": {"brightness": 100.0}, + }, + ) + await hass.async_block_till_done() + + # Verify group is back to 100% brightness + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.state == "on" + assert test_light.attributes["brightness"] == 255 + # Test calling the turn off service on a grouped light. mock_bridge_v2.mock_requests.clear() await hass.services.async_call( From bbe66f5cea9d2f53502afaa93c424ba868e2cac9 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:57:24 +0200 Subject: [PATCH 54/95] Improve unpair schema in homekit (#150235) --- homeassistant/components/homekit/__init__.py | 5 ++--- homeassistant/components/homekit/services.yaml | 10 +++++++--- homeassistant/components/homekit/strings.json | 8 +++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 50b11265cf462f..7c132a00a77835 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -224,9 +224,8 @@ def _has_all_unique_names_and_ports( ) -UNPAIR_SERVICE_SCHEMA = vol.All( - vol.Schema(cv.ENTITY_SERVICE_FIELDS), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +UNPAIR_SERVICE_SCHEMA = vol.Schema( + {vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [str])} ) diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index de271db0ad9c70..8e9d659af942d2 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -6,6 +6,10 @@ reset_accessory: entity: {} unpair: - target: - device: - integration: homekit + fields: + device_id: + required: true + selector: + device: + multiple: true + integration: homekit diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index e6507c4a912ccf..ce01773af2044c 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -80,7 +80,13 @@ }, "unpair": { "name": "Unpair an accessory or bridge", - "description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this action if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost." + "description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this action if the accessory is no longer responsive and you want to avoid deleting and re-adding the entry. Room locations and accessory preferences will be lost.", + "fields": { + "device_id": { + "name": "Device", + "description": "Device to unpair." + } + } } } } From 031ae3a9215e5748adbe0aeec2ad115c4562d59c Mon Sep 17 00:00:00 2001 From: Jozef Kruszynski <60214390+jozefKruszynski@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:27:48 +0200 Subject: [PATCH 55/95] Fix sort order in media browser for music assistant integration (#150910) --- .../components/music_assistant/media_browser.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/music_assistant/media_browser.py b/homeassistant/components/music_assistant/media_browser.py index e4724be650af26..23d6ab607e8c81 100644 --- a/homeassistant/components/music_assistant/media_browser.py +++ b/homeassistant/components/music_assistant/media_browser.py @@ -70,7 +70,7 @@ MEDIA_CONTENT_TYPE_FLAC = "audio/flac" THUMB_SIZE = 200 -SORT_NAME_DESC = "sort_name_desc" +SORT_NAME = "sort_name" LOGGER = logging.getLogger(__name__) @@ -173,7 +173,7 @@ async def build_playlists_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for item in await mass.music.get_library_playlists( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if item.available ], @@ -225,7 +225,7 @@ async def build_artists_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for artist in await mass.music.get_library_artists( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if artist.available ], @@ -275,7 +275,7 @@ async def build_albums_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for album in await mass.music.get_library_albums( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if album.available ], @@ -323,7 +323,7 @@ async def build_tracks_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for track in await mass.music.get_library_tracks( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if track.available ], @@ -346,7 +346,7 @@ async def build_podcasts_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for podcast in await mass.music.get_library_podcasts( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if podcast.available ], @@ -369,7 +369,7 @@ async def build_audiobooks_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for audiobook in await mass.music.get_library_audiobooks( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if audiobook.available ], @@ -392,7 +392,7 @@ async def build_radio_listing(mass: MusicAssistantClient) -> BrowseMedia: # we only grab the first page here because the # HA media browser does not support paging for track in await mass.music.get_library_radios( - limit=500, order_by=SORT_NAME_DESC + limit=500, order_by=SORT_NAME ) if track.available ], From 18ce6da4e66a9ec44f86b34554a91f55f439e47a Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:04:06 +0200 Subject: [PATCH 56/95] Allow ignored Onkyo devices to be set up from the user flow (#150921) --- homeassistant/components/onkyo/config_flow.py | 2 +- tests/components/onkyo/test_config_flow.py | 53 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onkyo/config_flow.py b/homeassistant/components/onkyo/config_flow.py index 75b0f92043dd6e..fab2f9b513e445 100644 --- a/homeassistant/components/onkyo/config_flow.py +++ b/homeassistant/components/onkyo/config_flow.py @@ -168,7 +168,7 @@ async def async_step_eiscp_discovery( self._discovered_infos = {} discovered_names = {} - current_unique_ids = self._async_current_ids() + current_unique_ids = self._async_current_ids(include_ignore=False) for info in infos: if info.identifier in current_unique_ids: continue diff --git a/tests/components/onkyo/test_config_flow.py b/tests/components/onkyo/test_config_flow.py index b56ab4b7028427..8ea8febf7c377e 100644 --- a/tests/components/onkyo/test_config_flow.py +++ b/tests/components/onkyo/test_config_flow.py @@ -14,7 +14,7 @@ OPTION_MAX_VOLUME_DEFAULT, OPTION_VOLUME_RESOLUTION, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -240,6 +240,57 @@ async def test_eiscp_discovery_error( assert result["reason"] == error_reason +async def test_eiscp_discovery_replace_ignored_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test eiscp discovery can replace an ignored config entry.""" + mock_config_entry.source = SOURCE_IGNORE + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "eiscp_discovery"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "eiscp_discovery" + + devices = result["data_schema"].schema["device"].container + assert devices == { + RECEIVER_INFO.identifier: _receiver_display_name(RECEIVER_INFO), + RECEIVER_INFO_2.identifier: _receiver_display_name(RECEIVER_INFO_2), + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"device": RECEIVER_INFO.identifier} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "configure_receiver" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + OPTION_VOLUME_RESOLUTION: 200, + OPTION_INPUT_SOURCES: ["TV"], + OPTION_LISTENING_MODES: ["THX"], + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_HOST] == RECEIVER_INFO.host + assert result["result"].unique_id == RECEIVER_INFO.identifier + assert result["title"] == RECEIVER_INFO.model_name + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + @pytest.mark.usefixtures("mock_setup_entry") async def test_ssdp_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry From f32d12c519f4f927ac8299a0b21ff07ffc2d2461 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 28 Aug 2025 14:24:24 +0200 Subject: [PATCH 57/95] Fix wrong description for `numeric_state` observation in `bayesian` (#151291) --- homeassistant/components/bayesian/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json index 2d296d549b82d7..7204c867623c82 100644 --- a/homeassistant/components/bayesian/strings.json +++ b/homeassistant/components/bayesian/strings.json @@ -96,7 +96,7 @@ }, "numeric_state": { "title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]", - "description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]", + "description": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::description%]", "data": { "name": "[%key:common::config_flow::data::name%]", "entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]", From e2a4a9393e8f06fc4e77b85c5ea1a047778fa782 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Mon, 1 Sep 2025 19:28:08 +0200 Subject: [PATCH 58/95] Miele refrigerators cause index out of range errors when offline (#151299) --- homeassistant/components/miele/climate.py | 30 +- .../miele/fixtures/action_offline.json | 15 + .../miele/fixtures/fridge_freezer.json | 77 +++ .../miele/snapshots/test_climate.ambr | 576 ++++++++++++++++++ .../miele/snapshots/test_sensor.ambr | 147 +++++ tests/components/miele/test_climate.py | 16 + 6 files changed, 851 insertions(+), 10 deletions(-) create mode 100644 tests/components/miele/fixtures/action_offline.json diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 24d020823c8372..07637c817b1028 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -8,7 +8,7 @@ from typing import Any, Final, cast import aiohttp -from pymiele import MieleDevice +from pymiele import MieleDevice, MieleTemperature from homeassistant.components.climate import ( ClimateEntity, @@ -31,6 +31,15 @@ _LOGGER = logging.getLogger(__name__) +def _get_temperature_value( + temperatures: list[MieleTemperature], index: int +) -> float | None: + """Return the temperature value for the given index.""" + if len(temperatures) > index: + return cast(int, temperatures[index].temperature) / 100.0 + return None + + @dataclass(frozen=True, kw_only=True) class MieleClimateDescription(ClimateEntityDescription): """Class describing Miele climate entities.""" @@ -62,11 +71,10 @@ class MieleClimateDefinition: description=MieleClimateDescription( key="thermostat", value_fn=( - lambda value: cast(int, value.state_temperatures[0].temperature) / 100.0 + lambda value: _get_temperature_value(value.state_temperatures, 0) ), target_fn=( - lambda value: cast(int, value.state_target_temperature[0].temperature) - / 100.0 + lambda value: _get_temperature_value(value.state_target_temperature, 0) ), zone=1, ), @@ -84,11 +92,10 @@ class MieleClimateDefinition: description=MieleClimateDescription( key="thermostat2", value_fn=( - lambda value: cast(int, value.state_temperatures[1].temperature) / 100.0 + lambda value: _get_temperature_value(value.state_temperatures, 1) ), target_fn=( - lambda value: cast(int, value.state_target_temperature[1].temperature) - / 100.0 + lambda value: _get_temperature_value(value.state_target_temperature, 1) ), translation_key="zone_2", zone=2, @@ -107,11 +114,10 @@ class MieleClimateDefinition: description=MieleClimateDescription( key="thermostat3", value_fn=( - lambda value: cast(int, value.state_temperatures[2].temperature) / 100.0 + lambda value: _get_temperature_value(value.state_temperatures, 2) ), target_fn=( - lambda value: cast(int, value.state_target_temperature[2].temperature) - / 100.0 + lambda value: _get_temperature_value(value.state_target_temperature, 2) ), translation_key="zone_3", zone=3, @@ -219,6 +225,8 @@ def target_temperature(self) -> float | None: @property def max_temp(self) -> float: """Return the maximum target temperature.""" + if len(self.action.target_temperature) < self.entity_description.zone: + return super().max_temp return cast( float, self.action.target_temperature[self.entity_description.zone - 1].max, @@ -227,6 +235,8 @@ def max_temp(self) -> float: @property def min_temp(self) -> float: """Return the minimum target temperature.""" + if len(self.action.target_temperature) < self.entity_description.zone: + return super().min_temp return cast( float, self.action.target_temperature[self.entity_description.zone - 1].min, diff --git a/tests/components/miele/fixtures/action_offline.json b/tests/components/miele/fixtures/action_offline.json new file mode 100644 index 00000000000000..e0eb9e14e87d07 --- /dev/null +++ b/tests/components/miele/fixtures/action_offline.json @@ -0,0 +1,15 @@ +{ + "processAction": [], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [], + "deviceName": false, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [], + "runOnTime": [] +} diff --git a/tests/components/miele/fixtures/fridge_freezer.json b/tests/components/miele/fixtures/fridge_freezer.json index 8ca28befc35804..abda7aeee094d4 100644 --- a/tests/components/miele/fixtures/fridge_freezer.json +++ b/tests/components/miele/fixtures/fridge_freezer.json @@ -110,5 +110,82 @@ "ecoFeedback": null, "batteryLevel": null } + }, + "DummyAppliance_Fridge_Freezer_Offline": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 21, + "value_localized": "Fridge freezer" + }, + "deviceName": "", + "protocolVersion": 203, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "00", + "techType": "KFN 7734 C", + "matNumber": "12336150", + "swids": [] + }, + "xkmIdentLabel": { + "techType": "EK037LHBM", + "releaseVersion": "32.33" + } + }, + "state": { + "ProgramID": { + "value_raw": null, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 255, + "value_localized": "Not connected", + "key_localized": "status" + }, + "programType": { + "value_raw": null, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": null, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [], + "startTime": [], + "targetTemperature": [], + "coreTargetTemperature": [], + "temperature": [], + "coreTemperature": [], + "remoteEnable": { + "fullRemoteControl": false, + "smartGrid": false, + "mobileStart": null + }, + "ambientLight": null, + "light": null, + "elapsedTime": [], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 3b8b7488d9b37a..1349cf9b2ade47 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -319,6 +319,70 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer_2', + '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': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': -14, + 'min_temp': -28, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- # name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -383,6 +447,70 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator_2', + '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': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- # name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -447,3 +575,451 @@ 'state': 'cool', }) # --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3_2', + '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': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_mulizone[fridge_freezer-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': -15, + 'min_temp': -30, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer', + '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': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -18, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -18, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_freezer_2', + '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': 'Freezer', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat2-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_freezer_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_freezer_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator', + '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': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 4, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 4, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_refrigerator_2', + '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': 'Refrigerator', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_refrigerator_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_refrigerator_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3', + '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': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -28, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -25, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.fridge_freezer_zone_3_2', + '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': 'Zone 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'zone_3', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-thermostat3-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_offline[fridge_freezer_offline-fridge_freezer.json-platforms0][climate.fridge_freezer_zone_3_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Fridge freezer Zone 3', + 'hvac_modes': list([ + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.fridge_freezer_zone_3_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index f385a53b6e4720..17941a586d1f16 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1595,6 +1595,97 @@ 'state': 'in_use', }) # --- +# name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.fridge_freezer_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:fridge-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Fridge freezer', + 'icon': 'mdi:fridge-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.fridge_freezer_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_connected', + }) +# --- # name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1651,6 +1742,62 @@ 'state': '4.0', }) # --- +# name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_temperature_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.fridge_freezer_temperature_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DummyAppliance_Fridge_Freezer_Offline-state_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_temperature_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Fridge freezer Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.fridge_freezer_temperature_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_fridge_freezer_sensor_states[platforms0-fridge_freezer.json][sensor.fridge_freezer_temperature_zone_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index 392a67127075f4..6cbae344a41c56 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -34,6 +34,22 @@ async def test_climate_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("load_device_file", ["fridge_freezer.json"]) +@pytest.mark.parametrize( + "load_action_file", ["action_offline.json"], ids=["fridge_freezer_offline"] +) +async def test_climate_states_offline( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test climate entity state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.parametrize("load_device_file", ["fridge_freezer.json"]) @pytest.mark.parametrize( "load_action_file", ["action_fridge_freezer.json"], ids=["fridge_freezer"] From 9581c705b9b733b6861ded01feb16617b907ae14 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Mon, 1 Sep 2025 16:47:59 +0200 Subject: [PATCH 59/95] Fix add checks for None values and check if DHW is available (#151376) --- homeassistant/components/bsblan/climate.py | 4 ++ .../components/bsblan/config_flow.py | 2 +- homeassistant/components/bsblan/sensor.py | 26 ++++++++- .../components/bsblan/water_heater.py | 24 +++++++- tests/components/bsblan/test_climate.py | 44 +++++++++++++++ tests/components/bsblan/test_sensor.py | 42 ++++++++++++++ tests/components/bsblan/test_water_heater.py | 55 +++++++++++++++++++ 7 files changed, 191 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index bef0388a57de54..5d181c074444f1 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -81,11 +81,15 @@ def __init__( @property def current_temperature(self) -> float | None: """Return the current temperature.""" + if self.coordinator.data.state.current_temperature is None: + return None return self.coordinator.data.state.current_temperature.value @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" + if self.coordinator.data.state.target_temperature is None: + return None return self.coordinator.data.state.target_temperature.value @property diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 5f4f67a114af11..72e053ad140202 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -25,7 +25,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize BSBLan flow.""" - self.host: str | None = None + self.host: str = "" self.port: int = DEFAULT_PORT self.mac: str | None = None self.passkey: str | None = None diff --git a/homeassistant/components/bsblan/sensor.py b/homeassistant/components/bsblan/sensor.py index 7f3f7f48afcb76..f28c7a2decf78e 100644 --- a/homeassistant/components/bsblan/sensor.py +++ b/homeassistant/components/bsblan/sensor.py @@ -28,6 +28,7 @@ class BSBLanSensorEntityDescription(SensorEntityDescription): """Describes BSB-Lan sensor entity.""" value_fn: Callable[[BSBLanCoordinatorData], StateType] + exists_fn: Callable[[BSBLanCoordinatorData], bool] = lambda data: True SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = ( @@ -37,7 +38,12 @@ class BSBLanSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.sensor.current_temperature.value, + value_fn=lambda data: ( + data.sensor.current_temperature.value + if data.sensor.current_temperature is not None + else None + ), + exists_fn=lambda data: data.sensor.current_temperature is not None, ), BSBLanSensorEntityDescription( key="outside_temperature", @@ -45,7 +51,12 @@ class BSBLanSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.sensor.outside_temperature.value, + value_fn=lambda data: ( + data.sensor.outside_temperature.value + if data.sensor.outside_temperature is not None + else None + ), + exists_fn=lambda data: data.sensor.outside_temperature is not None, ), ) @@ -57,7 +68,16 @@ async def async_setup_entry( ) -> None: """Set up BSB-Lan sensor based on a config entry.""" data = entry.runtime_data - async_add_entities(BSBLanSensor(data, description) for description in SENSOR_TYPES) + + # Only create sensors for available data points + entities = [ + BSBLanSensor(data, description) + for description in SENSOR_TYPES + if description.exists_fn(data.coordinator.data) + ] + + if entities: + async_add_entities(entities) class BSBLanSensor(BSBLanEntity, SensorEntity): diff --git a/homeassistant/components/bsblan/water_heater.py b/homeassistant/components/bsblan/water_heater.py index a3aee4cdc15a6f..248d7def8493c3 100644 --- a/homeassistant/components/bsblan/water_heater.py +++ b/homeassistant/components/bsblan/water_heater.py @@ -41,6 +41,18 @@ async def async_setup_entry( ) -> None: """Set up BSBLAN water heater based on a config entry.""" data = entry.runtime_data + + # Only create water heater entity if DHW (Domestic Hot Water) is available + # Check if we have any DHW-related data indicating water heater support + dhw_data = data.coordinator.data.dhw + if ( + dhw_data.operating_mode is None + and dhw_data.nominal_setpoint is None + and dhw_data.dhw_actual_value_top_temperature is None + ): + # No DHW functionality available, skip water heater setup + return + async_add_entities([BSBLANWaterHeater(data)]) @@ -61,23 +73,31 @@ def __init__(self, data: BSBLanData) -> None: # Set temperature limits based on device capabilities self._attr_temperature_unit = data.coordinator.client.get_temperature_unit - self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value - self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value + if data.coordinator.data.dhw.reduced_setpoint is not None: + self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value + if data.coordinator.data.dhw.nominal_setpoint_max is not None: + self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value @property def current_operation(self) -> str | None: """Return current operation.""" + if self.coordinator.data.dhw.operating_mode is None: + return None current_mode = self.coordinator.data.dhw.operating_mode.desc return OPERATION_MODES.get(current_mode) @property def current_temperature(self) -> float | None: """Return the current temperature.""" + if self.coordinator.data.dhw.dhw_actual_value_top_temperature is None: + return None return self.coordinator.data.dhw.dhw_actual_value_top_temperature.value @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" + if self.coordinator.data.dhw.nominal_setpoint is None: + return None return self.coordinator.data.dhw.nominal_setpoint.value async def async_set_temperature(self, **kwargs: Any) -> None: diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 41d566fc375bb8..f35f0c7bdf3397 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -91,6 +91,50 @@ async def test_climate_entity_properties( assert state.attributes["preset_mode"] == PRESET_ECO +async def test_climate_without_current_temperature_sensor( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test climate entity when current temperature sensor is not available.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + # Set current_temperature to None to simulate no temperature sensor + mock_bsblan.state.return_value.current_temperature = None + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Should not crash and current_temperature should be None in attributes + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.attributes["current_temperature"] is None + + +async def test_climate_without_target_temperature_sensor( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test climate entity when target temperature sensor is not available.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + # Set target_temperature to None to simulate no temperature sensor + mock_bsblan.state.return_value.target_temperature = None + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Should not crash and target temperature should be None in attributes + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.attributes["temperature"] is None + + @pytest.mark.parametrize( "mode", [HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF], diff --git a/tests/components/bsblan/test_sensor.py b/tests/components/bsblan/test_sensor.py index ba2af40f319111..fdfe8fec06b6ca 100644 --- a/tests/components/bsblan/test_sensor.py +++ b/tests/components/bsblan/test_sensor.py @@ -28,3 +28,45 @@ async def test_sensor_entity_properties( """Test the sensor entity properties.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_sensors_not_created_when_data_unavailable( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensors are not created when sensor data is not available.""" + # Set all sensor data to None to simulate no sensors available + mock_bsblan.sensor.return_value.current_temperature = None + mock_bsblan.sensor.return_value.outside_temperature = None + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + # Should not create any sensor entities + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + sensor_entities = [entry for entry in entity_entries if entry.domain == "sensor"] + assert len(sensor_entities) == 0 + + +async def test_partial_sensors_created_when_some_data_available( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test only available sensors are created when some sensor data is available.""" + # Only current temperature available, outside temperature not + mock_bsblan.sensor.return_value.outside_temperature = None + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + # Should create only the current temperature sensor + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + sensor_entities = [entry for entry in entity_entries if entry.domain == "sensor"] + assert len(sensor_entities) == 1 + assert sensor_entities[0].entity_id == ENTITY_CURRENT_TEMP diff --git a/tests/components/bsblan/test_water_heater.py b/tests/components/bsblan/test_water_heater.py index 173498b14ff5c2..466da1e6fda414 100644 --- a/tests/components/bsblan/test_water_heater.py +++ b/tests/components/bsblan/test_water_heater.py @@ -50,6 +50,33 @@ async def test_water_heater_states( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +async def test_water_heater_no_dhw_capability( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that no water heater entity is created when DHW capability is missing.""" + # Mock DHW data to simulate no water heater capability + mock_bsblan.hot_water_state.return_value.operating_mode = None + mock_bsblan.hot_water_state.return_value.nominal_setpoint = None + mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None + + await setup_with_selected_platforms( + hass, mock_config_entry, [Platform.WATER_HEATER] + ) + + # Verify no water heater entity was created + entities = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + water_heater_entities = [ + entity for entity in entities if entity.domain == Platform.WATER_HEATER + ] + + assert len(water_heater_entities) == 0 + + async def test_water_heater_entity_properties( hass: HomeAssistant, mock_bsblan: AsyncMock, @@ -208,3 +235,31 @@ async def test_operation_mode_error( }, blocking=True, ) + + +async def test_water_heater_no_sensors( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test water heater when sensors are not available.""" + await setup_with_selected_platforms( + hass, mock_config_entry, [Platform.WATER_HEATER] + ) + + # Set all sensors to None to simulate missing sensors + mock_bsblan.hot_water_state.return_value.operating_mode = None + mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None + mock_bsblan.hot_water_state.return_value.nominal_setpoint = None + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Should not crash and properties should return None + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.attributes.get("current_operation") is None + assert state.attributes.get("current_temperature") is None + assert state.attributes.get("temperature") is None From d9629affca9a57f0dcc514f06f4e155f53d608f8 Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:09:19 +0200 Subject: [PATCH 60/95] Bump pyiskra to 0.1.26 (#151489) --- homeassistant/components/iskra/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index da983db99697ef..e378a1442d2e18 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.21"] + "requirements": ["pyiskra==0.1.26"] } diff --git a/requirements_all.txt b/requirements_all.txt index b033c7eb053aba..64e51835c0aa54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2057,7 +2057,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.21 +pyiskra==0.1.26 # homeassistant.components.iss pyiss==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 992c5aaee1d204..dabe084e2fb0fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1711,7 +1711,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.21 +pyiskra==0.1.26 # homeassistant.components.iss pyiss==1.0.1 diff --git a/script/licenses.py b/script/licenses.py index ef62d4970dd1ca..f33fb176860b86 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -212,7 +212,6 @@ def from_dict(cls, data: PackageMetadata) -> PackageDefinition: "0.12.3" ), # https://github.com/aio-libs/aiocache/blob/master/LICENSE all rights reserved? "caldav": AwesomeVersion("1.6.0"), # None -- GPL -- ['GNU General Public License (GPL)', 'Apache Software License'] # https://github.com/python-caldav/caldav - "pyiskra": AwesomeVersion("0.1.21"), # None -- GPL -- ['GNU General Public License v3 (GPLv3)'] "xbox-webapi": AwesomeVersion("2.1.0"), # None -- GPL -- ['MIT License'] } # fmt: on From 1a2898cc899c340e74cd30dd946fb717b079d2d5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Sep 2025 15:15:21 +0200 Subject: [PATCH 61/95] Update Pooldose quality scale (#151499) --- .../components/pooldose/manifest.json | 2 +- .../components/pooldose/quality_scale.yaml | 40 ++++++------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/pooldose/manifest.json b/homeassistant/components/pooldose/manifest.json index 597a3fef553477..8bcbb18737cd07 100644 --- a/homeassistant/components/pooldose/manifest.json +++ b/homeassistant/components/pooldose/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pooldose", "iot_class": "local_polling", - "quality_scale": "gold", + "quality_scale": "bronze", "requirements": ["python-pooldose==0.5.0"] } diff --git a/homeassistant/components/pooldose/quality_scale.yaml b/homeassistant/components/pooldose/quality_scale.yaml index e9b790c74ad90d..dc3c2221d73cf9 100644 --- a/homeassistant/components/pooldose/quality_scale.yaml +++ b/homeassistant/components/pooldose/quality_scale.yaml @@ -17,7 +17,7 @@ rules: docs-removal-instructions: done entity-event-setup: status: exempt - comment: This integration does not subscribe to any events. + comment: This integration does not explicitly subscribe to any events. entity-unique-id: done has-entity-name: done runtime-data: done @@ -35,9 +35,7 @@ rules: entity-unavailable: done integration-owner: done log-when-unavailable: done - parallel-updates: - status: exempt - comment: This integration uses a central coordinator to manage updates, which is not compatible with parallel updates. + parallel-updates: todo reauthentication-flow: status: exempt comment: This integration does not need authentication for the local API. @@ -45,28 +43,20 @@ rules: # Gold devices: done - diagnostics: - status: exempt - comment: This integration does not provide any diagnostic information, but can provide detailed logs if needed. + diagnostics: todo discovery-update-info: - status: exempt - comment: This integration does not support discovery features. + status: todo + comment: DHCP discovery is possible discovery: - status: exempt - comment: This integration does not support discovery updates since the PoolDose device does not support standard discovery methods. + status: todo + comment: DHCP discovery is possible docs-data-update: done - docs-examples: - status: exempt - comment: This integration does not provide any examples, as it is a simple integration that does not require complex configurations. - docs-known-limitations: - status: exempt - comment: This integration has known and documented limitations in frequency of data polling and stability of the connection to the device. + docs-examples: todo + docs-known-limitations: todo docs-supported-devices: done docs-supported-functions: done docs-troubleshooting: done - docs-use-cases: - status: exempt - comment: This integration does not provide use cases, as it is a simple integration that does not require complex configurations. + docs-use-cases: todo dynamic-devices: status: exempt comment: This integration does not support dynamic devices, as it is designed for a single PoolDose device. @@ -76,9 +66,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: - status: exempt - comment: This integration does not support reconfiguration flows, as it is designed for a single PoolDose device with a fixed configuration. + reconfiguration-flow: todo repair-issues: status: exempt comment: This integration does not provide repair issues, as it is designed for a single PoolDose device with a fixed configuration. @@ -88,7 +76,5 @@ rules: # Platinum async-dependency: done - inject-websession: done - strict-typing: - status: exempt - comment: Dependency python-pooldose is not strictly typed and does not include a py.typed file. + inject-websession: todo + strict-typing: todo From 399286deaed5ebc4b28588b328c44acf8a5caafb Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Tue, 2 Sep 2025 01:10:07 +0200 Subject: [PATCH 62/95] Remove the vulcan integration (#151504) --- CODEOWNERS | 2 - homeassistant/components/vulcan/__init__.py | 48 - homeassistant/components/vulcan/calendar.py | 176 ---- .../components/vulcan/config_flow.py | 327 ------- homeassistant/components/vulcan/const.py | 3 - homeassistant/components/vulcan/fetch_data.py | 98 -- homeassistant/components/vulcan/manifest.json | 9 - homeassistant/components/vulcan/register.py | 12 - homeassistant/components/vulcan/strings.json | 62 -- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/hassfest/quality_scale.py | 2 - .../fixtures/current_data.json | 1 - tests/components/vulcan/__init__.py | 1 - .../fixtures/fake_config_entry_data.json | 16 - .../vulcan/fixtures/fake_student_1.json | 35 - .../vulcan/fixtures/fake_student_2.json | 35 - tests/components/vulcan/test_config_flow.py | 917 ------------------ 20 files changed, 1757 deletions(-) delete mode 100644 homeassistant/components/vulcan/__init__.py delete mode 100644 homeassistant/components/vulcan/calendar.py delete mode 100644 homeassistant/components/vulcan/config_flow.py delete mode 100644 homeassistant/components/vulcan/const.py delete mode 100644 homeassistant/components/vulcan/fetch_data.py delete mode 100644 homeassistant/components/vulcan/manifest.json delete mode 100644 homeassistant/components/vulcan/register.py delete mode 100644 homeassistant/components/vulcan/strings.json delete mode 100644 tests/components/vulcan/__init__.py delete mode 100644 tests/components/vulcan/fixtures/fake_config_entry_data.json delete mode 100644 tests/components/vulcan/fixtures/fake_student_1.json delete mode 100644 tests/components/vulcan/fixtures/fake_student_2.json delete mode 100644 tests/components/vulcan/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 1e1ee83837da09..d1f06d04b41d49 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1718,8 +1718,6 @@ build.json @home-assistant/supervisor /tests/components/volvo/ @thomasddn /homeassistant/components/volvooncall/ @molobrakos /tests/components/volvooncall/ @molobrakos -/homeassistant/components/vulcan/ @Antoni-Czaplicki -/tests/components/vulcan/ @Antoni-Czaplicki /homeassistant/components/wake_on_lan/ @ntilley905 /tests/components/wake_on_lan/ @ntilley905 /homeassistant/components/wake_word/ @home-assistant/core @synesthesiam diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py deleted file mode 100644 index 0bfd09d590d634..00000000000000 --- a/homeassistant/components/vulcan/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -"""The Vulcan component.""" - -from aiohttp import ClientConnectorError -from vulcan import Account, Keystore, UnauthorizedCertificateException, Vulcan - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import DOMAIN - -PLATFORMS = [Platform.CALENDAR] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Uonet+ Vulcan integration.""" - hass.data.setdefault(DOMAIN, {}) - try: - keystore = Keystore.load(entry.data["keystore"]) - account = Account.load(entry.data["account"]) - client = Vulcan(keystore, account, async_get_clientsession(hass)) - await client.select_student() - students = await client.get_students() - for student in students: - if str(student.pupil.id) == str(entry.data["student_id"]): - client.student = student - break - except UnauthorizedCertificateException as err: - raise ConfigEntryAuthFailed("The certificate is not authorized.") from err - except ClientConnectorError as err: - raise ConfigEntryNotReady( - f"Connection error - please check your internet connection: {err}" - ) from err - hass.data[DOMAIN][entry.entry_id] = client - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py deleted file mode 100644 index c2ef8b70d46166..00000000000000 --- a/homeassistant/components/vulcan/calendar.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Support for Vulcan Calendar platform.""" - -from __future__ import annotations - -from datetime import date, datetime, timedelta -import logging -from typing import cast -from zoneinfo import ZoneInfo - -from aiohttp import ClientConnectorError -from vulcan import UnauthorizedCertificateException - -from homeassistant.components.calendar import ( - ENTITY_ID_FORMAT, - CalendarEntity, - CalendarEvent, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from . import DOMAIN -from .fetch_data import get_lessons, get_student_info - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up the calendar platform for entity.""" - client = hass.data[DOMAIN][config_entry.entry_id] - data = { - "student_info": await get_student_info( - client, config_entry.data.get("student_id") - ), - } - async_add_entities( - [ - VulcanCalendarEntity( - client, - data, - generate_entity_id( - ENTITY_ID_FORMAT, - f"vulcan_calendar_{data['student_info']['full_name']}", - hass=hass, - ), - ) - ], - ) - - -class VulcanCalendarEntity(CalendarEntity): - """A calendar entity.""" - - _attr_has_entity_name = True - _attr_translation_key = "calendar" - - def __init__(self, client, data, entity_id) -> None: - """Create the Calendar entity.""" - self._event: CalendarEvent | None = None - self.client = client - self.entity_id = entity_id - student_info = data["student_info"] - self._attr_unique_id = f"vulcan_calendar_{student_info['id']}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"calendar_{student_info['id']}")}, - entry_type=DeviceEntryType.SERVICE, - name=cast(str, student_info["full_name"]), - model=( - f"{student_info['full_name']} -" - f" {student_info['class']} {student_info['school']}" - ), - manufacturer="Uonet +", - configuration_url=( - f"https://uonetplus.vulcan.net.pl/{student_info['symbol']}" - ), - ) - - @property - def event(self) -> CalendarEvent | None: - """Return the next upcoming event.""" - return self._event - - async def async_get_events( - self, hass: HomeAssistant, start_date: datetime, end_date: datetime - ) -> list[CalendarEvent]: - """Get all events in a specific time frame.""" - try: - events = await get_lessons( - self.client, - date_from=start_date, - date_to=end_date, - ) - except UnauthorizedCertificateException as err: - raise ConfigEntryAuthFailed( - "The certificate is not authorized, please authorize integration again" - ) from err - except ClientConnectorError as err: - if self.available: - _LOGGER.warning( - "Connection error - please check your internet connection: %s", err - ) - events = [] - - event_list = [] - for item in events: - event = CalendarEvent( - start=datetime.combine( - item["date"], item["time"].from_, ZoneInfo("Europe/Warsaw") - ), - end=datetime.combine( - item["date"], item["time"].to, ZoneInfo("Europe/Warsaw") - ), - summary=item["lesson"], - location=item["room"], - description=item["teacher"], - ) - - event_list.append(event) - - return event_list - - async def async_update(self) -> None: - """Get the latest data.""" - - try: - events = await get_lessons(self.client) - - if not self.available: - _LOGGER.warning("Restored connection with API") - self._attr_available = True - - if events == []: - events = await get_lessons( - self.client, - date_to=date.today() + timedelta(days=7), - ) - if events == []: - self._event = None - return - except UnauthorizedCertificateException as err: - raise ConfigEntryAuthFailed( - "The certificate is not authorized, please authorize integration again" - ) from err - except ClientConnectorError as err: - if self.available: - _LOGGER.warning( - "Connection error - please check your internet connection: %s", err - ) - self._attr_available = False - return - - new_event = min( - events, - key=lambda d: ( - datetime.combine(d["date"], d["time"].to) < datetime.now(), - abs(datetime.combine(d["date"], d["time"].to) - datetime.now()), - ), - ) - self._event = CalendarEvent( - start=datetime.combine( - new_event["date"], new_event["time"].from_, ZoneInfo("Europe/Warsaw") - ), - end=datetime.combine( - new_event["date"], new_event["time"].to, ZoneInfo("Europe/Warsaw") - ), - summary=new_event["lesson"], - location=new_event["room"], - description=new_event["teacher"], - ) diff --git a/homeassistant/components/vulcan/config_flow.py b/homeassistant/components/vulcan/config_flow.py deleted file mode 100644 index f02adba9f7522e..00000000000000 --- a/homeassistant/components/vulcan/config_flow.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Adds config flow for Vulcan.""" - -from collections.abc import Mapping -import logging -from typing import TYPE_CHECKING, Any - -from aiohttp import ClientConnectionError -import voluptuous as vol -from vulcan import ( - Account, - ExpiredTokenException, - InvalidPINException, - InvalidSymbolException, - InvalidTokenException, - Keystore, - UnauthorizedCertificateException, - Vulcan, -) -from vulcan.model import Student - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from . import DOMAIN -from .register import register - -_LOGGER = logging.getLogger(__name__) - -LOGIN_SCHEMA = { - vol.Required(CONF_TOKEN): str, - vol.Required(CONF_REGION): str, - vol.Required(CONF_PIN): str, -} - - -class VulcanFlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a Uonet+ Vulcan config flow.""" - - VERSION = 1 - - account: Account - keystore: Keystore - - def __init__(self) -> None: - """Initialize config flow.""" - self.students: list[Student] | None = None - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle config flow.""" - if self._async_current_entries(): - return await self.async_step_add_next_config_entry() - - return await self.async_step_auth() - - async def async_step_auth( - self, - user_input: dict[str, str] | None = None, - errors: dict[str, str] | None = None, - ) -> ConfigFlowResult: - """Authorize integration.""" - - if user_input is not None: - try: - credentials = await register( - user_input[CONF_TOKEN], - user_input[CONF_REGION], - user_input[CONF_PIN], - ) - except InvalidSymbolException: - errors = {"base": "invalid_symbol"} - except InvalidTokenException: - errors = {"base": "invalid_token"} - except InvalidPINException: - errors = {"base": "invalid_pin"} - except ExpiredTokenException: - errors = {"base": "expired_token"} - except ClientConnectionError as err: - errors = {"base": "cannot_connect"} - _LOGGER.error("Connection error: %s", err) - except Exception: - _LOGGER.exception("Unexpected exception") - errors = {"base": "unknown"} - if not errors: - account = credentials["account"] - keystore = credentials["keystore"] - client = Vulcan(keystore, account, async_get_clientsession(self.hass)) - students = await client.get_students() - - if len(students) > 1: - self.account = account - self.keystore = keystore - self.students = students - return await self.async_step_select_student() - student = students[0] - await self.async_set_unique_id(str(student.pupil.id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"{student.pupil.first_name} {student.pupil.last_name}", - data={ - "student_id": str(student.pupil.id), - "keystore": keystore.as_dict, - "account": account.as_dict, - }, - ) - - return self.async_show_form( - step_id="auth", - data_schema=vol.Schema(LOGIN_SCHEMA), - errors=errors, - ) - - async def async_step_select_student( - self, user_input: dict[str, str] | None = None - ) -> ConfigFlowResult: - """Allow user to select student.""" - errors: dict[str, str] = {} - students: dict[str, str] = {} - if self.students is not None: - for student in self.students: - students[str(student.pupil.id)] = ( - f"{student.pupil.first_name} {student.pupil.last_name}" - ) - if user_input is not None: - if TYPE_CHECKING: - assert self.keystore is not None - student_id = user_input["student"] - await self.async_set_unique_id(str(student_id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=students[student_id], - data={ - "student_id": str(student_id), - "keystore": self.keystore.as_dict, - "account": self.account.as_dict, - }, - ) - - return self.async_show_form( - step_id="select_student", - data_schema=vol.Schema({vol.Required("student"): vol.In(students)}), - errors=errors, - ) - - async def async_step_select_saved_credentials( - self, - user_input: dict[str, str] | None = None, - errors: dict[str, str] | None = None, - ) -> ConfigFlowResult: - """Allow user to select saved credentials.""" - - credentials: dict[str, Any] = {} - for entry in self.hass.config_entries.async_entries(DOMAIN): - credentials[entry.entry_id] = entry.data["account"]["UserName"] - - if user_input is not None: - existing_entry = self.hass.config_entries.async_get_entry( - user_input["credentials"] - ) - if TYPE_CHECKING: - assert existing_entry is not None - keystore = Keystore.load(existing_entry.data["keystore"]) - account = Account.load(existing_entry.data["account"]) - client = Vulcan(keystore, account, async_get_clientsession(self.hass)) - try: - students = await client.get_students() - except UnauthorizedCertificateException: - return await self.async_step_auth( - errors={"base": "expired_credentials"} - ) - except ClientConnectionError as err: - _LOGGER.error("Connection error: %s", err) - return await self.async_step_select_saved_credentials( - errors={"base": "cannot_connect"} - ) - except Exception: - _LOGGER.exception("Unexpected exception") - return await self.async_step_auth(errors={"base": "unknown"}) - if len(students) == 1: - student = students[0] - await self.async_set_unique_id(str(student.pupil.id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"{student.pupil.first_name} {student.pupil.last_name}", - data={ - "student_id": str(student.pupil.id), - "keystore": keystore.as_dict, - "account": account.as_dict, - }, - ) - self.account = account - self.keystore = keystore - self.students = students - return await self.async_step_select_student() - - data_schema = { - vol.Required( - "credentials", - ): vol.In(credentials), - } - return self.async_show_form( - step_id="select_saved_credentials", - data_schema=vol.Schema(data_schema), - errors=errors, - ) - - async def async_step_add_next_config_entry( - self, user_input: dict[str, bool] | None = None - ) -> ConfigFlowResult: - """Flow initialized when user is adding next entry of that integration.""" - - existing_entries = self.hass.config_entries.async_entries(DOMAIN) - - errors: dict[str, str] = {} - - if user_input is not None: - if not user_input["use_saved_credentials"]: - return await self.async_step_auth() - if len(existing_entries) > 1: - return await self.async_step_select_saved_credentials() - keystore = Keystore.load(existing_entries[0].data["keystore"]) - account = Account.load(existing_entries[0].data["account"]) - client = Vulcan(keystore, account, async_get_clientsession(self.hass)) - students = await client.get_students() - existing_entry_ids = [ - entry.data["student_id"] for entry in existing_entries - ] - new_students = [ - student - for student in students - if str(student.pupil.id) not in existing_entry_ids - ] - if not new_students: - return self.async_abort(reason="all_student_already_configured") - if len(new_students) == 1: - await self.async_set_unique_id(str(new_students[0].pupil.id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=( - f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}" - ), - data={ - "student_id": str(new_students[0].pupil.id), - "keystore": keystore.as_dict, - "account": account.as_dict, - }, - ) - self.account = account - self.keystore = keystore - self.students = new_students - return await self.async_step_select_student() - - data_schema = { - vol.Required("use_saved_credentials", default=True): bool, - } - return self.async_show_form( - step_id="add_next_config_entry", - data_schema=vol.Schema(data_schema), - errors=errors, - ) - - async def async_step_reauth( - self, entry_data: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon an API authentication error.""" - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: dict[str, str] | None = None - ) -> ConfigFlowResult: - """Reauthorize integration.""" - errors = {} - if user_input is not None: - try: - credentials = await register( - user_input[CONF_TOKEN], - user_input[CONF_REGION], - user_input[CONF_PIN], - ) - except InvalidSymbolException: - errors = {"base": "invalid_symbol"} - except InvalidTokenException: - errors = {"base": "invalid_token"} - except InvalidPINException: - errors = {"base": "invalid_pin"} - except ExpiredTokenException: - errors = {"base": "expired_token"} - except ClientConnectionError as err: - errors["base"] = "cannot_connect" - _LOGGER.error("Connection error: %s", err) - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - if not errors: - account = credentials["account"] - keystore = credentials["keystore"] - client = Vulcan(keystore, account, async_get_clientsession(self.hass)) - students = await client.get_students() - existing_entries = self.hass.config_entries.async_entries(DOMAIN) - matching_entries = False - for student in students: - for entry in existing_entries: - if str(student.pupil.id) == str(entry.data["student_id"]): - self.hass.config_entries.async_update_entry( - entry, - title=( - f"{student.pupil.first_name} {student.pupil.last_name}" - ), - data={ - "student_id": str(student.pupil.id), - "keystore": keystore.as_dict, - "account": account.as_dict, - }, - ) - await self.hass.config_entries.async_reload(entry.entry_id) - matching_entries = True - if not matching_entries: - return self.async_abort(reason="no_matching_entries") - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema(LOGIN_SCHEMA), - errors=errors, - ) diff --git a/homeassistant/components/vulcan/const.py b/homeassistant/components/vulcan/const.py deleted file mode 100644 index 4f17d43c3427ac..00000000000000 --- a/homeassistant/components/vulcan/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Vulcan integration.""" - -DOMAIN = "vulcan" diff --git a/homeassistant/components/vulcan/fetch_data.py b/homeassistant/components/vulcan/fetch_data.py deleted file mode 100644 index cd82346d5b7380..00000000000000 --- a/homeassistant/components/vulcan/fetch_data.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Support for fetching Vulcan data.""" - - -async def get_lessons(client, date_from=None, date_to=None): - """Support for fetching Vulcan lessons.""" - changes = {} - list_ans = [] - async for lesson in await client.data.get_changed_lessons( - date_from=date_from, date_to=date_to - ): - temp_dict = {} - _id = str(lesson.id) - temp_dict["id"] = lesson.id - temp_dict["number"] = lesson.time.position if lesson.time is not None else None - temp_dict["lesson"] = ( - lesson.subject.name if lesson.subject is not None else None - ) - temp_dict["room"] = lesson.room.code if lesson.room is not None else None - temp_dict["changes"] = lesson.changes - temp_dict["note"] = lesson.note - temp_dict["reason"] = lesson.reason - temp_dict["event"] = lesson.event - temp_dict["group"] = lesson.group - temp_dict["teacher"] = ( - lesson.teacher.display_name if lesson.teacher is not None else None - ) - temp_dict["from_to"] = ( - lesson.time.displayed_time if lesson.time is not None else None - ) - - changes[str(_id)] = temp_dict - - async for lesson in await client.data.get_lessons( - date_from=date_from, date_to=date_to - ): - temp_dict = {} - temp_dict["id"] = lesson.id - temp_dict["number"] = lesson.time.position - temp_dict["time"] = lesson.time - temp_dict["date"] = lesson.date.date - temp_dict["lesson"] = ( - lesson.subject.name if lesson.subject is not None else None - ) - if lesson.room is not None: - temp_dict["room"] = lesson.room.code - else: - temp_dict["room"] = "-" - temp_dict["visible"] = lesson.visible - temp_dict["changes"] = lesson.changes - temp_dict["group"] = lesson.group - temp_dict["reason"] = None - temp_dict["teacher"] = ( - lesson.teacher.display_name if lesson.teacher is not None else None - ) - temp_dict["from_to"] = ( - lesson.time.displayed_time if lesson.time is not None else None - ) - if temp_dict["changes"] is None: - temp_dict["changes"] = "" - elif temp_dict["changes"].type == 1: - temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})" - temp_dict["changes_info"] = f"Lekcja odwołana ({temp_dict['lesson']})" - if str(temp_dict["changes"].id) in changes: - temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] - elif temp_dict["changes"].type == 2: - temp_dict["lesson"] = f"{temp_dict['lesson']} (Zastępstwo)" - temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"] - if str(temp_dict["changes"].id) in changes: - temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"] - temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] - elif temp_dict["changes"].type == 4: - temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})" - if str(temp_dict["changes"].id) in changes: - temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] - if temp_dict["visible"]: - list_ans.append(temp_dict) - - return list_ans - - -async def get_student_info(client, student_id): - """Support for fetching Student info by student id.""" - student_info = {} - for student in await client.get_students(): - if str(student.pupil.id) == str(student_id): - student_info["first_name"] = student.pupil.first_name - if student.pupil.second_name: - student_info["second_name"] = student.pupil.second_name - student_info["last_name"] = student.pupil.last_name - student_info["full_name"] = ( - f"{student.pupil.first_name} {student.pupil.last_name}" - ) - student_info["id"] = student.pupil.id - student_info["class"] = student.class_ - student_info["school"] = student.school.name - student_info["symbol"] = student.symbol - break - return student_info diff --git a/homeassistant/components/vulcan/manifest.json b/homeassistant/components/vulcan/manifest.json deleted file mode 100644 index f9385262f05b62..00000000000000 --- a/homeassistant/components/vulcan/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "vulcan", - "name": "Uonet+ Vulcan", - "codeowners": ["@Antoni-Czaplicki"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/vulcan", - "iot_class": "cloud_polling", - "requirements": ["vulcan-api==2.4.2"] -} diff --git a/homeassistant/components/vulcan/register.py b/homeassistant/components/vulcan/register.py deleted file mode 100644 index a3dec97f622b82..00000000000000 --- a/homeassistant/components/vulcan/register.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Support for register Vulcan account.""" - -from typing import Any - -from vulcan import Account, Keystore - - -async def register(token: str, symbol: str, pin: str) -> dict[str, Any]: - """Register integration and save credentials.""" - keystore = await Keystore.create(device_model="Home Assistant") - account = await Account.register(keystore, token, symbol, pin) - return {"account": account, "keystore": keystore} diff --git a/homeassistant/components/vulcan/strings.json b/homeassistant/components/vulcan/strings.json deleted file mode 100644 index d8344cbdeec378..00000000000000 --- a/homeassistant/components/vulcan/strings.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "That student has already been added.", - "all_student_already_configured": "All students have already been added.", - "reauth_successful": "Reauth successful", - "no_matching_entries": "No matching entries found, please use different account or remove outdated student integration." - }, - "error": { - "unknown": "[%key:common::config_flow::error::unknown%]", - "invalid_token": "[%key:common::config_flow::error::invalid_access_token%]", - "expired_token": "Expired token - please generate a new token", - "invalid_pin": "Invalid PIN", - "invalid_symbol": "Invalid symbol", - "expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "auth": { - "description": "Log in to your Vulcan Account using mobile app registration page.", - "data": { - "token": "Token", - "region": "Symbol", - "pin": "[%key:common::config_flow::data::pin%]" - } - }, - "reauth_confirm": { - "description": "[%key:component::vulcan::config::step::auth::description%]", - "data": { - "token": "Token", - "region": "[%key:component::vulcan::config::step::auth::data::region%]", - "pin": "[%key:common::config_flow::data::pin%]" - } - }, - "select_student": { - "description": "Select student, you can add more students by adding integration again.", - "data": { - "student_name": "Select student" - } - }, - "select_saved_credentials": { - "description": "Select saved credentials.", - "data": { - "credentials": "Login" - } - }, - "add_next_config_entry": { - "description": "Add another student.", - "data": { - "use_saved_credentials": "Use saved credentials" - } - } - } - }, - "entity": { - "calendar": { - "calendar": { - "name": "[%key:component::calendar::title%]" - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 96ef5fd4c930d5..75062289c3863b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -708,7 +708,6 @@ "volumio", "volvo", "volvooncall", - "vulcan", "wake_on_lan", "wallbox", "waqi", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0df4cc993cd5c0..1e6f2a247c8d07 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -7306,12 +7306,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "vulcan": { - "name": "Uonet+ Vulcan", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "vultr": { "name": "Vultr", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 64e51835c0aa54..5311374d4e6a8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3075,9 +3075,6 @@ vsure==2.6.7 # homeassistant.components.vasttrafik vtjp==0.2.1 -# homeassistant.components.vulcan -vulcan-api==2.4.2 - # homeassistant.components.vultr vultr==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dabe084e2fb0fb..ff39e8e525afe8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2537,9 +2537,6 @@ volvooncall==0.10.3 # homeassistant.components.verisure vsure==2.6.7 -# homeassistant.components.vulcan -vulcan-api==2.4.2 - # homeassistant.components.vultr vultr==0.1.2 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 750cefbb749af7..598d0f5a99cef0 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -1069,7 +1069,6 @@ class Rule: "volkszaehler", "volumio", "volvooncall", - "vulcan", "vultr", "w800rf32", "wake_on_lan", @@ -2124,7 +2123,6 @@ class Rule: "volkszaehler", "volumio", "volvooncall", - "vulcan", "vultr", "w800rf32", "wake_on_lan", diff --git a/tests/components/analytics_insights/fixtures/current_data.json b/tests/components/analytics_insights/fixtures/current_data.json index ff1baca49ed6bb..f939d28da4fd08 100644 --- a/tests/components/analytics_insights/fixtures/current_data.json +++ b/tests/components/analytics_insights/fixtures/current_data.json @@ -799,7 +799,6 @@ "geofency": 313, "hvv_departures": 70, "devolo_home_control": 65, - "vulcan": 24, "laundrify": 151, "openhome": 730, "rainmachine": 381, diff --git a/tests/components/vulcan/__init__.py b/tests/components/vulcan/__init__.py deleted file mode 100644 index 6f165c36c3685a..00000000000000 --- a/tests/components/vulcan/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Uonet+ Vulcan integration.""" diff --git a/tests/components/vulcan/fixtures/fake_config_entry_data.json b/tests/components/vulcan/fixtures/fake_config_entry_data.json deleted file mode 100644 index 4dfcd630140964..00000000000000 --- a/tests/components/vulcan/fixtures/fake_config_entry_data.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "student_id": "123", - "keystore": { - "Certificate": "certificate", - "DeviceModel": "Home Assistant", - "Fingerprint": "fingerprint", - "FirebaseToken": "firebase_token", - "PrivateKey": "private_key" - }, - "account": { - "LoginId": 0, - "RestURL": "", - "UserLogin": "example@example.com", - "UserName": "example@example.com" - } -} diff --git a/tests/components/vulcan/fixtures/fake_student_1.json b/tests/components/vulcan/fixtures/fake_student_1.json deleted file mode 100644 index fef69684550d38..00000000000000 --- a/tests/components/vulcan/fixtures/fake_student_1.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "TopLevelPartition": "", - "Partition": "", - "ClassDisplay": "", - "Unit": { - "Id": 1, - "Symbol": "", - "Short": "", - "RestURL": "", - "Name": "", - "DisplayName": "" - }, - "ConstituentUnit": { - "Id": 1, - "Short": "", - "Name": "", - "Address": "" - }, - "Pupil": { - "Id": 0, - "LoginId": 0, - "LoginValue": "", - "FirstName": "Jan", - "SecondName": "Maciej", - "Surname": "Kowalski", - "Sex": true - }, - "Periods": [], - "State": 0, - "MessageBox": { - "Id": 1, - "GlobalKey": "00000000-0000-0000-0000-000000000000", - "Name": "Test" - } -} diff --git a/tests/components/vulcan/fixtures/fake_student_2.json b/tests/components/vulcan/fixtures/fake_student_2.json deleted file mode 100644 index e5200c12e17c37..00000000000000 --- a/tests/components/vulcan/fixtures/fake_student_2.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "TopLevelPartition": "", - "Partition": "", - "ClassDisplay": "", - "Unit": { - "Id": 1, - "Symbol": "", - "Short": "", - "RestURL": "", - "Name": "", - "DisplayName": "" - }, - "ConstituentUnit": { - "Id": 1, - "Short": "", - "Name": "", - "Address": "" - }, - "Pupil": { - "Id": 1, - "LoginId": 1, - "LoginValue": "", - "FirstName": "Magda", - "SecondName": "", - "Surname": "Kowalska", - "Sex": false - }, - "Periods": [], - "State": 0, - "MessageBox": { - "Id": 1, - "GlobalKey": "00000000-0000-0000-0000-000000000000", - "Name": "Test" - } -} diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py deleted file mode 100644 index e0b7c1a4fdc836..00000000000000 --- a/tests/components/vulcan/test_config_flow.py +++ /dev/null @@ -1,917 +0,0 @@ -"""Test the Uonet+ Vulcan config flow.""" - -import json -from unittest import mock -from unittest.mock import patch - -from vulcan import ( - Account, - ExpiredTokenException, - InvalidPINException, - InvalidSymbolException, - InvalidTokenException, - UnauthorizedCertificateException, -) -from vulcan.model import Student - -from homeassistant import config_entries -from homeassistant.components.vulcan import config_flow, register -from homeassistant.components.vulcan.config_flow import ClientConnectionError, Keystore -from homeassistant.components.vulcan.const import DOMAIN -from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry, async_load_fixture - -fake_keystore = Keystore("", "", "", "", "") -fake_account = Account( - login_id=1, - user_login="example@example.com", - user_name="example@example.com", - rest_url="rest_url", -) - - -async def test_show_form(hass: HomeAssistant) -> None: - """Test that the form is served with no input.""" - flow = config_flow.VulcanFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_success( - mock_keystore, mock_account, mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow initialized by the user.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 1 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_success_with_multiple_students( - mock_keystore, mock_account, mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow with multiple students.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(student) - for student in ( - await async_load_fixture(hass, "fake_student_1.json", DOMAIN), - await async_load_fixture(hass, "fake_student_2.json", DOMAIN), - ) - ] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_student" - assert result["errors"] == {} - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 1 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -async def test_config_flow_reauth_success( - mock_account, mock_keystore, mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow reauth.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="0", - data={"student_id": "0"}, - ) - entry.add_to_hass(hass) - result = await entry.start_reauth_flow(hass) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {} - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -async def test_config_flow_reauth_without_matching_entries( - mock_account, mock_keystore, mock_student, hass: HomeAssistant -) -> None: - """Test a aborted config flow reauth caused by leak of matching entries.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="0", - data={"student_id": "1"}, - ) - entry.add_to_hass(hass) - result = await entry.start_reauth_flow(hass) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "no_matching_entries" - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -async def test_config_flow_reauth_with_errors( - mock_account, mock_keystore, hass: HomeAssistant -) -> None: - """Test reauth config flow with errors.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="0", - data={"student_id": "0"}, - ) - entry.add_to_hass(hass) - result = await entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {} - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidTokenException, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "invalid_token"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=ExpiredTokenException, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "expired_token"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidPINException, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "invalid_pin"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidSymbolException, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "invalid_symbol"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=ClientConnectionError, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "cannot_connect"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=Exception, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "unknown"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -async def test_multiple_config_entries( - mock_account, mock_keystore, mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow for multiple config entries.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - await register.register("token", "region", "000000") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": False}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 2 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -async def test_multiple_config_entries_using_saved_credentials( - mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow for multiple config entries using saved credentials.""" - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 2 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -async def test_multiple_config_entries_using_saved_credentials_2( - mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow for multiple config entries using saved credentials (different situation).""" - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)), - Student.load(await async_load_fixture(hass, "fake_student_2.json", DOMAIN)), - ] - MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_student" - assert result["errors"] == {} - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 2 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -async def test_multiple_config_entries_using_saved_credentials_3( - mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow for multiple config entries using saved credentials.""" - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - MockConfigEntry( - entry_id="456", - domain=DOMAIN, - unique_id="234567", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 3 - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -async def test_multiple_config_entries_using_saved_credentials_4( - mock_student, hass: HomeAssistant -) -> None: - """Test a successful config flow for multiple config entries using saved credentials (different situation).""" - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)), - Student.load(await async_load_fixture(hass, "fake_student_2.json", DOMAIN)), - ] - MockConfigEntry( - entry_id="456", - domain=DOMAIN, - unique_id="234567", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_student" - assert result["errors"] == {} - - with patch( - "homeassistant.components.vulcan.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Jan Kowalski" - assert len(mock_setup_entry.mock_calls) == 3 - - -async def test_multiple_config_entries_without_valid_saved_credentials( - hass: HomeAssistant, -) -> None: - """Test a unsuccessful config flow for multiple config entries without valid saved credentials.""" - MockConfigEntry( - entry_id="456", - domain=DOMAIN, - unique_id="234567", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - with patch( - "homeassistant.components.vulcan.config_flow.Vulcan.get_students", - side_effect=UnauthorizedCertificateException, - ): - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "expired_credentials"} - - -async def test_multiple_config_entries_using_saved_credentials_with_connections_issues( - hass: HomeAssistant, -) -> None: - """Test a unsuccessful config flow for multiple config entries without valid saved credentials.""" - MockConfigEntry( - entry_id="456", - domain=DOMAIN, - unique_id="234567", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - with patch( - "homeassistant.components.vulcan.config_flow.Vulcan.get_students", - side_effect=ClientConnectionError, - ): - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_multiple_config_entries_using_saved_credentials_with_unknown_error( - hass: HomeAssistant, -) -> None: - """Test a unsuccessful config flow for multiple config entries without valid saved credentials.""" - MockConfigEntry( - entry_id="456", - domain=DOMAIN, - unique_id="234567", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=DOMAIN, - unique_id="123456", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - with patch( - "homeassistant.components.vulcan.config_flow.Vulcan.get_students", - side_effect=Exception, - ): - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "unknown"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") -async def test_student_already_exists( - mock_account, mock_keystore, mock_student, hass: HomeAssistant -) -> None: - """Test config entry when student's entry already exists.""" - mock_keystore.return_value = fake_keystore - mock_account.return_value = fake_account - mock_student.return_value = [ - Student.load(await async_load_fixture(hass, "fake_student_1.json", DOMAIN)) - ] - MockConfigEntry( - domain=DOMAIN, - unique_id="0", - data=json.loads( - await async_load_fixture(hass, "fake_config_entry_data.json", DOMAIN) - ) - | {"student_id": "0"}, - ).add_to_hass(hass) - - await register.register("token", "region", "000000") - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "all_student_already_configured" - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_invalid_token( - mock_keystore, hass: HomeAssistant -) -> None: - """Test a config flow initialized by the user using invalid token.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidTokenException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "invalid_token"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_invalid_region( - mock_keystore, hass: HomeAssistant -) -> None: - """Test a config flow initialized by the user using invalid region.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidSymbolException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "invalid_symbol"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_invalid_pin(mock_keystore, hass: HomeAssistant) -> None: - """Test a config flow initialized by the with invalid pin.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=InvalidPINException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "invalid_pin"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_expired_token( - mock_keystore, hass: HomeAssistant -) -> None: - """Test a config flow initialized by the with expired token.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=ExpiredTokenException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "expired_token"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_connection_error( - mock_keystore, hass: HomeAssistant -) -> None: - """Test a config flow with connection error.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=ClientConnectionError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "cannot_connect"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_unknown_error( - mock_keystore, hass: HomeAssistant -) -> None: - """Test a config flow with unknown error.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=Exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "unknown"} From dc4d6ddbefdfa9a281e6c9cde832edb592d9b0d9 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 1 Sep 2025 17:07:36 +0100 Subject: [PATCH 63/95] Bump aiomealie to 0.10.2 (#151514) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index a744b9e6ced29b..dba018349eb851 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["aiomealie==0.10.1"] + "requirements": ["aiomealie==0.10.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5311374d4e6a8e..47e86db72b65c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ aiolookin==1.0.0 aiolyric==2.0.2 # homeassistant.components.mealie -aiomealie==0.10.1 +aiomealie==0.10.2 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff39e8e525afe8..fe3a23794bf763 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -292,7 +292,7 @@ aiolookin==1.0.0 aiolyric==2.0.2 # homeassistant.components.mealie -aiomealie==0.10.1 +aiomealie==0.10.2 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From 1f6853db282e9a91243be86cf68da65ed6567d32 Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Mon, 1 Sep 2025 17:54:16 +0200 Subject: [PATCH 64/95] Fix typo in const.py for Imeon inverter integration (#151515) Co-authored-by: TheBushBoy --- homeassistant/components/imeon_inverter/const.py | 2 +- tests/components/imeon_inverter/snapshots/test_sensor.ambr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imeon_inverter/const.py b/homeassistant/components/imeon_inverter/const.py index 45d43b1c1efd2d..44413a4c3402ae 100644 --- a/homeassistant/components/imeon_inverter/const.py +++ b/homeassistant/components/imeon_inverter/const.py @@ -13,7 +13,7 @@ "unsynchronized", "grid_consumption", "grid_injection", - "grid_synchronised_but_not_used", + "grid_synchronized_but_not_used", ] ATTR_TIMELINE_STATUS = [ "com_lost", diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index b860566a516579..5101880e7a5757 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -1355,7 +1355,7 @@ 'unsynchronized', 'grid_consumption', 'grid_injection', - 'grid_synchronised_but_not_used', + 'grid_synchronized_but_not_used', ]), }), 'config_entry_id': , @@ -1397,7 +1397,7 @@ 'unsynchronized', 'grid_consumption', 'grid_injection', - 'grid_synchronised_but_not_used', + 'grid_synchronized_but_not_used', ]), }), 'context': , From e57019a80b3d2baf63508da9409618faac8865ab Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 1 Sep 2025 23:37:49 +0200 Subject: [PATCH 65/95] Update frontend to 20250901.0 (#151529) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 8de9ccd4e5b3ef..2ecf80dcf217b0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250829.0"] + "requirements": ["home-assistant-frontend==20250901.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 21c72c8fbb5b44..a3921da6b1396e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250829.0 +home-assistant-frontend==20250901.0 home-assistant-intents==2025.8.29 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 47e86db72b65c7..7ab6a1c7387339 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250829.0 +home-assistant-frontend==20250901.0 # homeassistant.components.conversation home-assistant-intents==2025.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe3a23794bf763..68d4ab6f690c28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250829.0 +home-assistant-frontend==20250901.0 # homeassistant.components.conversation home-assistant-intents==2025.8.29 From 9910df2b21c76451faa72e67acee34946a8732da Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Mon, 1 Sep 2025 23:19:24 +0200 Subject: [PATCH 66/95] Remove mac address from Pooldose device (#151536) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- homeassistant/components/pooldose/entity.py | 5 +---- tests/components/pooldose/snapshots/test_init.ambr | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/pooldose/entity.py b/homeassistant/components/pooldose/entity.py index 806081ea41b117..84ae216e8ba3bf 100644 --- a/homeassistant/components/pooldose/entity.py +++ b/homeassistant/components/pooldose/entity.py @@ -4,7 +4,7 @@ from typing import Any -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -32,9 +32,6 @@ def device_info(info: dict | None, unique_id: str) -> DeviceInfo: else None ), hw_version=info.get("FW_CODE") or None, - connections=( - {(CONNECTION_NETWORK_MAC, str(info["MAC"]))} if info.get("MAC") else set() - ), configuration_url=( f"http://{info['IP']}/index.html" if info.get("IP") else None ), diff --git a/tests/components/pooldose/snapshots/test_init.ambr b/tests/components/pooldose/snapshots/test_init.ambr index 075a3d6a21d11f..b4a76f55c83bc3 100644 --- a/tests/components/pooldose/snapshots/test_init.ambr +++ b/tests/components/pooldose/snapshots/test_init.ambr @@ -6,10 +6,6 @@ 'config_entries_subentries': , 'configuration_url': 'http://192.168.1.100/index.html', 'connections': set({ - tuple( - 'mac', - 'aa:bb:cc:dd:ee:ff', - ), }), 'disabled_by': None, 'entry_type': None, From 1039936f39e11d3e81205c7b360701421e0525f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 2 Sep 2025 09:27:38 +0100 Subject: [PATCH 67/95] Filter out IPv6 addresses in Govee Light Local (#151540) --- homeassistant/components/govee_light_local/__init__.py | 7 ++++++- .../components/govee_light_local/config_flow.py | 10 +++++----- .../components/govee_light_local/coordinator.py | 5 ++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index 00f77189e2b8ba..803f4b3ead543c 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -5,6 +5,7 @@ import asyncio from contextlib import suppress from errno import EADDRINUSE +from ipaddress import IPv4Address import logging from govee_local_api.controller import LISTENING_PORT @@ -30,7 +31,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) - _LOGGER.debug("Enabled source IPs: %s", source_ips) coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator( - hass=hass, config_entry=entry, source_ips=source_ips + hass=hass, + config_entry=entry, + source_ips=[ + source_ip for source_ip in source_ips if isinstance(source_ip, IPv4Address) + ], ) async def await_cleanup(): diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index 67fa4b548cdd7f..8370da01669d25 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -4,7 +4,7 @@ import asyncio from contextlib import suppress -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address import logging from govee_local_api import GoveeController @@ -24,9 +24,7 @@ _LOGGER = logging.getLogger(__name__) -async def _async_discover( - hass: HomeAssistant, adapter_ip: IPv4Address | IPv6Address -) -> bool: +async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool: controller: GoveeController = GoveeController( loop=hass.loop, logger=_LOGGER, @@ -74,7 +72,9 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: _LOGGER.debug("Enabled source IPs: %s", source_ips) # Run discovery on every IPv4 address and gather results - results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips]) + results = await asyncio.gather( + *[_async_discover(hass, ip) for ip in source_ips if isinstance(ip, IPv4Address)] + ) return any(results) diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 9e0792a132dce7..31efeb55680d24 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Callable -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address import logging from govee_local_api import GoveeController, GoveeDevice @@ -30,7 +30,7 @@ def __init__( self, hass: HomeAssistant, config_entry: GoveeLocalConfigEntry, - source_ips: list[IPv4Address | IPv6Address], + source_ips: list[IPv4Address], ) -> None: """Initialize my coordinator.""" super().__init__( @@ -114,5 +114,4 @@ def devices(self) -> list[GoveeDevice]: async def _async_update_data(self) -> list[GoveeDevice]: for controller in self._controllers: controller.send_update_message() - return self.devices From ac4eef0571b1ad9559b63badc3d643eb2e6b4d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 2 Sep 2025 09:37:43 +0100 Subject: [PATCH 68/95] Add back missing controller cleanup to Govee Light Local (#151541) --- .../components/govee_light_local/config_flow.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index 8370da01669d25..a1f601b288893c 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -52,14 +52,9 @@ async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool: _LOGGER.debug("No devices found with IP %s", adapter_ip) devices_count = len(controller.devices) - cleanup_complete_events: list[asyncio.Event] = [] + cleanup_complete: asyncio.Event = controller.cleanup() with suppress(TimeoutError): - await asyncio.gather( - *[ - asyncio.wait_for(cleanup_complete_event.wait(), 1) - for cleanup_complete_event in cleanup_complete_events - ] - ) + await asyncio.wait_for(cleanup_complete.wait(), 1) return devices_count > 0 From ed9e46bbca7af87961be934a57daa98f52c41b67 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Sep 2025 08:42:14 +0000 Subject: [PATCH 69/95] Bump version to 2025.9.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 864c18ae23c075..b095fb9a32d1ff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index a087ecfe6c4dbd..1cfb34cf5af6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b2" +version = "2025.9.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 869801b643befa812543ab44cf658f754bb198f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Sep 2025 12:57:39 +0200 Subject: [PATCH 70/95] Exclude non mowers from husqvarna_automower_ble discovery (#151507) --- .../husqvarna_automower_ble/config_flow.py | 35 ++++++++---- .../husqvarna_automower_ble/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../husqvarna_automower_ble/__init__.py | 53 ++++++++++++++----- .../husqvarna_automower_ble/conftest.py | 6 +-- .../test_config_flow.py | 40 ++++++++++---- .../husqvarna_automower_ble/test_init.py | 4 +- 8 files changed, 100 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/husqvarna_automower_ble/config_flow.py b/homeassistant/components/husqvarna_automower_ble/config_flow.py index c8f1cfaf630234..d6ec59f0ec9600 100644 --- a/homeassistant/components/husqvarna_automower_ble/config_flow.py +++ b/homeassistant/components/husqvarna_automower_ble/config_flow.py @@ -10,6 +10,8 @@ from automower_ble.protocol import ResponseResult from bleak import BleakError from bleak_retry_connector import get_device +from gardena_bluetooth.const import ScanService +from gardena_bluetooth.parse import ManufacturerData, ProductType import voluptuous as vol from homeassistant.components import bluetooth @@ -22,20 +24,31 @@ def _is_supported(discovery_info: BluetoothServiceInfo): """Check if device is supported.""" + if ScanService not in discovery_info.service_uuids: + LOGGER.debug( + "Unsupported device, missing service %s: %s", ScanService, discovery_info + ) + return False - LOGGER.debug( - "%s manufacturer data: %s", - discovery_info.address, - discovery_info.manufacturer_data, - ) + if not (data := discovery_info.manufacturer_data.get(ManufacturerData.company)): + LOGGER.debug( + "Unsupported device, missing manufacturer data %s: %s", + ManufacturerData.company, + discovery_info, + ) + return False - manufacturer = any(key == 1062 for key in discovery_info.manufacturer_data) - service_husqvarna = any( - service == "98bd0001-0b0e-421a-84e5-ddbf75dc6de4" - for service in discovery_info.service_uuids - ) + manufacturer_data = ManufacturerData.decode(data) + product_type = ProductType.from_manufacturer_data(manufacturer_data) - return manufacturer and service_husqvarna + # Some mowers only expose the serial number in the manufacturer data + # and not the product type, so we allow None here as well. + if product_type not in (ProductType.MOWER, None): + LOGGER.debug("Unsupported device: %s (%s)", manufacturer_data, discovery_info) + return False + + LOGGER.debug("Supported device: %s", manufacturer_data) + return True def _pin_valid(pin: str) -> bool: diff --git a/homeassistant/components/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json index 50430c2a9fadf5..68cfd5e8486028 100644 --- a/homeassistant/components/husqvarna_automower_ble/manifest.json +++ b/homeassistant/components/husqvarna_automower_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "iot_class": "local_polling", - "requirements": ["automower-ble==0.2.7"] + "requirements": ["automower-ble==0.2.7", "gardena-bluetooth==1.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ab6a1c7387339..2b06cec2250aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -992,6 +992,7 @@ fyta_cli==0.7.2 gTTS==2.5.3 # homeassistant.components.gardena_bluetooth +# homeassistant.components.husqvarna_automower_ble gardena-bluetooth==1.6.0 # homeassistant.components.google_assistant_sdk diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68d4ab6f690c28..e4273d3a0e8ca7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,6 +862,7 @@ fyta_cli==0.7.2 gTTS==2.5.3 # homeassistant.components.gardena_bluetooth +# homeassistant.components.husqvarna_automower_ble gardena-bluetooth==1.6.0 # homeassistant.components.google_assistant_sdk diff --git a/tests/components/husqvarna_automower_ble/__init__.py b/tests/components/husqvarna_automower_ble/__init__.py index 841b6f65516af2..fbb2a67ab9ad35 100644 --- a/tests/components/husqvarna_automower_ble/__init__.py +++ b/tests/components/husqvarna_automower_ble/__init__.py @@ -9,15 +9,23 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -AUTOMOWER_SERVICE_INFO = BluetoothServiceInfo( +AUTOMOWER_SERVICE_INFO_SERIAL = BluetoothServiceInfo( name="305", address="00000000-0000-0000-0000-000000000003", rssi=-63, service_data={}, manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"}, - service_uuids=[ - "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - ], + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) + +AUTOMOWER_SERVICE_INFO_MOWER = BluetoothServiceInfo( + name="305", + address="00000000-0000-0000-0000-000000000003", + rssi=-63, + service_data={}, + manufacturer_data={1062: bytes.fromhex("02050104060a2301")}, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) @@ -27,9 +35,7 @@ rssi=-63, service_data={}, manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"}, - service_uuids=[ - "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - ], + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) @@ -39,9 +45,7 @@ rssi=-63, service_data={}, manufacturer_data={}, - service_uuids=[ - "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - ], + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) @@ -51,9 +55,30 @@ rssi=-63, service_data={}, manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"}, - service_uuids=[ - "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", - ], + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) + +MISSING_SERVICE_SERVICE_INFO = BluetoothServiceInfo( + name="Blah", + address="00000000-0000-0000-0000-000000000001", + rssi=-63, + service_data={}, + manufacturer_data={}, + service_uuids=[], + source="local", +) + + +WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo( + name="Timer", + address="00000000-0000-0000-0000-000000000001", + rssi=-63, + service_data={}, + manufacturer_data={ + 1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x12\x00\x01" + }, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) @@ -63,7 +88,7 @@ async def setup_entry( ) -> None: """Make sure the device is available.""" - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) + inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO_SERIAL) with patch("homeassistant.components.husqvarna_automower_ble.PLATFORMS", platforms): mock_entry.add_to_hass(hass) diff --git a/tests/components/husqvarna_automower_ble/conftest.py b/tests/components/husqvarna_automower_ble/conftest.py index 820edb29059ab6..f5aebf54b7aa14 100644 --- a/tests/components/husqvarna_automower_ble/conftest.py +++ b/tests/components/husqvarna_automower_ble/conftest.py @@ -9,7 +9,7 @@ from homeassistant.components.husqvarna_automower_ble.const import DOMAIN from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN -from . import AUTOMOWER_SERVICE_INFO +from . import AUTOMOWER_SERVICE_INFO_SERIAL from tests.common import MockConfigEntry @@ -56,9 +56,9 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, title="Husqvarna AutoMower", data={ - CONF_ADDRESS: AUTOMOWER_SERVICE_INFO.address, + CONF_ADDRESS: AUTOMOWER_SERVICE_INFO_SERIAL.address, CONF_CLIENT_ID: 1197489078, CONF_PIN: "1234", }, - unique_id=AUTOMOWER_SERVICE_INFO.address, + unique_id=AUTOMOWER_SERVICE_INFO_SERIAL.address, ) diff --git a/tests/components/husqvarna_automower_ble/test_config_flow.py b/tests/components/husqvarna_automower_ble/test_config_flow.py index 7b47063975e14f..affa3715ab8208 100644 --- a/tests/components/husqvarna_automower_ble/test_config_flow.py +++ b/tests/components/husqvarna_automower_ble/test_config_flow.py @@ -11,11 +11,15 @@ from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from . import ( AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO, - AUTOMOWER_SERVICE_INFO, + AUTOMOWER_SERVICE_INFO_MOWER, + AUTOMOWER_SERVICE_INFO_SERIAL, AUTOMOWER_UNNAMED_SERVICE_INFO, + MISSING_SERVICE_SERVICE_INFO, + WATER_TIMER_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -121,10 +125,16 @@ async def test_user_selection_incorrect_pin( } -async def test_bluetooth(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "service_info", + [AUTOMOWER_SERVICE_INFO_MOWER, AUTOMOWER_SERVICE_INFO_SERIAL], +) +async def test_bluetooth( + hass: HomeAssistant, service_info: BluetoothServiceInfo +) -> None: """Test bluetooth device discovery.""" - inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO) + inject_bluetooth_service_info(hass, service_info) await hass.async_block_till_done(wait_background_tasks=True) result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] @@ -157,7 +167,7 @@ async def test_bluetooth_incorrect_pin( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, - data=AUTOMOWER_SERVICE_INFO, + data=AUTOMOWER_SERVICE_INFO_SERIAL, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -214,7 +224,7 @@ async def test_bluetooth_unknown_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, - data=AUTOMOWER_SERVICE_INFO, + data=AUTOMOWER_SERVICE_INFO_SERIAL, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -241,7 +251,7 @@ async def test_bluetooth_not_paired( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, - data=AUTOMOWER_SERVICE_INFO, + data=AUTOMOWER_SERVICE_INFO_SERIAL, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -274,18 +284,26 @@ async def test_bluetooth_not_paired( } -async def test_bluetooth_invalid(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "service_info", + [ + AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO, + MISSING_SERVICE_SERVICE_INFO, + WATER_TIMER_SERVICE_INFO, + ], +) +async def test_bluetooth_invalid( + hass: HomeAssistant, service_info: BluetoothServiceInfo +) -> None: """Test bluetooth device discovery with invalid data.""" - inject_bluetooth_service_info( - hass, AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO - ) + inject_bluetooth_service_info(hass, service_info) await hass.async_block_till_done(wait_background_tasks=True) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, - data=AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO, + data=service_info, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/husqvarna_automower_ble/test_init.py b/tests/components/husqvarna_automower_ble/test_init.py index 341cc3c282fe84..f10ae1fa7430ac 100644 --- a/tests/components/husqvarna_automower_ble/test_init.py +++ b/tests/components/husqvarna_automower_ble/test_init.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from . import AUTOMOWER_SERVICE_INFO +from . import AUTOMOWER_SERVICE_INFO_SERIAL from tests.common import MockConfigEntry @@ -34,7 +34,7 @@ async def test_setup( assert mock_config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, f"{AUTOMOWER_SERVICE_INFO.address}_1197489078")} + identifiers={(DOMAIN, f"{AUTOMOWER_SERVICE_INFO_SERIAL.address}_1197489078")} ) assert device_entry == snapshot From 6023a8e6b04fd0fb8c395a726ccf07e173ec9cb6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 2 Sep 2025 13:06:07 +0200 Subject: [PATCH 71/95] Remove config entry from device instead of deleting in Uptime robot (#151557) --- homeassistant/components/uptimerobot/coordinator.py | 5 ++++- homeassistant/components/uptimerobot/quality_scale.yaml | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 7ecb1ee3313d5d..78866800effb75 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -65,7 +65,10 @@ async def _async_update_data(self) -> list[UptimeRobotMonitor]: if device := device_registry.async_get_device( identifiers={(DOMAIN, monitor_id)} ): - device_registry.async_remove_device(device.id) + device_registry.async_update_device( + device_id=device.id, + remove_config_entry_id=self.config_entry.entry_id, + ) # If there are new monitors, we should reload the config entry so we can # create new devices and entities. diff --git a/homeassistant/components/uptimerobot/quality_scale.yaml b/homeassistant/components/uptimerobot/quality_scale.yaml index 1244d6a4c19ea6..2152f572853719 100644 --- a/homeassistant/components/uptimerobot/quality_scale.yaml +++ b/homeassistant/components/uptimerobot/quality_scale.yaml @@ -74,9 +74,7 @@ rules: repair-issues: status: exempt comment: no known use cases for repair issues or flows, yet - stale-devices: - status: todo - comment: We should remove the config entry from the device rather than remove the device + stale-devices: done # Platinum async-dependency: done From 2afbca9751181f5f94b101e1d2b1a94e138cdf0a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Sep 2025 11:54:37 +0200 Subject: [PATCH 72/95] Revert "Improve migration to entity registry version 1.18" (#151561) --- homeassistant/helpers/entity_registry.py | 97 ++---- tests/helpers/test_entity_registry.py | 392 +---------------------- 2 files changed, 35 insertions(+), 454 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 95aa153ff0090c..571f914e9d3074 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -16,7 +16,7 @@ from enum import StrEnum import logging import time -from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict +from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict import attr import voluptuous as vol @@ -85,8 +85,6 @@ CLEANUP_INTERVAL = 3600 * 24 ORPHANED_ENTITY_KEEP_SECONDS = 3600 * 24 * 30 -UNDEFINED_STR: Final = "UNDEFINED" - ENTITY_CATEGORY_VALUE_TO_INDEX: dict[EntityCategory | None, int] = { val: idx for idx, val in enumerate(EntityCategory) } @@ -166,17 +164,6 @@ def _protect_entity_options( return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) -def _protect_optional_entity_options( - data: EntityOptionsType | UndefinedType | None, -) -> ReadOnlyEntityOptionsType | UndefinedType: - """Protect entity options from being modified.""" - if data is UNDEFINED: - return UNDEFINED - if data is None: - return ReadOnlyDict({}) - return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) - - @attr.s(frozen=True, kw_only=True, slots=True) class RegistryEntry: """Entity Registry Entry.""" @@ -427,17 +414,15 @@ class DeletedRegistryEntry: config_subentry_id: str | None = attr.ib() created_at: datetime = attr.ib() device_class: str | None = attr.ib() - disabled_by: RegistryEntryDisabler | UndefinedType | None = attr.ib() + disabled_by: RegistryEntryDisabler | None = attr.ib() domain: str = attr.ib(init=False, repr=False) - hidden_by: RegistryEntryHider | UndefinedType | None = attr.ib() + hidden_by: RegistryEntryHider | None = attr.ib() icon: str | None = attr.ib() id: str = attr.ib() labels: set[str] = attr.ib() modified_at: datetime = attr.ib() name: str | None = attr.ib() - options: ReadOnlyEntityOptionsType | UndefinedType = attr.ib( - converter=_protect_optional_entity_options - ) + options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options) orphaned_timestamp: float | None = attr.ib() _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) @@ -460,21 +445,15 @@ def as_storage_fragment(self) -> json_fragment: "config_subentry_id": self.config_subentry_id, "created_at": self.created_at, "device_class": self.device_class, - "disabled_by": self.disabled_by - if self.disabled_by is not UNDEFINED - else UNDEFINED_STR, + "disabled_by": self.disabled_by, "entity_id": self.entity_id, - "hidden_by": self.hidden_by - if self.hidden_by is not UNDEFINED - else UNDEFINED_STR, + "hidden_by": self.hidden_by, "icon": self.icon, "id": self.id, "labels": list(self.labels), "modified_at": self.modified_at, "name": self.name, - "options": self.options - if self.options is not UNDEFINED - else UNDEFINED_STR, + "options": self.options, "orphaned_timestamp": self.orphaned_timestamp, "platform": self.platform, "unique_id": self.unique_id, @@ -605,12 +584,12 @@ async def _async_migrate_func( # noqa: C901 entity["area_id"] = None entity["categories"] = {} entity["device_class"] = None - entity["disabled_by"] = UNDEFINED_STR - entity["hidden_by"] = UNDEFINED_STR + entity["disabled_by"] = None + entity["hidden_by"] = None entity["icon"] = None entity["labels"] = [] entity["name"] = None - entity["options"] = UNDEFINED_STR + entity["options"] = {} if old_major_version > 1: raise NotImplementedError @@ -979,30 +958,25 @@ def async_get_or_create( categories = deleted_entity.categories created_at = deleted_entity.created_at device_class = deleted_entity.device_class - if deleted_entity.disabled_by is not UNDEFINED: - disabled_by = deleted_entity.disabled_by - # Adjust disabled_by based on config entry state - if config_entry and config_entry is not UNDEFINED: - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = RegistryEntryDisabler.CONFIG_ENTRY - elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: - disabled_by = None + disabled_by = deleted_entity.disabled_by + # Adjust disabled_by based on config entry state + if config_entry and config_entry is not UNDEFINED: + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = RegistryEntryDisabler.CONFIG_ENTRY elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: disabled_by = None + elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: + disabled_by = None # Restore entity_id if it's available if self._entity_id_available(deleted_entity.entity_id): entity_id = deleted_entity.entity_id entity_registry_id = deleted_entity.id - if deleted_entity.hidden_by is not UNDEFINED: - hidden_by = deleted_entity.hidden_by + hidden_by = deleted_entity.hidden_by icon = deleted_entity.icon labels = deleted_entity.labels name = deleted_entity.name - if deleted_entity.options is not UNDEFINED: - options = deleted_entity.options - else: - options = get_initial_options() if get_initial_options else None + options = deleted_entity.options else: aliases = set() area_id = None @@ -1555,20 +1529,6 @@ async def async_load(self) -> None: previous_unique_id=entity["previous_unique_id"], unit_of_measurement=entity["unit_of_measurement"], ) - - def get_optional_enum[_EnumT: StrEnum]( - cls: type[_EnumT], value: str | None - ) -> _EnumT | UndefinedType | None: - """Convert string to the passed enum, UNDEFINED or None.""" - if value is None: - return None - if value == UNDEFINED_STR: - return UNDEFINED - try: - return cls(value) - except ValueError: - return None - for entity in data["deleted_entities"]: try: domain = split_entity_id(entity["entity_id"])[0] @@ -1586,7 +1546,6 @@ def get_optional_enum[_EnumT: StrEnum]( entity["platform"], entity["unique_id"], ) - deleted_entities[key] = DeletedRegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], @@ -1595,21 +1554,23 @@ def get_optional_enum[_EnumT: StrEnum]( config_subentry_id=entity["config_subentry_id"], created_at=datetime.fromisoformat(entity["created_at"]), device_class=entity["device_class"], - disabled_by=get_optional_enum( - RegistryEntryDisabler, entity["disabled_by"] + disabled_by=( + RegistryEntryDisabler(entity["disabled_by"]) + if entity["disabled_by"] + else None ), entity_id=entity["entity_id"], - hidden_by=get_optional_enum( - RegistryEntryHider, entity["hidden_by"] + hidden_by=( + RegistryEntryHider(entity["hidden_by"]) + if entity["hidden_by"] + else None ), icon=entity["icon"], id=entity["id"], labels=set(entity["labels"]), modified_at=datetime.fromisoformat(entity["modified_at"]), name=entity["name"], - options=entity["options"] - if entity["options"] is not UNDEFINED_STR - else UNDEFINED, + options=entity["options"], orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index da6cdf806d7a82..acbcb02a5ded8a 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -20,7 +20,6 @@ from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event -from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.dt import utc_from_timestamp, utcnow from tests.common import ( @@ -963,10 +962,9 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) assert entry.device_class is None assert entry.original_device_class == "best_class" - # Check migrated data + # Check we store migrated data await flush_store(registry._store) - migrated_data = hass_storage[er.STORAGE_KEY] - assert migrated_data == { + assert hass_storage[er.STORAGE_KEY] == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1009,11 +1007,6 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) }, } - # Serialize the migrated data again - registry.async_schedule_save() - await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == migrated_data - @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: @@ -1149,17 +1142,9 @@ async def test_migration_1_11( assert entry.device_class is None assert entry.original_device_class == "best_class" - deleted_entry = registry.deleted_entities[ - ("test", "super_duper_platform", "very_very_unique") - ] - assert deleted_entry.disabled_by is UNDEFINED - assert deleted_entry.hidden_by is UNDEFINED - assert deleted_entry.options is UNDEFINED - # Check migrated data await flush_store(registry._store) - migrated_data = hass_storage[er.STORAGE_KEY] - assert migrated_data == { + assert hass_storage[er.STORAGE_KEY] == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1207,15 +1192,15 @@ async def test_migration_1_11( "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", "device_class": None, - "disabled_by": "UNDEFINED", + "disabled_by": None, "entity_id": "test.deleted_entity", - "hidden_by": "UNDEFINED", + "hidden_by": None, "icon": None, "id": "23456", "labels": [], "modified_at": "1970-01-01T00:00:00+00:00", "name": None, - "options": "UNDEFINED", + "options": {}, "orphaned_timestamp": None, "platform": "super_duper_platform", "unique_id": "very_very_unique", @@ -1224,11 +1209,6 @@ async def test_migration_1_11( }, } - # Serialize the migrated data again - registry.async_schedule_save() - await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == migrated_data - async def test_update_entity_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry @@ -3170,366 +3150,6 @@ async def test_restore_entity( assert update_events[16].data == {"action": "create", "entity_id": "light.hue_1234"} -@pytest.mark.parametrize( - ("entity_disabled_by"), - [ - None, - er.RegistryEntryDisabler.CONFIG_ENTRY, - er.RegistryEntryDisabler.DEVICE, - er.RegistryEntryDisabler.HASS, - er.RegistryEntryDisabler.INTEGRATION, - er.RegistryEntryDisabler.USER, - ], -) -@pytest.mark.usefixtures("freezer") -async def test_restore_migrated_entity_disabled_by( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - entity_disabled_by: er.RegistryEntryDisabler | None, -) -> None: - """Check how the disabled_by flag is treated when restoring an entity.""" - update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entry = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key1": "value1"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.DIAGNOSTIC, - get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, - has_entity_name=True, - hidden_by=er.RegistryEntryHider.INTEGRATION, - original_device_class="device_class_1", - original_icon="original_icon_1", - original_name="original_name_1", - suggested_object_id="hue_5678", - supported_features=1, - translation_key="translation_key_1", - unit_of_measurement="unit_1", - ) - - entity_registry.async_remove(entry.entity_id) - assert len(entity_registry.entities) == 0 - assert len(entity_registry.deleted_entities) == 1 - - deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] - entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( - deleted_entry, disabled_by=UNDEFINED - ) - - # Re-add entity, integration has changed - entry_restored = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key2": "value2"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=entity_disabled_by, - entity_category=EntityCategory.CONFIG, - get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, - has_entity_name=False, - hidden_by=None, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - assert len(entity_registry.entities) == 1 - assert len(entity_registry.deleted_entities) == 0 - assert entry != entry_restored - # entity_id and user customizations are restored. new integration options are - # respected. - assert entry_restored == er.RegistryEntry( - entity_id="light.hue_5678", - unique_id="1234", - platform="hue", - aliases=set(), - area_id=None, - categories={}, - capabilities={"key2": "value2"}, - config_entry_id=config_entry.entry_id, - config_subentry_id=None, - created_at=utcnow(), - device_class=None, - device_id=device_entry.id, - disabled_by=entity_disabled_by, - entity_category=EntityCategory.CONFIG, - has_entity_name=False, - hidden_by=er.RegistryEntryHider.INTEGRATION, - icon=None, - id=entry.id, - labels=set(), - modified_at=utcnow(), - name=None, - options={"test_domain": {"key1": "value1"}}, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - # Check the events - await hass.async_block_till_done() - assert len(update_events) == 3 - assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} - assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} - - -@pytest.mark.parametrize( - ("entity_hidden_by"), - [ - None, - er.RegistryEntryHider.INTEGRATION, - er.RegistryEntryHider.USER, - ], -) -@pytest.mark.usefixtures("freezer") -async def test_restore_migrated_entity_hidden_by( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - entity_hidden_by: er.RegistryEntryHider | None, -) -> None: - """Check how the hidden_by flag is treated when restoring an entity.""" - update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entry = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key1": "value1"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.DIAGNOSTIC, - get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, - has_entity_name=True, - hidden_by=er.RegistryEntryHider.INTEGRATION, - original_device_class="device_class_1", - original_icon="original_icon_1", - original_name="original_name_1", - suggested_object_id="hue_5678", - supported_features=1, - translation_key="translation_key_1", - unit_of_measurement="unit_1", - ) - - entity_registry.async_remove(entry.entity_id) - assert len(entity_registry.entities) == 0 - assert len(entity_registry.deleted_entities) == 1 - - deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] - entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( - deleted_entry, hidden_by=UNDEFINED - ) - - # Re-add entity, integration has changed - entry_restored = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key2": "value2"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.CONFIG, - get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, - has_entity_name=False, - hidden_by=entity_hidden_by, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - assert len(entity_registry.entities) == 1 - assert len(entity_registry.deleted_entities) == 0 - assert entry != entry_restored - # entity_id and user customizations are restored. new integration options are - # respected. - assert entry_restored == er.RegistryEntry( - entity_id="light.hue_5678", - unique_id="1234", - platform="hue", - aliases=set(), - area_id=None, - categories={}, - capabilities={"key2": "value2"}, - config_entry_id=config_entry.entry_id, - config_subentry_id=None, - created_at=utcnow(), - device_class=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.CONFIG, - has_entity_name=False, - hidden_by=entity_hidden_by, - icon=None, - id=entry.id, - labels=set(), - modified_at=utcnow(), - name=None, - options={"test_domain": {"key1": "value1"}}, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - # Check the events - await hass.async_block_till_done() - assert len(update_events) == 3 - assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} - assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} - - -@pytest.mark.usefixtures("freezer") -async def test_restore_migrated_entity_initial_options( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Check how the initial options is treated when restoring an entity.""" - update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entry = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key1": "value1"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.DIAGNOSTIC, - get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, - has_entity_name=True, - hidden_by=er.RegistryEntryHider.INTEGRATION, - original_device_class="device_class_1", - original_icon="original_icon_1", - original_name="original_name_1", - suggested_object_id="hue_5678", - supported_features=1, - translation_key="translation_key_1", - unit_of_measurement="unit_1", - ) - - entity_registry.async_remove(entry.entity_id) - assert len(entity_registry.entities) == 0 - assert len(entity_registry.deleted_entities) == 1 - - deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] - entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( - deleted_entry, options=UNDEFINED - ) - - # Re-add entity, integration has changed - entry_restored = entity_registry.async_get_or_create( - "light", - "hue", - "1234", - capabilities={"key2": "value2"}, - config_entry=config_entry, - config_subentry_id=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.CONFIG, - get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, - has_entity_name=False, - hidden_by=None, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - assert len(entity_registry.entities) == 1 - assert len(entity_registry.deleted_entities) == 0 - assert entry != entry_restored - # entity_id and user customizations are restored. new integration options are - # respected. - assert entry_restored == er.RegistryEntry( - entity_id="light.hue_5678", - unique_id="1234", - platform="hue", - aliases=set(), - area_id=None, - categories={}, - capabilities={"key2": "value2"}, - config_entry_id=config_entry.entry_id, - config_subentry_id=None, - created_at=utcnow(), - device_class=None, - device_id=device_entry.id, - disabled_by=None, - entity_category=EntityCategory.CONFIG, - has_entity_name=False, - hidden_by=er.RegistryEntryHider.INTEGRATION, - icon=None, - id=entry.id, - labels=set(), - modified_at=utcnow(), - name=None, - options={"test_domain": {"key2": "value2"}}, - original_device_class="device_class_2", - original_icon="original_icon_2", - original_name="original_name_2", - suggested_object_id="suggested_2", - supported_features=2, - translation_key="translation_key_2", - unit_of_measurement="unit_2", - ) - - # Check the events - await hass.async_block_till_done() - assert len(update_events) == 3 - assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} - assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} - - @pytest.mark.parametrize( ( "config_entry_disabled_by", From 7500406e36830d1e3aac06c0bf4efc864062d90f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Sep 2025 11:58:27 +0200 Subject: [PATCH 73/95] Revert "Improve migration to device registry version 1.11" (#151563) --- homeassistant/helpers/device_registry.py | 49 +++------ tests/helpers/test_device_registry.py | 131 +---------------------- 2 files changed, 15 insertions(+), 165 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 222e13963808fc..f08114095d4a79 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -9,7 +9,7 @@ from functools import lru_cache import logging import time -from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, TypedDict import attr from yarl import URL @@ -68,8 +68,6 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 -UNDEFINED_STR: Final = "UNDEFINED" - # Can be removed when suggested_area is removed from DeviceEntry RUNTIME_ONLY_ATTRS = {"suggested_area"} @@ -467,7 +465,7 @@ class DeletedDeviceEntry: validator=_normalize_connections_validator ) created_at: datetime = attr.ib() - disabled_by: DeviceEntryDisabler | UndefinedType | None = attr.ib() + disabled_by: DeviceEntryDisabler | None = attr.ib() id: str = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() labels: set[str] = attr.ib() @@ -482,19 +480,15 @@ def to_device_entry( config_subentry_id: str | None, connections: set[tuple[str, str]], identifiers: set[tuple[str, str]], - disabled_by: DeviceEntryDisabler | UndefinedType | None, ) -> DeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" # Adjust disabled_by based on config entry state - if self.disabled_by is not UNDEFINED: - disabled_by = self.disabled_by - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = DeviceEntryDisabler.CONFIG_ENTRY - elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: - disabled_by = None - else: - disabled_by = disabled_by if disabled_by is not UNDEFINED else None + disabled_by = self.disabled_by + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = DeviceEntryDisabler.CONFIG_ENTRY + elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: + disabled_by = None return DeviceEntry( area_id=self.area_id, # type ignores: likely https://github.com/python/mypy/issues/8625 @@ -526,9 +520,7 @@ def as_storage_fragment(self) -> json_fragment: }, "connections": list(self.connections), "created_at": self.created_at, - "disabled_by": self.disabled_by - if self.disabled_by is not UNDEFINED - else UNDEFINED_STR, + "disabled_by": self.disabled_by, "identifiers": list(self.identifiers), "id": self.id, "labels": list(self.labels), @@ -616,7 +608,7 @@ async def _async_migrate_func( # noqa: C901 # Introduced in 2025.6 for device in old_data["deleted_devices"]: device["area_id"] = None - device["disabled_by"] = UNDEFINED_STR + device["disabled_by"] = None device["labels"] = [] device["name_by_user"] = None if old_minor_version < 11: @@ -942,7 +934,6 @@ def async_get_or_create( config_subentry_id if config_subentry_id is not UNDEFINED else None, connections, identifiers, - disabled_by, ) disabled_by = UNDEFINED @@ -1453,21 +1444,7 @@ async def async_load(self) -> None: sw_version=device["sw_version"], via_device_id=device["via_device_id"], ) - # Introduced in 0.111 - def get_optional_enum[_EnumT: StrEnum]( - cls: type[_EnumT], value: str | None - ) -> _EnumT | UndefinedType | None: - """Convert string to the passed enum, UNDEFINED or None.""" - if value is None: - return None - if value == UNDEFINED_STR: - return UNDEFINED - try: - return cls(value) - except ValueError: - return None - for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( area_id=device["area_id"], @@ -1480,8 +1457,10 @@ def get_optional_enum[_EnumT: StrEnum]( }, connections={tuple(conn) for conn in device["connections"]}, created_at=datetime.fromisoformat(device["created_at"]), - disabled_by=get_optional_enum( - DeviceEntryDisabler, device["disabled_by"] + disabled_by=( + DeviceEntryDisabler(device["disabled_by"]) + if device["disabled_by"] + else None ), identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 8cfd3c66ad9be4..9690b2a52fae59 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,7 +8,6 @@ from typing import Any from unittest.mock import ANY, patch -import attr from freezegun.api import FrozenDateTimeFactory import pytest from yarl import URL @@ -22,7 +21,6 @@ device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_capture_events, flush_store @@ -510,9 +508,6 @@ async def test_migration_from_1_1( ) assert entry.id == "abcdefghijklm" - deleted_entry = registry.deleted_devices["deletedid"] - assert deleted_entry.disabled_by is UNDEFINED - # Update to trigger a store entry = registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, @@ -586,7 +581,7 @@ async def test_migration_from_1_1( "config_entries_subentries": {"123456": [None]}, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", - "disabled_by": "UNDEFINED", + "disabled_by": None, "id": "deletedid", "identifiers": [["serial", "123456ABCDFF"]], "labels": [], @@ -3838,130 +3833,6 @@ async def test_restore_device( } -@pytest.mark.parametrize( - ("device_disabled_by", "expected_disabled_by"), - [ - (None, None), - (dr.DeviceEntryDisabler.CONFIG_ENTRY, dr.DeviceEntryDisabler.CONFIG_ENTRY), - (dr.DeviceEntryDisabler.INTEGRATION, dr.DeviceEntryDisabler.INTEGRATION), - (dr.DeviceEntryDisabler.USER, dr.DeviceEntryDisabler.USER), - (UNDEFINED, None), - ], -) -@pytest.mark.usefixtures("freezer") -async def test_restore_migrated_device_disabled_by( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_config_entry: MockConfigEntry, - device_disabled_by: dr.DeviceEntryDisabler | UndefinedType | None, - expected_disabled_by: dr.DeviceEntryDisabler | None, -) -> None: - """Check how the disabled_by flag is treated when restoring a device.""" - entry_id = mock_config_entry.entry_id - update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) - entry = device_registry.async_get_or_create( - config_entry_id=entry_id, - config_subentry_id=None, - configuration_url="http://config_url_orig.bla", - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - disabled_by=None, - entry_type=dr.DeviceEntryType.SERVICE, - hw_version="hw_version_orig", - identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer_orig", - model="model_orig", - model_id="model_id_orig", - name="name_orig", - serial_number="serial_no_orig", - suggested_area="suggested_area_orig", - sw_version="version_orig", - via_device="via_device_id_orig", - ) - - assert len(device_registry.devices) == 1 - assert len(device_registry.deleted_devices) == 0 - - device_registry.async_remove_device(entry.id) - - assert len(device_registry.devices) == 0 - assert len(device_registry.deleted_devices) == 1 - - deleted_entry = device_registry.deleted_devices[entry.id] - device_registry.deleted_devices[entry.id] = attr.evolve( - deleted_entry, disabled_by=UNDEFINED - ) - - # This will restore the original device, user customizations of - # area_id, disabled_by, labels and name_by_user will be restored - entry3 = device_registry.async_get_or_create( - config_entry_id=entry_id, - config_subentry_id=None, - configuration_url="http://config_url_new.bla", - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - disabled_by=device_disabled_by, - entry_type=None, - hw_version="hw_version_new", - identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer_new", - model="model_new", - model_id="model_id_new", - name="name_new", - serial_number="serial_no_new", - suggested_area="suggested_area_new", - sw_version="version_new", - via_device="via_device_id_new", - ) - assert entry3 == dr.DeviceEntry( - area_id="suggested_area_orig", - config_entries={entry_id}, - config_entries_subentries={entry_id: {None}}, - configuration_url="http://config_url_new.bla", - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, - created_at=utcnow(), - disabled_by=expected_disabled_by, - entry_type=None, - hw_version="hw_version_new", - id=entry.id, - identifiers={("bridgeid", "0123")}, - labels=set(), - manufacturer="manufacturer_new", - model="model_new", - model_id="model_id_new", - modified_at=utcnow(), - name_by_user=None, - name="name_new", - primary_config_entry=entry_id, - serial_number="serial_no_new", - suggested_area="suggested_area_new", - sw_version="version_new", - ) - - assert entry.id == entry3.id - assert len(device_registry.devices) == 1 - assert len(device_registry.deleted_devices) == 0 - - assert isinstance(entry3.config_entries, set) - assert isinstance(entry3.connections, set) - assert isinstance(entry3.identifiers, set) - - await hass.async_block_till_done() - - assert len(update_events) == 3 - assert update_events[0].data == { - "action": "create", - "device_id": entry.id, - } - assert update_events[1].data == { - "action": "remove", - "device_id": entry.id, - "device": entry.dict_repr, - } - assert update_events[2].data == { - "action": "create", - "device_id": entry3.id, - } - - @pytest.mark.parametrize( ( "config_entry_disabled_by", From 75d6c0bb53309e2e4fc95408b9cbc708a8e12056 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Sep 2025 10:08:59 +0200 Subject: [PATCH 74/95] Improve migration to entity registry version 1.18 (#151570) --- homeassistant/helpers/entity_registry.py | 101 ++-- tests/helpers/test_entity_registry.py | 559 ++++++++++++++++++++++- 2 files changed, 631 insertions(+), 29 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 571f914e9d3074..f1a765b3ddcc87 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -79,7 +79,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 18 +STORAGE_VERSION_MINOR = 19 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -164,6 +164,17 @@ def _protect_entity_options( return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) +def _protect_optional_entity_options( + data: EntityOptionsType | UndefinedType | None, +) -> ReadOnlyEntityOptionsType | UndefinedType: + """Protect entity options from being modified.""" + if data is UNDEFINED: + return UNDEFINED + if data is None: + return ReadOnlyDict({}) + return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) + + @attr.s(frozen=True, kw_only=True, slots=True) class RegistryEntry: """Entity Registry Entry.""" @@ -414,15 +425,17 @@ class DeletedRegistryEntry: config_subentry_id: str | None = attr.ib() created_at: datetime = attr.ib() device_class: str | None = attr.ib() - disabled_by: RegistryEntryDisabler | None = attr.ib() + disabled_by: RegistryEntryDisabler | UndefinedType | None = attr.ib() domain: str = attr.ib(init=False, repr=False) - hidden_by: RegistryEntryHider | None = attr.ib() + hidden_by: RegistryEntryHider | UndefinedType | None = attr.ib() icon: str | None = attr.ib() id: str = attr.ib() labels: set[str] = attr.ib() modified_at: datetime = attr.ib() name: str | None = attr.ib() - options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options) + options: ReadOnlyEntityOptionsType | UndefinedType = attr.ib( + converter=_protect_optional_entity_options + ) orphaned_timestamp: float | None = attr.ib() _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) @@ -445,15 +458,22 @@ def as_storage_fragment(self) -> json_fragment: "config_subentry_id": self.config_subentry_id, "created_at": self.created_at, "device_class": self.device_class, - "disabled_by": self.disabled_by, + "disabled_by": self.disabled_by + if self.disabled_by is not UNDEFINED + else None, + "disabled_by_undefined": self.disabled_by is UNDEFINED, "entity_id": self.entity_id, - "hidden_by": self.hidden_by, + "hidden_by": self.hidden_by + if self.hidden_by is not UNDEFINED + else None, + "hidden_by_undefined": self.hidden_by is UNDEFINED, "icon": self.icon, "id": self.id, "labels": list(self.labels), "modified_at": self.modified_at, "name": self.name, - "options": self.options, + "options": self.options if self.options is not UNDEFINED else {}, + "options_undefined": self.options is UNDEFINED, "orphaned_timestamp": self.orphaned_timestamp, "platform": self.platform, "unique_id": self.unique_id, @@ -590,6 +610,14 @@ async def _async_migrate_func( # noqa: C901 entity["labels"] = [] entity["name"] = None entity["options"] = {} + if old_minor_version < 19: + # Version 1.19 adds undefined flags to deleted entities, this is a bugfix + # of version 1.18 + set_to_undefined = old_minor_version < 18 + for entity in data["deleted_entities"]: + entity["disabled_by_undefined"] = set_to_undefined + entity["hidden_by_undefined"] = set_to_undefined + entity["options_undefined"] = set_to_undefined if old_major_version > 1: raise NotImplementedError @@ -958,25 +986,30 @@ def async_get_or_create( categories = deleted_entity.categories created_at = deleted_entity.created_at device_class = deleted_entity.device_class - disabled_by = deleted_entity.disabled_by - # Adjust disabled_by based on config entry state - if config_entry and config_entry is not UNDEFINED: - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = RegistryEntryDisabler.CONFIG_ENTRY + if deleted_entity.disabled_by is not UNDEFINED: + disabled_by = deleted_entity.disabled_by + # Adjust disabled_by based on config entry state + if config_entry and config_entry is not UNDEFINED: + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = RegistryEntryDisabler.CONFIG_ENTRY + elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: + disabled_by = None elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: disabled_by = None - elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY: - disabled_by = None # Restore entity_id if it's available if self._entity_id_available(deleted_entity.entity_id): entity_id = deleted_entity.entity_id entity_registry_id = deleted_entity.id - hidden_by = deleted_entity.hidden_by + if deleted_entity.hidden_by is not UNDEFINED: + hidden_by = deleted_entity.hidden_by icon = deleted_entity.icon labels = deleted_entity.labels name = deleted_entity.name - options = deleted_entity.options + if deleted_entity.options is not UNDEFINED: + options = deleted_entity.options + else: + options = get_initial_options() if get_initial_options else None else: aliases = set() area_id = None @@ -1529,6 +1562,20 @@ async def async_load(self) -> None: previous_unique_id=entity["previous_unique_id"], unit_of_measurement=entity["unit_of_measurement"], ) + + def get_optional_enum[_EnumT: StrEnum]( + cls: type[_EnumT], value: str | None, undefined: bool + ) -> _EnumT | UndefinedType | None: + """Convert string to the passed enum, UNDEFINED or None.""" + if undefined: + return UNDEFINED + if value is None: + return None + try: + return cls(value) + except ValueError: + return None + for entity in data["deleted_entities"]: try: domain = split_entity_id(entity["entity_id"])[0] @@ -1554,23 +1601,25 @@ async def async_load(self) -> None: config_subentry_id=entity["config_subentry_id"], created_at=datetime.fromisoformat(entity["created_at"]), device_class=entity["device_class"], - disabled_by=( - RegistryEntryDisabler(entity["disabled_by"]) - if entity["disabled_by"] - else None + disabled_by=get_optional_enum( + RegistryEntryDisabler, + entity["disabled_by"], + entity["disabled_by_undefined"], ), entity_id=entity["entity_id"], - hidden_by=( - RegistryEntryHider(entity["hidden_by"]) - if entity["hidden_by"] - else None + hidden_by=get_optional_enum( + RegistryEntryHider, + entity["hidden_by"], + entity["hidden_by_undefined"], ), icon=entity["icon"], id=entity["id"], labels=set(entity["labels"]), modified_at=datetime.fromisoformat(entity["modified_at"]), name=entity["name"], - options=entity["options"], + options=entity["options"] + if not entity["options_undefined"] + else UNDEFINED, orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index acbcb02a5ded8a..421f52bca73769 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -20,6 +20,7 @@ from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.dt import utc_from_timestamp, utcnow from tests.common import ( @@ -610,14 +611,17 @@ async def test_load_bad_data( "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "disabled_by": None, + "disabled_by_undefined": False, "entity_id": "test.test3", "hidden_by": None, + "hidden_by_undefined": False, "icon": None, "id": "00003", "labels": [], "modified_at": "2024-02-14T12:00:00.900075+00:00", "name": None, "options": None, + "options_undefined": False, "orphaned_timestamp": None, "platform": "super_platform", "unique_id": 234, # Should not load @@ -631,14 +635,17 @@ async def test_load_bad_data( "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "disabled_by": None, + "disabled_by_undefined": False, "entity_id": "test.test4", "hidden_by": None, + "hidden_by_undefined": False, "icon": None, "id": "00004", "labels": [], "modified_at": "2024-02-14T12:00:00.900075+00:00", "name": None, "options": None, + "options_undefined": False, "orphaned_timestamp": None, "platform": "super_platform", "unique_id": ["also", "not", "valid"], # Should not load @@ -962,9 +969,10 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) assert entry.device_class is None assert entry.original_device_class == "best_class" - # Check we store migrated data + # Check migrated data await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == { + migrated_data = hass_storage[er.STORAGE_KEY] + assert migrated_data == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1007,6 +1015,11 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) }, } + # Serialize the migrated data again + registry.async_schedule_save() + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == migrated_data + @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: @@ -1142,9 +1155,181 @@ async def test_migration_1_11( assert entry.device_class is None assert entry.original_device_class == "best_class" + deleted_entry = registry.deleted_entities[ + ("test", "super_duper_platform", "very_very_unique") + ] + assert deleted_entry.disabled_by is UNDEFINED + assert deleted_entry.hidden_by is UNDEFINED + assert deleted_entry.options is UNDEFINED + + # Check migrated data + await flush_store(registry._store) + migrated_data = hass_storage[er.STORAGE_KEY] + assert migrated_data == { + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": er.STORAGE_VERSION_MINOR, + "key": er.STORAGE_KEY, + "data": { + "entities": [ + { + "aliases": [], + "area_id": None, + "capabilities": {}, + "categories": {}, + "config_entry_id": None, + "config_subentry_id": None, + "created_at": "1970-01-01T00:00:00+00:00", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.entity", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, + "original_device_class": "best_class", + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "suggested_object_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": "very_unique", + "unit_of_measurement": None, + "device_class": None, + } + ], + "deleted_entities": [ + { + "aliases": [], + "area_id": None, + "categories": {}, + "config_entry_id": None, + "config_subentry_id": None, + "created_at": "1970-01-01T00:00:00+00:00", + "device_class": None, + "disabled_by": None, + "disabled_by_undefined": True, + "entity_id": "test.deleted_entity", + "hidden_by": None, + "hidden_by_undefined": True, + "icon": None, + "id": "23456", + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, + "options_undefined": True, + "orphaned_timestamp": None, + "platform": "super_duper_platform", + "unique_id": "very_very_unique", + } + ], + }, + } + + # Serialize the migrated data again + registry.async_schedule_save() + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == migrated_data + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_18( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test migration from version 1.18. + + This version has a flawed migration. + """ + hass_storage[er.STORAGE_KEY] = { + "version": 1, + "minor_version": 18, + "data": { + "entities": [ + { + "aliases": [], + "area_id": None, + "capabilities": {}, + "categories": {}, + "config_entry_id": None, + "config_subentry_id": None, + "created_at": "1970-01-01T00:00:00+00:00", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.entity", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "12345", + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, + "original_device_class": "best_class", + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "suggested_object_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": "very_unique", + "unit_of_measurement": None, + "device_class": None, + } + ], + "deleted_entities": [ + { + "aliases": [], + "area_id": None, + "categories": {}, + "config_entry_id": None, + "config_subentry_id": None, + "created_at": "1970-01-01T00:00:00+00:00", + "device_class": None, + "disabled_by": None, + "entity_id": "test.deleted_entity", + "hidden_by": None, + "icon": None, + "id": "23456", + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, + "orphaned_timestamp": None, + "platform": "super_duper_platform", + "unique_id": "very_very_unique", + } + ], + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + entry = registry.async_get_or_create("test", "super_platform", "very_unique") + + assert entry.device_class is None + assert entry.original_device_class == "best_class" + + deleted_entry = registry.deleted_entities[ + ("test", "super_duper_platform", "very_very_unique") + ] + assert deleted_entry.disabled_by is None + assert deleted_entry.hidden_by is None + assert deleted_entry.options == {} + # Check migrated data await flush_store(registry._store) - assert hass_storage[er.STORAGE_KEY] == { + migrated_data = hass_storage[er.STORAGE_KEY] + assert migrated_data == { "version": er.STORAGE_VERSION_MAJOR, "minor_version": er.STORAGE_VERSION_MINOR, "key": er.STORAGE_KEY, @@ -1193,14 +1378,17 @@ async def test_migration_1_11( "created_at": "1970-01-01T00:00:00+00:00", "device_class": None, "disabled_by": None, + "disabled_by_undefined": False, "entity_id": "test.deleted_entity", "hidden_by": None, + "hidden_by_undefined": False, "icon": None, "id": "23456", "labels": [], "modified_at": "1970-01-01T00:00:00+00:00", "name": None, "options": {}, + "options_undefined": False, "orphaned_timestamp": None, "platform": "super_duper_platform", "unique_id": "very_very_unique", @@ -1209,6 +1397,11 @@ async def test_migration_1_11( }, } + # Serialize the migrated data again + registry.async_schedule_save() + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == migrated_data + async def test_update_entity_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry @@ -3150,6 +3343,366 @@ async def test_restore_entity( assert update_events[16].data == {"action": "create", "entity_id": "light.hue_1234"} +@pytest.mark.parametrize( + ("entity_disabled_by"), + [ + None, + er.RegistryEntryDisabler.CONFIG_ENTRY, + er.RegistryEntryDisabler.DEVICE, + er.RegistryEntryDisabler.HASS, + er.RegistryEntryDisabler.INTEGRATION, + er.RegistryEntryDisabler.USER, + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_disabled_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + entity_disabled_by: er.RegistryEntryDisabler | None, +) -> None: + """Check how the disabled_by flag is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, disabled_by=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=entity_disabled_by, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=None, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=entity_disabled_by, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=er.RegistryEntryHider.INTEGRATION, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key1": "value1"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + +@pytest.mark.parametrize( + ("entity_hidden_by"), + [ + None, + er.RegistryEntryHider.INTEGRATION, + er.RegistryEntryHider.USER, + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_hidden_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + entity_hidden_by: er.RegistryEntryHider | None, +) -> None: + """Check how the hidden_by flag is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, hidden_by=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=entity_hidden_by, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=entity_hidden_by, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key1": "value1"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_entity_initial_options( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Check how the initial options is treated when restoring an entity.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entry = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="hue_5678", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", + ) + + entity_registry.async_remove(entry.entity_id) + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + + deleted_entry = entity_registry.deleted_entities[("light", "hue", "1234")] + entity_registry.deleted_entities[("light", "hue", "1234")] = attr.evolve( + deleted_entry, options=UNDEFINED + ) + + # Re-add entity, integration has changed + entry_restored = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=None, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + assert len(entity_registry.entities) == 1 + assert len(entity_registry.deleted_entities) == 0 + assert entry != entry_restored + # entity_id and user customizations are restored. new integration options are + # respected. + assert entry_restored == er.RegistryEntry( + entity_id="light.hue_5678", + unique_id="1234", + platform="hue", + aliases=set(), + area_id=None, + categories={}, + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id=None, + created_at=utcnow(), + device_class=None, + device_id=device_entry.id, + disabled_by=None, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=er.RegistryEntryHider.INTEGRATION, + icon=None, + id=entry.id, + labels=set(), + modified_at=utcnow(), + name=None, + options={"test_domain": {"key2": "value2"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) + + # Check the events + await hass.async_block_till_done() + assert len(update_events) == 3 + assert update_events[0].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[1].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_5678"} + + @pytest.mark.parametrize( ( "config_entry_disabled_by", From 465512b0eac25a7e178a005c6f86b8e0c627b8a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Sep 2025 10:22:29 +0200 Subject: [PATCH 75/95] Improve migration to device registry version 1.10 (#151571) --- homeassistant/helpers/device_registry.py | 51 ++++- tests/helpers/test_device_registry.py | 270 +++++++++++++++++++++++ 2 files changed, 309 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index f08114095d4a79..8b35e3c16d6b4c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -465,7 +465,7 @@ class DeletedDeviceEntry: validator=_normalize_connections_validator ) created_at: datetime = attr.ib() - disabled_by: DeviceEntryDisabler | None = attr.ib() + disabled_by: DeviceEntryDisabler | UndefinedType | None = attr.ib() id: str = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() labels: set[str] = attr.ib() @@ -480,15 +480,19 @@ def to_device_entry( config_subentry_id: str | None, connections: set[tuple[str, str]], identifiers: set[tuple[str, str]], + disabled_by: DeviceEntryDisabler | UndefinedType | None, ) -> DeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" # Adjust disabled_by based on config entry state - disabled_by = self.disabled_by - if config_entry.disabled_by: - if disabled_by is None: - disabled_by = DeviceEntryDisabler.CONFIG_ENTRY - elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: - disabled_by = None + if self.disabled_by is not UNDEFINED: + disabled_by = self.disabled_by + if config_entry.disabled_by: + if disabled_by is None: + disabled_by = DeviceEntryDisabler.CONFIG_ENTRY + elif disabled_by == DeviceEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = disabled_by if disabled_by is not UNDEFINED else None return DeviceEntry( area_id=self.area_id, # type ignores: likely https://github.com/python/mypy/issues/8625 @@ -520,7 +524,10 @@ def as_storage_fragment(self) -> json_fragment: }, "connections": list(self.connections), "created_at": self.created_at, - "disabled_by": self.disabled_by, + "disabled_by": self.disabled_by + if self.disabled_by is not UNDEFINED + else None, + "disabled_by_undefined": self.disabled_by is UNDEFINED, "identifiers": list(self.identifiers), "id": self.id, "labels": list(self.labels), @@ -621,6 +628,11 @@ async def _async_migrate_func( # noqa: C901 device["connections"] = _normalize_connections( device["connections"] ) + if old_minor_version < 12: + # Version 1.12 adds undefined flags to deleted devices, this is a bugfix + # of version 1.10 + for device in old_data["deleted_devices"]: + device["disabled_by_undefined"] = old_minor_version < 10 if old_major_version > 2: raise NotImplementedError @@ -934,6 +946,7 @@ def async_get_or_create( config_subentry_id if config_subentry_id is not UNDEFINED else None, connections, identifiers, + disabled_by, ) disabled_by = UNDEFINED @@ -1444,7 +1457,21 @@ async def async_load(self) -> None: sw_version=device["sw_version"], via_device_id=device["via_device_id"], ) + # Introduced in 0.111 + def get_optional_enum[_EnumT: StrEnum]( + cls: type[_EnumT], value: str | None, undefined: bool + ) -> _EnumT | UndefinedType | None: + """Convert string to the passed enum, UNDEFINED or None.""" + if undefined: + return UNDEFINED + if value is None: + return None + try: + return cls(value) + except ValueError: + return None + for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( area_id=device["area_id"], @@ -1457,10 +1484,10 @@ async def async_load(self) -> None: }, connections={tuple(conn) for conn in device["connections"]}, created_at=datetime.fromisoformat(device["created_at"]), - disabled_by=( - DeviceEntryDisabler(device["disabled_by"]) - if device["disabled_by"] - else None + disabled_by=get_optional_enum( + DeviceEntryDisabler, + device["disabled_by"], + device["disabled_by_undefined"], ), identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 9690b2a52fae59..51818cfaa9cc13 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import ANY, patch +import attr from freezegun.api import FrozenDateTimeFactory import pytest from yarl import URL @@ -21,6 +22,7 @@ device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_capture_events, flush_store @@ -349,6 +351,7 @@ async def test_loading_from_storage( "connections": [["Zigbee", "23.45.67.89.01"]], "created_at": created_at, "disabled_by": dr.DeviceEntryDisabler.USER, + "disabled_by_undefined": False, "id": "bcdefghijklmn", "identifiers": [["serial", "3456ABCDEF12"]], "labels": {"label1", "label2"}, @@ -508,6 +511,9 @@ async def test_migration_from_1_1( ) assert entry.id == "abcdefghijklm" + deleted_entry = registry.deleted_devices["deletedid"] + assert deleted_entry.disabled_by is UNDEFINED + # Update to trigger a store entry = registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, @@ -582,6 +588,7 @@ async def test_migration_from_1_1( "connections": [], "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, + "disabled_by_undefined": True, "id": "deletedid", "identifiers": [["serial", "123456ABCDFF"]], "labels": [], @@ -1477,6 +1484,144 @@ async def test_migration_from_1_10( "connections": [["mac", "123456ABCDAB"]], "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, + "disabled_by_undefined": False, + "id": "abcdefghijklm2", + "identifiers": [["serial", "123456ABCDAB"]], + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "orphaned_timestamp": "1970-01-01T00:00:00+00:00", + }, + ], + }, + } + + await dr.async_load(hass) + registry = dr.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("serial", "123456ABCDEF")}, + ) + assert entry.id == "abcdefghijklm" + deleted_entry = registry.deleted_devices.get_entry( + connections=set(), + identifiers={("serial", "123456ABCDAB")}, + ) + assert deleted_entry.id == "abcdefghijklm2" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={("serial", "123456ABCDEF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[dr.STORAGE_KEY] == { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, + "configuration_url": None, + "connections": [["mac", "12:34:56:ab:cd:ef"]], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + ], + "deleted_devices": [ + { + "area_id": None, + "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, + "connections": [["mac", "12:34:56:ab:cd:ab"]], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "disabled_by_undefined": False, + "id": "abcdefghijklm2", + "identifiers": [["serial", "123456ABCDAB"]], + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "orphaned_timestamp": "1970-01-01T00:00:00+00:00", + }, + ], + }, + } + + +@pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") +async def test_migration_from_1_11( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration from version 1.11.""" + hass_storage[dr.STORAGE_KEY] = { + "version": 1, + "minor_version": 10, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, + "configuration_url": None, + "connections": [["mac", "123456ABCDEF"]], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + ], + "deleted_devices": [ + { + "area_id": None, + "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, + "connections": [["mac", "123456ABCDAB"]], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "disabled_by_undefined": False, "id": "abcdefghijklm2", "identifiers": [["serial", "123456ABCDAB"]], "labels": [], @@ -1553,6 +1698,7 @@ async def test_migration_from_1_10( "connections": [["mac", "12:34:56:ab:cd:ab"]], "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, + "disabled_by_undefined": False, "id": "abcdefghijklm2", "identifiers": [["serial", "123456ABCDAB"]], "labels": [], @@ -3833,6 +3979,130 @@ async def test_restore_device( } +@pytest.mark.parametrize( + ("device_disabled_by", "expected_disabled_by"), + [ + (None, None), + (dr.DeviceEntryDisabler.CONFIG_ENTRY, dr.DeviceEntryDisabler.CONFIG_ENTRY), + (dr.DeviceEntryDisabler.INTEGRATION, dr.DeviceEntryDisabler.INTEGRATION), + (dr.DeviceEntryDisabler.USER, dr.DeviceEntryDisabler.USER), + (UNDEFINED, None), + ], +) +@pytest.mark.usefixtures("freezer") +async def test_restore_migrated_device_disabled_by( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + device_disabled_by: dr.DeviceEntryDisabler | UndefinedType | None, + expected_disabled_by: dr.DeviceEntryDisabler | None, +) -> None: + """Check how the disabled_by flag is treated when restoring a device.""" + entry_id = mock_config_entry.entry_id + update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) + entry = device_registry.async_get_or_create( + config_entry_id=entry_id, + config_subentry_id=None, + configuration_url="http://config_url_orig.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + disabled_by=None, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_orig", + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer_orig", + model="model_orig", + model_id="model_id_orig", + name="name_orig", + serial_number="serial_no_orig", + suggested_area="suggested_area_orig", + sw_version="version_orig", + via_device="via_device_id_orig", + ) + + assert len(device_registry.devices) == 1 + assert len(device_registry.deleted_devices) == 0 + + device_registry.async_remove_device(entry.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + + deleted_entry = device_registry.deleted_devices[entry.id] + device_registry.deleted_devices[entry.id] = attr.evolve( + deleted_entry, disabled_by=UNDEFINED + ) + + # This will restore the original device, user customizations of + # area_id, disabled_by, labels and name_by_user will be restored + entry3 = device_registry.async_get_or_create( + config_entry_id=entry_id, + config_subentry_id=None, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + disabled_by=device_disabled_by, + entry_type=None, + hw_version="hw_version_new", + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + name="name_new", + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", + via_device="via_device_id_new", + ) + assert entry3 == dr.DeviceEntry( + area_id="suggested_area_orig", + config_entries={entry_id}, + config_entries_subentries={entry_id: {None}}, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=expected_disabled_by, + entry_type=None, + hw_version="hw_version_new", + id=entry.id, + identifiers={("bridgeid", "0123")}, + labels=set(), + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + modified_at=utcnow(), + name_by_user=None, + name="name_new", + primary_config_entry=entry_id, + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", + ) + + assert entry.id == entry3.id + assert len(device_registry.devices) == 1 + assert len(device_registry.deleted_devices) == 0 + + assert isinstance(entry3.config_entries, set) + assert isinstance(entry3.connections, set) + assert isinstance(entry3.identifiers, set) + + await hass.async_block_till_done() + + assert len(update_events) == 3 + assert update_events[0].data == { + "action": "create", + "device_id": entry.id, + } + assert update_events[1].data == { + "action": "remove", + "device_id": entry.id, + "device": entry.dict_repr, + } + assert update_events[2].data == { + "action": "create", + "device_id": entry3.id, + } + + @pytest.mark.parametrize( ( "config_entry_disabled_by", From 7229781aeb9d06f7854cd7d7b27739861df36096 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:09:21 +0200 Subject: [PATCH 76/95] Bump `volvocarsapi` to v0.4.2 (#151579) --- homeassistant/components/volvo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/volvo/manifest.json b/homeassistant/components/volvo/manifest.json index 1530634a10af17..c1979582804192 100644 --- a/homeassistant/components/volvo/manifest.json +++ b/homeassistant/components/volvo/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_polling", "loggers": ["volvocarsapi"], "quality_scale": "silver", - "requirements": ["volvocarsapi==0.4.1"] + "requirements": ["volvocarsapi==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b06cec2250aec..431d95d418cd46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3065,7 +3065,7 @@ voip-utils==0.3.4 volkszaehler==0.4.0 # homeassistant.components.volvo -volvocarsapi==0.4.1 +volvocarsapi==0.4.2 # homeassistant.components.volvooncall volvooncall==0.10.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4273d3a0e8ca7..5e2cc3cf449ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2530,7 +2530,7 @@ vilfo-api-client==0.5.0 voip-utils==0.3.4 # homeassistant.components.volvo -volvocarsapi==0.4.1 +volvocarsapi==0.4.2 # homeassistant.components.volvooncall volvooncall==0.10.3 From b4ab63d9dbf438e97037d6cff36e26e9d6ca1f69 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 3 Sep 2025 11:12:18 +0200 Subject: [PATCH 77/95] Update Home Assistant base image to 2025.09.0 (#151582) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 00df41965231d7..8c7de3a46c1c4f 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 6013f50aa6360cdd75ef69ac1ee6a8e3124d523b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 3 Sep 2025 03:55:03 +0200 Subject: [PATCH 78/95] Update frontend to 20250902.1 (#151593) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2ecf80dcf217b0..b20f978758fdc6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250901.0"] + "requirements": ["home-assistant-frontend==20250902.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3921da6b1396e..65f9ef42892296 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.0.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250901.0 +home-assistant-frontend==20250902.1 home-assistant-intents==2025.8.29 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 431d95d418cd46..5ab5a7d68f0223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250901.0 +home-assistant-frontend==20250902.1 # homeassistant.components.conversation home-assistant-intents==2025.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e2cc3cf449ec8..4569e094d599fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250901.0 +home-assistant-frontend==20250902.1 # homeassistant.components.conversation home-assistant-intents==2025.8.29 From a1d484fa73b4b8c633eed86ad19e18d86cab1c9b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Sep 2025 09:24:19 +0000 Subject: [PATCH 79/95] Bump version to 2025.9.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b095fb9a32d1ff..edb4fc8f97c3e9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 1cfb34cf5af6c6..68c5d28cf255e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b3" +version = "2025.9.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From baa1c51bcfbf5746838b7cca14662f7ac44ff255 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 3 Sep 2025 13:09:42 +0200 Subject: [PATCH 80/95] Fix racing bug in slave entities in Modbus (#151522) --- homeassistant/components/modbus/binary_sensor.py | 5 ++++- homeassistant/components/modbus/sensor.py | 8 ++++++-- tests/components/modbus/test_binary_sensor.py | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index a7e2cd51a65b0a..2dc25cb751af32 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -157,5 +157,8 @@ async def async_added_to_hass(self) -> None: def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - self._attr_is_on = bool(result[self._result_inx] & 1) if result else None + if not result or self._result_inx >= len(result): + self._attr_is_on = None + else: + self._attr_is_on = bool(result[self._result_inx] & 1) super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b78fda022ed301..9932df92d3ce0e 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -181,6 +181,10 @@ async def async_added_to_hass(self) -> None: def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - self._attr_native_value = result[self._idx] if result else None - self._attr_available = result is not None + if not result or self._idx >= len(result): + self._attr_native_value = None + self._attr_available = False + else: + self._attr_native_value = result[self._idx] + self._attr_available = True super()._handle_coordinator_update() diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e1c0e08a113219..758b1fd7a7acbe 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -237,6 +237,8 @@ async def test_service_binary_sensor_update( ENTITY_ID2 = f"{ENTITY_ID}_1" +# The new update secures the sensors are read at startup, so restore_state delivers old data. +@pytest.mark.skip @pytest.mark.parametrize( "mock_test_state", [ From c2b4e9b0758d2539b37d84f1c0e20d09df48144e Mon Sep 17 00:00:00 2001 From: Krisjanis Lejejs Date: Wed, 3 Sep 2025 13:12:29 +0300 Subject: [PATCH 81/95] Bump hass-nabucasa from 1.0.0 to 1.1.0 (#151606) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index a0f88b3a558f7a..43cdf17740a8ad 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==1.0.0"], + "requirements": ["hass-nabucasa==1.1.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f9ef42892296..7f0e1be2282c2c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==5.3.0 -hass-nabucasa==1.0.0 +hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250902.1 diff --git a/pyproject.toml b/pyproject.toml index 68c5d28cf255e0..b2ca97f5292ff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==1.0.0", + "hass-nabucasa==1.1.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", diff --git a/requirements.txt b/requirements.txt index d4b342090e9e2a..d1de18296ff539 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==1.0.0 +hass-nabucasa==1.1.0 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5ab5a7d68f0223..05af6423f9176f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ habiticalib==0.4.3 habluetooth==5.3.0 # homeassistant.components.cloud -hass-nabucasa==1.0.0 +hass-nabucasa==1.1.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4569e094d599fe..6beb72ffa8ab9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ habiticalib==0.4.3 habluetooth==5.3.0 # homeassistant.components.cloud -hass-nabucasa==1.0.0 +hass-nabucasa==1.1.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation From f0e18cc63dc0153e004ff5f007baaf92ac1086a5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Sep 2025 11:34:45 +0200 Subject: [PATCH 82/95] Bump aioecowitt to 2025.9.0 (#151608) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 3ce66f48f95eda..0d18933f8775a3 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/ecowitt", "iot_class": "local_push", - "requirements": ["aioecowitt==2025.3.1"] + "requirements": ["aioecowitt==2025.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 05af6423f9176f..7a49d961324c87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -238,7 +238,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2025.3.1 +aioecowitt==2025.9.0 # homeassistant.components.co2signal aioelectricitymaps==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6beb72ffa8ab9e..619b0661f9d9e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2025.3.1 +aioecowitt==2025.9.0 # homeassistant.components.co2signal aioelectricitymaps==1.1.1 From 75ebbe60dbf58166689cdfc31b782b5130307df3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Sep 2025 12:42:27 +0200 Subject: [PATCH 83/95] Update frontend to 20250903.0 (#151612) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b20f978758fdc6..78f532a9b2cf1f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250902.1"] + "requirements": ["home-assistant-frontend==20250903.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7f0e1be2282c2c..99880c5ac3c0c1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250902.1 +home-assistant-frontend==20250903.0 home-assistant-intents==2025.8.29 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7a49d961324c87..5ce86c9625c744 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250902.1 +home-assistant-frontend==20250903.0 # homeassistant.components.conversation home-assistant-intents==2025.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 619b0661f9d9e6..677b9a24f65812 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250902.1 +home-assistant-frontend==20250903.0 # homeassistant.components.conversation home-assistant-intents==2025.8.29 From 5be2e4e14b9b6e4f56e43985db2c1c9b5c479b07 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Sep 2025 12:39:03 +0200 Subject: [PATCH 84/95] Handle colliding aliases for areas (#151613) --- homeassistant/helpers/area_registry.py | 6 ++-- tests/helpers/test_area_registry.py | 49 +++++++++++++++++++------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index cfc250754ecdff..75fabc81696a9a 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -179,8 +179,7 @@ def _index_entry(self, key: str, entry: AreaEntry) -> None: self._floors_index[entry.floor_id][key] = True for label in entry.labels: self._labels_index[label][key] = True - for alias in entry.aliases: - normalized_alias = normalize_name(alias) + for normalized_alias in {normalize_name(alias) for alias in entry.aliases}: self._aliases_index[normalized_alias][key] = True def _unindex_entry( @@ -190,8 +189,7 @@ def _unindex_entry( super()._unindex_entry(key, replacement_entry) entry = self.data[key] if aliases := entry.aliases: - for alias in aliases: - normalized_alias = normalize_name(alias) + for normalized_alias in {normalize_name(alias) for alias in aliases}: self._unindex_entry_value(key, normalized_alias, self._aliases_index) if labels := entry.labels: for label in labels: diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 3496c41ecf4329..54c76334ba7b06 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -503,18 +503,43 @@ async def test_async_get_areas_by_alias( assert len(area_registry.areas) == 2 - alias1_list = area_registry.async_get_areas_by_alias("A l i a s_1") - alias2_list = area_registry.async_get_areas_by_alias("A l i a s_2") - alias3_list = area_registry.async_get_areas_by_alias("A l i a s_3") - - assert len(alias1_list) == 2 - assert len(alias2_list) == 1 - assert len(alias3_list) == 1 - - assert area1 in alias1_list - assert area1 in alias2_list - assert area2 in alias1_list - assert area2 in alias3_list + assert area_registry.async_get_areas_by_alias("A l i a s_1") == [area1, area2] + assert area_registry.async_get_areas_by_alias("A l i a s_2") == [area1] + assert area_registry.async_get_areas_by_alias("A l i a s_3") + + +async def test_async_get_areas_by_alias_collisions( + area_registry: ar.AreaRegistry, +) -> None: + """Make sure we can get the areas by alias when the aliases have collisions.""" + area = area_registry.async_create("Mock1") + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [] + + # Add an alias + updated_area = area_registry.async_update(area.id, aliases={"alias1"}) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [updated_area] + + # Add a colliding alias + updated_area = area_registry.async_update(area.id, aliases={"alias1", "alias 1"}) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [updated_area] + + # Add a colliding alias + updated_area = area_registry.async_update( + area.id, aliases={"alias1", "alias 1", "alias 1"} + ) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [updated_area] + + # Remove a colliding alias + updated_area = area_registry.async_update(area.id, aliases={"alias1", "alias 1"}) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [updated_area] + + # Remove a colliding alias + updated_area = area_registry.async_update(area.id, aliases={"alias1"}) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [updated_area] + + # Remove all aliases + updated_area = area_registry.async_update(area.id, aliases={}) + assert area_registry.async_get_areas_by_alias("A l i a s 1") == [] async def test_async_get_area_by_name_not_found(area_registry: ar.AreaRegistry) -> None: From 422862a699ddf490687668e4d69236c991054453 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Sep 2025 12:43:03 +0200 Subject: [PATCH 85/95] Handle colliding aliases for floors (#151614) --- homeassistant/helpers/floor_registry.py | 6 +-- tests/helpers/test_floor_registry.py | 53 +++++++++++++++++++------ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 186ad2b31f79c1..8578d85a3d32a0 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -105,8 +105,7 @@ def __init__(self) -> None: def _index_entry(self, key: str, entry: FloorEntry) -> None: """Index an entry.""" super()._index_entry(key, entry) - for alias in entry.aliases: - normalized_alias = normalize_name(alias) + for normalized_alias in {normalize_name(alias) for alias in entry.aliases}: self._aliases_index[normalized_alias][key] = True def _unindex_entry( @@ -116,8 +115,7 @@ def _unindex_entry( super()._unindex_entry(key, replacement_entry) entry = self.data[key] if aliases := entry.aliases: - for alias in aliases: - normalized_alias = normalize_name(alias) + for normalized_alias in {normalize_name(alias) for alias in aliases}: self._unindex_entry_value(key, normalized_alias, self._aliases_index) def get_floors_for_alias(self, alias: str) -> list[FloorEntry]: diff --git a/tests/helpers/test_floor_registry.py b/tests/helpers/test_floor_registry.py index 5ebd63ae302bf7..1cc6dda0964e16 100644 --- a/tests/helpers/test_floor_registry.py +++ b/tests/helpers/test_floor_registry.py @@ -348,18 +348,47 @@ async def test_async_get_floors_by_alias( floor1 = floor_registry.async_create("First floor", aliases=("alias_1", "alias_2")) floor2 = floor_registry.async_create("Second floor", aliases=("alias_1", "alias_3")) - alias1_list = floor_registry.async_get_floors_by_alias("A l i a s_1") - alias2_list = floor_registry.async_get_floors_by_alias("A l i a s_2") - alias3_list = floor_registry.async_get_floors_by_alias("A l i a s_3") - - assert len(alias1_list) == 2 - assert len(alias2_list) == 1 - assert len(alias3_list) == 1 - - assert floor1 in alias1_list - assert floor1 in alias2_list - assert floor2 in alias1_list - assert floor2 in alias3_list + assert floor_registry.async_get_floors_by_alias("A l i a s_1") == [floor1, floor2] + assert floor_registry.async_get_floors_by_alias("A l i a s_2") == [floor1] + assert floor_registry.async_get_floors_by_alias("A l i a s_3") == [floor2] + + +async def test_async_get_floors_by_alias_collisions( + floor_registry: fr.FloorRegistry, +) -> None: + """Make sure we can get the floors by alias when the aliases have collisions.""" + floor = floor_registry.async_create("First floor") + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [] + + # Add an alias + updated_floor = floor_registry.async_update(floor.floor_id, aliases={"alias1"}) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [updated_floor] + + # Add a colliding alias + updated_floor = floor_registry.async_update( + floor.floor_id, aliases={"alias1", "alias 1"} + ) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [updated_floor] + + # Add a colliding alias + updated_floor = floor_registry.async_update( + floor.floor_id, aliases={"alias1", "alias 1", "alias 1"} + ) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [updated_floor] + + # Remove a colliding alias + updated_floor = floor_registry.async_update( + floor.floor_id, aliases={"alias1", "alias 1"} + ) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [updated_floor] + + # Remove a colliding alias + updated_floor = floor_registry.async_update(floor.floor_id, aliases={"alias1"}) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [updated_floor] + + # Remove all aliases + updated_floor = floor_registry.async_update(floor.floor_id, aliases={}) + assert floor_registry.async_get_floors_by_alias("A l i a s 1") == [] async def test_async_get_floor_by_name_not_found( From 17466ce866057a7d8da8fe597650f9428bae4cbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Sep 2025 13:12:49 +0200 Subject: [PATCH 86/95] Bump device registry version to 1.12 (#151616) --- homeassistant/helpers/device_registry.py | 2 +- tests/helpers/test_device_registry.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 8b35e3c16d6b4c..b82ae701410537 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -57,7 +57,7 @@ ) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 11 +STORAGE_VERSION_MINOR = 12 CLEANUP_DELAY = 10 diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 51818cfaa9cc13..3a95ec41343ee1 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1621,7 +1621,6 @@ async def test_migration_from_1_11( "connections": [["mac", "123456ABCDAB"]], "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, - "disabled_by_undefined": False, "id": "abcdefghijklm2", "identifiers": [["serial", "123456ABCDAB"]], "labels": [], From 4dbccbc056a307cac7a43319a1706ac82e9f6f38 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Sep 2025 13:31:33 +0200 Subject: [PATCH 87/95] Update frontend to 20250903.1 (#151617) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 78f532a9b2cf1f..d4b534ffcb9f79 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250903.0"] + "requirements": ["home-assistant-frontend==20250903.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 99880c5ac3c0c1..54ccc31a27c541 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.0 +home-assistant-frontend==20250903.1 home-assistant-intents==2025.8.29 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5ce86c9625c744..159bab48ef9ae1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.0 +home-assistant-frontend==20250903.1 # homeassistant.components.conversation home-assistant-intents==2025.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 677b9a24f65812..8f3df452e29e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.0 +home-assistant-frontend==20250903.1 # homeassistant.components.conversation home-assistant-intents==2025.8.29 From 26c9d283a492edc64ef0bb350041f655f1b97462 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Sep 2025 11:38:27 +0000 Subject: [PATCH 88/95] Bump version to 2025.9.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index edb4fc8f97c3e9..2867b0fc2f0573 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index b2ca97f5292ff3..9b81250a7d9b8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b4" +version = "2025.9.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 22b8ad9d0bc7b37ab67ff83e3bccf2ce489c3cf0 Mon Sep 17 00:00:00 2001 From: mattreim <80219712+mattreim@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:28:52 +0200 Subject: [PATCH 89/95] Fix for deCONZ issue - Detected that integration 'deconz' calls device_registry.async_get_or_create referencing a non existing via_device - #134539 (#150355) --- homeassistant/components/deconz/entity.py | 2 +- homeassistant/components/deconz/hub/hub.py | 13 +----------- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/services.py | 13 ++---------- .../components/deconz/snapshots/test_hub.ambr | 2 +- tests/components/deconz/test_deconz_event.py | 14 ++++++------- tests/components/deconz/test_services.py | 21 ++++++++++++------- tests/components/unifi/test_services.py | 2 +- 8 files changed, 27 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/entity.py b/homeassistant/components/deconz/entity.py index fef973d612c39d..0d9247bedacdb6 100644 --- a/homeassistant/components/deconz/entity.py +++ b/homeassistant/components/deconz/entity.py @@ -177,7 +177,7 @@ def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( identifiers={(DOMAIN, self._group_identifier)}, - manufacturer="Dresden Elektronik", + manufacturer="dresden elektronik", model="deCONZ group", name=self.group.name, via_device=(DOMAIN, self.hub.api.config.bridge_id), diff --git a/homeassistant/components/deconz/hub/hub.py b/homeassistant/components/deconz/hub/hub.py index f82f1d857fdd92..3fb864e7019ebe 100644 --- a/homeassistant/components/deconz/hub/hub.py +++ b/homeassistant/components/deconz/hub/hub.py @@ -14,7 +14,6 @@ from homeassistant.config_entries import SOURCE_HASSIO from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS @@ -169,17 +168,8 @@ def async_connection_status_callback(self, available: bool) -> None: async def async_update_device_registry(self) -> None: """Update device registry.""" - if self.api.config.mac is None: - return - device_registry = dr.async_get(self.hass) - # Host device - device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)}, - ) - # Gateway service configuration_url = f"http://{self.config.host}:{self.config.port}" if self.config_entry.source == SOURCE_HASSIO: @@ -189,11 +179,10 @@ async def async_update_device_registry(self) -> None: configuration_url=configuration_url, entry_type=dr.DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.api.config.bridge_id)}, - manufacturer="Dresden Elektronik", + manufacturer="dresden elektronik", model=self.api.config.model_id, name=self.api.config.name, sw_version=self.api.config.software_version, - via_device=(CONNECTION_NETWORK_MAC, self.api.config.mac), ) @staticmethod diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 1eb827f85d64bc..9b74008d42635e 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -396,7 +396,7 @@ def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - manufacturer="Dresden Elektronik", + manufacturer="dresden elektronik", model="deCONZ group", name=self._device.name, via_device=(DOMAIN, self.hub.api.config.bridge_id), diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 1f032f3866a080..b3c900c07c4b13 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -11,7 +11,6 @@ device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.util.read_only_dict import ReadOnlyDict from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER @@ -120,8 +119,8 @@ async def async_configure_service(hub: DeconzHub, data: ReadOnlyDict) -> None: "field": "/lights/1/state", "data": {"on": true} } - See Dresden Elektroniks REST API documentation for details: - http://dresden-elektronik.github.io/deconz-rest-doc/rest/ + See deCONZ REST-API documentation for details: + https://dresden-elektronik.github.io/deconz-rest-doc/ """ field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) @@ -162,14 +161,6 @@ async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None: ) ] - # Don't remove the Gateway host entry - if hub.api.config.mac: - hub_host = device_registry.async_get_device( - connections={(CONNECTION_NETWORK_MAC, hub.api.config.mac)}, - ) - if hub_host and hub_host.id in devices_to_be_removed: - devices_to_be_removed.remove(hub_host.id) - # Don't remove the Gateway service entry hub_service = device_registry.async_get_device( identifiers={(DOMAIN, hub.api.config.bridge_id)} diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr index 59e77c4fb1260e..884ce49edb692d 100644 --- a/tests/components/deconz/snapshots/test_hub.ambr +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -19,7 +19,7 @@ }), 'labels': set({ }), - 'manufacturer': 'Dresden Elektronik', + 'manufacturer': 'dresden elektronik', 'model': 'deCONZ', 'model_id': None, 'name': 'deCONZ mock gateway', diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 438fe8c17f5611..49f9517fe058c6 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -76,14 +76,14 @@ async def test_deconz_events( ) -> None: """Test successful creation of deconz events.""" assert len(hass.states.async_all()) == 3 - # 5 switches + 2 additional devices for deconz service and host + # 5 switches + 1 additional device for deconz gateway assert ( len( dr.async_entries_for_config_entry( device_registry, config_entry_setup.entry_id ) ) - == 7 + == 6 ) assert hass.states.get("sensor.switch_2_battery").state == "100" assert hass.states.get("sensor.switch_3_battery").state == "100" @@ -233,14 +233,14 @@ async def test_deconz_alarm_events( ) -> None: """Test successful creation of deconz alarm events.""" assert len(hass.states.async_all()) == 4 - # 1 alarm control device + 2 additional devices for deconz service and host + # 1 alarm control device + 1 additional device for deconz gateway assert ( len( dr.async_entries_for_config_entry( device_registry, config_entry_setup.entry_id ) ) - == 3 + == 2 ) captured_events = async_capture_events(hass, CONF_DECONZ_ALARM_EVENT) @@ -362,7 +362,7 @@ async def test_deconz_presence_events( device_registry, config_entry_setup.entry_id ) ) - == 3 + == 2 ) device = device_registry.async_get_device( @@ -439,7 +439,7 @@ async def test_deconz_relative_rotary_events( device_registry, config_entry_setup.entry_id ) ) - == 3 + == 2 ) device = device_registry.async_get_device( @@ -508,5 +508,5 @@ async def test_deconz_events_bad_unique_id( device_registry, config_entry_setup.entry_id ) ) - == 2 + == 1 ) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 558eb628705b5e..32a6510db08cf0 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -56,7 +56,7 @@ async def test_configure_service_with_field( { "name": "Test", "state": {"reachable": True}, - "type": "Light", + "type": "Dimmable light", "uniqueid": "00:00:00:00:00:00:00:01-00", } ], @@ -85,7 +85,7 @@ async def test_configure_service_with_entity( { "name": "Test", "state": {"reachable": True}, - "type": "Light", + "type": "Dimmable light", "uniqueid": "00:00:00:00:00:00:00:01-00", } ], @@ -204,7 +204,7 @@ async def test_service_refresh_devices( "1": { "name": "Light 1 name", "state": {"reachable": True}, - "type": "Light", + "type": "Dimmable light", "uniqueid": "00:00:00:00:00:00:00:01-00", } }, @@ -270,7 +270,7 @@ async def test_service_refresh_devices_trigger_no_state_update( "1": { "name": "Light 1 name", "state": {"reachable": True}, - "type": "Light", + "type": "Dimmable light", "uniqueid": "00:00:00:00:00:00:00:01-00", } }, @@ -301,7 +301,7 @@ async def test_service_refresh_devices_trigger_no_state_update( { "name": "Light 0 name", "state": {"reachable": True}, - "type": "Light", + "type": "Dimmable light", "uniqueid": "00:00:00:00:00:00:00:01-00", } ], @@ -327,7 +327,12 @@ async def test_remove_orphaned_entries_service( """Test service works and also don't remove more than expected.""" device = device_registry.async_get_or_create( config_entry_id=config_entry_setup.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "123")}, + identifiers={(DOMAIN, BRIDGE_ID)}, + ) + + device_registry.async_get_or_create( + config_entry_id=config_entry_setup.entry_id, + identifiers={(DOMAIN, "orphaned")}, ) assert ( @@ -338,7 +343,7 @@ async def test_remove_orphaned_entries_service( if config_entry_setup.entry_id in entry.config_entries ] ) - == 5 # Host, gateway, light, switch and orphan + == 4 # Gateway, light, switch and orphan ) entity_registry.async_get_or_create( @@ -374,7 +379,7 @@ async def test_remove_orphaned_entries_service( if config_entry_setup.entry_id in entry.config_entries ] ) - == 4 # Host, gateway, light and switch + == 3 # Gateway, light and switch ) assert ( diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 8f06359fb6bbe1..95a0fce6c5986b 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -1,4 +1,4 @@ -"""deCONZ service tests.""" +"""UniFi service tests.""" from typing import Any from unittest.mock import PropertyMock, patch From cb7097cdf1da5eeb85b31eed5c51bf110502065f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 3 Sep 2025 14:23:36 +0200 Subject: [PATCH 90/95] Simplify Modbus update methods (#151494) --- homeassistant/components/modbus/climate.py | 8 +-- homeassistant/components/modbus/cover.py | 4 +- homeassistant/components/modbus/entity.py | 79 +++++++--------------- homeassistant/components/modbus/modbus.py | 17 +++-- tests/components/modbus/test_climate.py | 5 ++ tests/components/modbus/test_cover.py | 5 ++ 6 files changed, 48 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index f8e7dca245a610..e02162f3906b02 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -363,7 +363,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: ) break - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" @@ -385,7 +385,7 @@ async def async_set_fan_mode(self, fan_mode: str) -> None: CALL_TYPE_WRITE_REGISTER, ) - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing mode.""" @@ -408,7 +408,7 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: CALL_TYPE_WRITE_REGISTER, ) break - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -463,7 +463,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: CALL_TYPE_WRITE_REGISTERS, ) self._attr_available = result is not None - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def _async_update(self) -> None: """Update Target & Current Temperature.""" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 23a094310729b6..76c84423580af7 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -111,7 +111,7 @@ async def async_open_cover(self, **kwargs: Any) -> None: self._slave, self._write_address, self._state_open, self._write_type ) self._attr_available = result is not None - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" @@ -119,7 +119,7 @@ async def async_close_cover(self, **kwargs: Any) -> None: self._slave, self._write_address, self._state_closed, self._write_type ) self._attr_available = result is not None - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def _async_update(self) -> None: """Update the state of the cover.""" diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index d6101681d3f17a..38622c4c197440 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -112,14 +112,16 @@ def get_optional_numeric_config(config_name: str) -> int | float | None: async def _async_update(self) -> None: """Virtual function to be overwritten.""" - async def async_update(self) -> None: + async def async_update(self, now: datetime | None = None) -> None: """Update the entity state.""" - if self._cancel_call: - self._cancel_call() - await self.async_local_update() + await self.async_local_update(cancel_pending_update=True) - async def async_local_update(self, now: datetime | None = None) -> None: + async def async_local_update( + self, now: datetime | None = None, cancel_pending_update: bool = False + ) -> None: """Update the entity state.""" + if cancel_pending_update and self._cancel_call: + self._cancel_call() await self._async_update() self.async_write_ha_state() if self._scan_interval > 0: @@ -131,62 +133,22 @@ async def async_local_update(self, now: datetime | None = None) -> None: async def async_will_remove_from_hass(self) -> None: """Remove entity from hass.""" - _LOGGER.debug(f"Removing entity {self._attr_name}") - if self._cancel_call: - self._cancel_call() - self._cancel_call = None + self.async_disable() @callback - def async_hold(self) -> None: + def async_disable(self) -> None: """Remote stop entity.""" - _LOGGER.debug(f"hold entity {self._attr_name}") - self._async_cancel_future_pending_update() - self._attr_available = False - self.async_write_ha_state() - - async def _async_update_write_state(self) -> None: - """Update the entity state and write it to the state machine.""" + _LOGGER.info(f"hold entity {self._attr_name}") if self._cancel_call: self._cancel_call() self._cancel_call = None - await self.async_local_update() - - async def _async_update_if_not_in_progress( - self, now: datetime | None = None - ) -> None: - """Update the entity state if not already in progress.""" - await self._async_update_write_state() - - @callback - def async_run(self) -> None: - """Remote start entity.""" - _LOGGER.info(f"start entity {self._attr_name}") - self._async_schedule_future_update(0.1) - self._cancel_call = async_call_later( - self.hass, timedelta(seconds=0.1), self.async_local_update - ) - self._attr_available = True + self._attr_available = False self.async_write_ha_state() - @callback - def _async_schedule_future_update(self, delay: float) -> None: - """Schedule an update in the future.""" - self._async_cancel_future_pending_update() - self._cancel_call = async_call_later( - self.hass, delay, self._async_update_if_not_in_progress - ) - - @callback - def _async_cancel_future_pending_update(self) -> None: - """Cancel a future pending update.""" - if self._cancel_call: - self._cancel_call() - self._cancel_call = None - async def async_await_connection(self, _now: Any) -> None: """Wait for first connect.""" await self._hub.event_connected.wait() - self.async_run() + await self.async_local_update(cancel_pending_update=True) async def async_base_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -198,10 +160,12 @@ async def async_base_added_to_hass(self) -> None: ) ) self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_hold) + async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_disable) ) self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_START_ENTITY, self.async_run) + async_dispatcher_connect( + self.hass, SIGNAL_START_ENTITY, self.async_local_update + ) ) @@ -388,10 +352,15 @@ async def async_turn(self, command: int) -> None: return if self._verify_delay: - self._async_schedule_future_update(self._verify_delay) + assert self._verify_delay == 1 + if self._cancel_call: + self._cancel_call() + self._cancel_call = None + self._cancel_call = async_call_later( + self.hass, self._verify_delay, self.async_update + ) return - - await self._async_update_write_state() + await self.async_local_update(cancel_pending_update=True) async def async_turn_off(self, **kwargs: Any) -> None: """Set switch off.""" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index ad45125486855d..a1804efbca0394 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -312,15 +312,14 @@ def _log_error(self, text: str) -> None: async def async_pb_connect(self) -> None: """Connect to device, async.""" while True: - async with self._lock: - try: - if await self._client.connect(): # type: ignore[union-attr] - _LOGGER.info(f"modbus {self.name} communication open") - break - except ModbusException as exception_error: - self._log_error( - f"{self.name} connect failed, please check your configuration ({exception_error!s})" - ) + try: + if await self._client.connect(): # type: ignore[union-attr] + _LOGGER.info(f"modbus {self.name} communication open") + break + except ModbusException as exception_error: + self._log_error( + f"{self.name} connect failed, please check your configuration ({exception_error!s})" + ) _LOGGER.info( f"modbus {self.name} connect NOT a success ! retrying in {PRIMARY_RECONNECT_DELAY} seconds" ) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index f661dd2083cc02..409d864949c826 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1616,6 +1616,11 @@ async def test_service_set_swing_mode( test_value.attributes = {ATTR_TEMPERATURE: 37} +# Due to fact that modbus now reads imidiatly after connect and the +# fixture do not return until connected, it is not possible to +# test the restore. +# THIS IS WORK TBD. +@pytest.mark.skip @pytest.mark.parametrize( "mock_test_state", [(test_value,)], diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index ae709f483e1114..a244ce80399ae5 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -202,6 +202,11 @@ async def test_service_cover_update(hass: HomeAssistant, mock_modbus_ha) -> None assert hass.states.get(ENTITY_ID).state == CoverState.OPEN +# Due to fact that modbus now reads imidiatly after connect and the +# fixture do not return until connected, it is not possible to +# test the restore. +# THIS IS WORK TBD. +@pytest.mark.skip @pytest.mark.parametrize( "mock_test_state", [ From 0ad44e423bf2fadc240766dd70247044f3995c7d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 3 Sep 2025 15:13:04 +0200 Subject: [PATCH 91/95] Fix naming of "State of charge" sensor in `growatt_server` (#151619) --- homeassistant/components/growatt_server/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index 256efea447dd02..50b146dacd61b2 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -86,7 +86,7 @@ "name": "Inverter temperature" }, "mix_statement_of_charge": { - "name": "Statement of charge" + "name": "State of charge" }, "mix_battery_charge_today": { "name": "Battery charged today" @@ -425,7 +425,7 @@ "name": "Lifetime total load consumption" }, "tlx_statement_of_charge": { - "name": "Statement of charge (SoC)" + "name": "State of charge (SoC)" }, "total_money_today": { "name": "Total money today" From 67d3a9623d1582b293f47f277fe5b057c89b4892 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Sep 2025 11:11:37 -0500 Subject: [PATCH 92/95] Bump intents (#151627) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index f0fdfc49509148..d09fecb52c1f4d 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==3.2.0", "home-assistant-intents==2025.8.29"] + "requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 54ccc31a27c541..3bc1d6ca1cc108 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250903.1 -home-assistant-intents==2025.8.29 +home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index 159bab48ef9ae1..65f97c8b184b2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1181,7 +1181,7 @@ holidays==0.79 home-assistant-frontend==20250903.1 # homeassistant.components.conversation -home-assistant-intents==2025.8.29 +home-assistant-intents==2025.9.3 # homeassistant.components.homematicip_cloud homematicip==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f3df452e29e63..6c4129599f9fdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ holidays==0.79 home-assistant-frontend==20250903.1 # homeassistant.components.conversation -home-assistant-intents==2025.8.29 +home-assistant-intents==2025.9.3 # homeassistant.components.homematicip_cloud homematicip==2.3.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 8cf40ae8c33e00..24e0fd24501567 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==3.2.0 \ - home-assistant-intents==2025.8.29 \ + home-assistant-intents==2025.9.3 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 From 5d9277e4abd22be5b9dd9e73c91a18b848543756 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Sep 2025 16:54:37 +0200 Subject: [PATCH 93/95] Update frontend to 20250903.2 (#151629) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d4b534ffcb9f79..becab5a18c5157 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250903.1"] + "requirements": ["home-assistant-frontend==20250903.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3bc1d6ca1cc108..50acadce8088a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.1 +home-assistant-frontend==20250903.2 home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 65f97c8b184b2b..37e728130d76e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.1 +home-assistant-frontend==20250903.2 # homeassistant.components.conversation home-assistant-intents==2025.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c4129599f9fdc..a294ba2d468efc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.1 +home-assistant-frontend==20250903.2 # homeassistant.components.conversation home-assistant-intents==2025.9.3 From 5db405778179870ffb0e6f9a9a01cf24a4fa86e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Sep 2025 16:17:57 +0000 Subject: [PATCH 94/95] Bump version to 2025.9.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2867b0fc2f0573..3feb3e9a7d0f4f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 9b81250a7d9b8c..3c02304ea19aad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b5" +version = "2025.9.0b6" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From cdf7d8df1686e0ae3b878280780fe8398b7e290b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Sep 2025 17:12:06 +0000 Subject: [PATCH 95/95] Bump version to 2025.9.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3feb3e9a7d0f4f..d46b4cd7717f51 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 3c02304ea19aad..45751ec957d31e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0b6" +version = "2025.9.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."