ha-inlite

Home Assistant integration for in-lite
git clone https://git.stephank.nl/ha-inlite
Log | Files | Refs | README | LICENSE | ZIP

commit 4268ac4c08a7af653d4d74b8f5878ba3987b9cf2
parent ccb941e8649067c89758b8907a6a682673058f4d
Author: StΓ©phan Kochen <git@stephank.nl>
Date: Wed, 02 Sep 2026 20:37:59 +0200

Local association

diff --git a/README.md b/README.md index 4fa92bb16319d9803855e6aa892a6cd5209b4397..c4d902189fc7276d04b6c8edc0915a81ac7f3482 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ## Features - πŸ’‘ Turn lights on/off per zone through Home Assistant - πŸ“‘ Bluetooth Low Energy (BLE) mesh control β€” no cloud dependency for operation -- ☁️ One-time cloud login for initial pairing (retrieves encryption keys) +- πŸ” Local-only factory-reset onboarding; no in-lite account is used - πŸ”„ Automatic BLE discovery of the in-lite hub - πŸ” Reliable command delivery with retry and reconnect logic - πŸ”Œ Persistent BLE connection with idle disconnect to save resources @@ -30,7 +30,8 @@ - **Home Assistant** 2024.12.0 or newer - **Bluetooth adapter** β€” built-in, USB dongle, or [ESPHome Bluetooth Proxy](https://esphome.github.io/bluetooth-proxies/) - **in-lite SMART HUB-150** or **SMART HUB-75** with BLE gateway (device advertises as `inlitebt`) -- **in-lite cloud account** β€” needed once during setup to retrieve the encryption passphrase +- A **factory-reset** in-lite hub. During setup, Home Assistant makes the + selected hub blink, then creates its own local mesh key. ## Supported Devices @@ -79,10 +80,10 @@ ### Manual Setup 1. Go to **Settings** β†’ **Devices & Services** β†’ **Add Integration** 2. Search for **in-lite Outdoor Lighting** -3. Enter the email address associated with your in-lite cloud account -4. Check your email for a verification code and enter it -5. If you have multiple gardens, select the one to configure -6. The integration will connect to your hub and create light entities for each zone +3. Select the discovered in-lite gateway +4. Select a factory-reset hub and verify that it blinks +5. Confirm the blinking hub; Home Assistant creates a local-only mesh key +6. The integration reconnects and creates light entities for each zone ### Entities Created diff --git a/custom_components/inlite/config_flow.py b/custom_components/inlite/config_flow.py index e3045c89b73662d2a2f1bb8beb49ea77a5e286f0..36a95d13dde00b197060ddcfb20f568a7864d8bd 100644 --- a/custom_components/inlite/config_flow.py +++ b/custom_components/inlite/config_flow.py @@ -1,50 +1,25 @@ -"""Config flow for in-lite integration. - -Supports three entry points: -- User-initiated: email β†’ code β†’ select garden -- Bluetooth discovery: HA finds "inlitebt" β†’ user confirms β†’ email flow -- Reauth: re-run email/code flow to update credentials -- Options: configure scan interval and idle disconnect timeout -""" - +"""Local-only setup flow for factory-reset in-lite hubs.""" from __future__ import annotations import logging +import secrets from typing import Any import voluptuous as vol - +from bleak_retry_connector import BleakClientWithServiceCache, establish_connection +from homeassistant.components import bluetooth from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from inlite_ble.cloud import ( - ApiError, - AuthenticationError, - Garden, - async_login, - async_send_code, -) +from inlite_ble.hub import InliteHub +from inlite_ble.protocol import FACTORY_NETWORK_PASSPHRASE from .const import ( - CONF_GARDEN_ID, - CONF_GARDEN_NAME, - CONF_IDLE_DISCONNECT, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TRANSFORMERS, - DEFAULT_IDLE_DISCONNECT_SECONDS, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - MAX_IDLE_DISCONNECT_SECONDS, - MAX_SCAN_INTERVAL, - MIN_IDLE_DISCONNECT_SECONDS, + BLE_LOCAL_NAME, CONF_GARDEN_ID, CONF_GARDEN_NAME, CONF_IDLE_DISCONNECT, + CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TRANSFORMERS, + DEFAULT_IDLE_DISCONNECT_SECONDS, DEFAULT_SCAN_INTERVAL, DOMAIN, + MAX_IDLE_DISCONNECT_SECONDS, MAX_SCAN_INTERVAL, MIN_IDLE_DISCONNECT_SECONDS, MIN_SCAN_INTERVAL, ) @@ -52,242 +27,206 @@ _LOGGER = logging.getLogger(__name__) class InliteConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle the in-lite config flow.""" + """Onboard a factory-reset hub entirely over the local BLE mesh.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: - """Initialize.""" - self._email: str | None = None - self._gardens: list[Garden] = [] - self._discovery_info: BluetoothServiceInfoBleak | None = None - self._reauth_entry: ConfigEntry | None = None + self._service_info: BluetoothServiceInfoBleak | None = None + self._candidates: set[int] = set() + self._selected_id: int | None = None + self._garden_name = "in-lite" @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> InliteOptionsFlow: - """Get the options flow handler.""" + def async_get_options_flow(config_entry: Any) -> InliteOptionsFlow: return InliteOptionsFlow(config_entry) - # ------------------------------------------------------------------ - # Bluetooth discovery entry point - # ------------------------------------------------------------------ - async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak ) -> ConfigFlowResult: - """Handle Bluetooth discovery of an in-lite hub.""" + self._service_info = discovery_info await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() - - # The BLE device is a shared gateway β€” if ANY inlite entry exists, - # the gateway is already in use. Abort to suppress repeated discovery. - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - self._discovery_info = discovery_info return await self.async_step_bluetooth_confirm() - async def async_step_bluetooth_confirm( + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Confirm Bluetooth discovery, then start the cloud login flow.""" - if user_input is not None: - return await self.async_step_user() - - self._set_confirm_only() - return self.async_show_form(step_id="bluetooth_confirm") - - # ------------------------------------------------------------------ - # User-initiated flow: email β†’ code β†’ select garden - # ------------------------------------------------------------------ + for info in bluetooth.async_discovered_service_info(self.hass, connectable=True): + if info.name and info.name.lower() == BLE_LOCAL_NAME: + self._service_info = info + break + if self._service_info is None: + return self.async_abort(reason="no_hub_found") + await self.async_set_unique_id(self._service_info.address) + self._abort_if_unique_id_configured() + return await self.async_step_bluetooth_confirm() - async def async_step_user( + async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Step 1: User enters their in-lite email address.""" - errors: dict[str, str] = {} + if user_input is None: + self._set_confirm_only() + return self.async_show_form(step_id="bluetooth_confirm") + try: + self._candidates = await self._discover_factory_hubs() + except Exception: + _LOGGER.exception("Factory-network discovery failed") + return self.async_show_form( + step_id="bluetooth_confirm", errors={"base": "cannot_connect"} + ) + if not self._candidates: + return self.async_show_form( + step_id="bluetooth_confirm", errors={"base": "no_factory_hubs"} + ) + return await self.async_step_identify() - if user_input is not None: - self._email = user_input["email"].strip() - try: - session = async_get_clientsession(self.hass) - await async_send_code(session, self._email) - return await self.async_step_verify() - except ApiError: - errors["base"] = "cannot_connect" - except Exception: - _LOGGER.exception("Unexpected error sending verification code") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema({ - vol.Required("email"): str, - }), - errors=errors, - ) - - async def async_step_verify( + async def async_step_identify( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Step 2: User enters the 6-digit verification code.""" + choices = {f"0x{id_:04X}": id_ for id_ in sorted(self._candidates)} errors: dict[str, str] = {} - if user_input is not None: - code = user_input["code"].strip() + self._selected_id = choices[user_input["device_id"]] + self._garden_name = user_input["garden_name"].strip() or "in-lite" try: - session = async_get_clientsession(self.hass) - self._gardens = await async_login(session, self._email, code) - if not self._gardens: - errors["base"] = "no_gardens" - elif len(self._gardens) == 1: - return await self._create_entry(self._gardens[0]) - else: - return await self.async_step_select_garden() - except AuthenticationError: - errors["base"] = "invalid_auth" - except ApiError: - errors["base"] = "cannot_connect" + await self._identify(self._selected_id) except Exception: - _LOGGER.exception("Unexpected error during login") - errors["base"] = "unknown" - + _LOGGER.exception("Identify command failed") + errors["base"] = "cannot_connect" + else: + return await self.async_step_confirm_blink() return self.async_show_form( - step_id="verify", + step_id="identify", data_schema=vol.Schema({ - vol.Required("code"): str, + vol.Required("device_id"): vol.In(choices), + vol.Required("garden_name", default=self._garden_name): str, }), errors=errors, - description_placeholders={"email": self._email}, ) - - async def async_step_select_garden( + async def async_step_confirm_blink( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Step 3: User selects a garden (only shown if >1 garden).""" - if user_input is not None: - garden_name = user_input["garden"] - for g in self._gardens: - if g.name == garden_name: - return await self._create_entry(g) - - garden_names = [g.name for g in self._gardens] - return self.async_show_form( - step_id="select_garden", - data_schema=vol.Schema({ - vol.Required("garden"): vol.In(garden_names), - }), - ) - - async def _create_entry(self, garden: Garden) -> ConfigFlowResult: - """Create a config entry for the selected garden.""" - await self.async_set_unique_id(garden.id) - self._abort_if_unique_id_configured() - - transformers = [] - for t in garden.transformers: - zones = [] - for z in t.light_zones: - zones.append({ - "output_id": z.output_id, - "name": z.name, - }) - transformers.append({ - "device_id": t.device_id, - "name": t.name, - "firmware_version": t.firmware_version, - "zones": zones, - }) - - data = { - "email": self._email, - CONF_GARDEN_ID: garden.id, - CONF_GARDEN_NAME: garden.name, - CONF_PASSWORD: garden.password, - CONF_TRANSFORMERS: transformers, - } - - # If this is a reauth, update the existing entry - if self._reauth_entry is not None: - self.hass.config_entries.async_update_entry( - self._reauth_entry, data=data + if user_input is None: + return self.async_show_form( + step_id="confirm_blink", + data_schema=vol.Schema({vol.Required("confirmed"): bool}), + ) + if not user_input["confirmed"]: + return await self.async_step_identify() + if self._selected_id is None: + return self.async_abort(reason="unknown") + try: + password, zones, firmware = await self._associate_and_read(self._selected_id) + except Exception: + _LOGGER.exception("Local association failed") + return self.async_show_form( + step_id="confirm_blink", errors={"base": "association_failed"} ) - await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") + return self.async_create_entry( + title=f"in-lite {self._garden_name}", + data={ + CONF_GARDEN_ID: self._service_info.address, + CONF_GARDEN_NAME: self._garden_name, + CONF_PASSWORD: password, + CONF_TRANSFORMERS: [{ + "device_id": self._selected_id, + "name": f"in-lite hub 0x{self._selected_id:04X}", + "firmware_version": firmware if firmware is not None else "unknown", + "zones": [ + {"output_id": zone_id, "name": f"Zone {zone_id + 1}"} + for zone_id in sorted(zones) + ], + }], + }, + ) - return self.async_create_entry( - title="in-lite %s" % garden.name, - data=data, + async def _connect(self) -> Any: + if self._service_info is None: + raise ConnectionError("No in-lite gateway available") + return await establish_connection( + BleakClientWithServiceCache, self._service_info.device, + self._service_info.address, max_attempts=3, ) - # ------------------------------------------------------------------ - # Reauth flow - # ------------------------------------------------------------------ + async def _with_factory_hub(self, device_id: int) -> InliteHub: + hub = InliteHub(device_id, FACTORY_NETWORK_PASSPHRASE) + client = await self._connect() + if not await hub.connect(client=client): + await hub.disconnect() + raise ConnectionError("Could not subscribe to hub notifications") + return hub - async def async_step_reauth( - self, entry_data: dict[str, Any] - ) -> ConfigFlowResult: - """Handle re-authentication when credentials become invalid.""" - self._reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - self._email = entry_data.get("email") - return await self.async_step_reauth_confirm() + async def _discover_factory_hubs(self) -> set[int]: + hub = await self._with_factory_hub(0) + try: + return await hub.discover_hubs() + finally: + await hub.disconnect() - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm reauth and restart the email/code flow.""" - if user_input is not None: - return await self.async_step_user() + async def _identify(self, device_id: int) -> None: + hub = await self._with_factory_hub(device_id) + try: + if not await hub.identify(): + raise ConnectionError("Hub did not acknowledge identify") + finally: + await hub.disconnect() - return self.async_show_form( - step_id="reauth_confirm", - description_placeholders={"email": self._email or ""}, - ) + async def _associate_and_read( + self, device_id: int + ) -> tuple[str, dict[int, Any], int | None]: + password = secrets.token_urlsafe(32) + hub = await self._with_factory_hub(device_id) + try: + if not await hub.associate(password): + raise ConnectionError("Hub did not acknowledge association") + finally: + await hub.disconnect() + local_hub = InliteHub(device_id, password) + client = await self._connect() + try: + if not await local_hub.connect(client=client): + raise ConnectionError("Could not reconnect after association") + return password, await local_hub.query_zone_states(), local_hub.firmware_version + finally: + await local_hub.disconnect() class InliteOptionsFlow(OptionsFlow): - """Handle in-lite options.""" + """Options for local polling and connection lifetime.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" + def __init__(self, config_entry: Any) -> None: self._config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) - - current_scan = self._config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - current_idle = self._config_entry.options.get( - CONF_IDLE_DISCONNECT, DEFAULT_IDLE_DISCONNECT_SECONDS - ) - return self.async_show_form( step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_SCAN_INTERVAL, default=current_scan - ): vol.All( - vol.Coerce(int), - vol.Range(min=MIN_SCAN_INTERVAL, max=MAX_SCAN_INTERVAL), + data_schema=vol.Schema({ + vol.Required( + CONF_SCAN_INTERVAL, + default=self._config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - vol.Required( - CONF_IDLE_DISCONNECT, default=current_idle - ): vol.All( - vol.Coerce(int), - vol.Range( - min=MIN_IDLE_DISCONNECT_SECONDS, - max=MAX_IDLE_DISCONNECT_SECONDS, - ), + ): vol.All( + vol.Coerce(int), + vol.Range(min=MIN_SCAN_INTERVAL, max=MAX_SCAN_INTERVAL), + ), + vol.Required( + CONF_IDLE_DISCONNECT, + default=self._config_entry.options.get( + CONF_IDLE_DISCONNECT, DEFAULT_IDLE_DISCONNECT_SECONDS + ), + ): vol.All( + vol.Coerce(int), + vol.Range( + min=MIN_IDLE_DISCONNECT_SECONDS, + max=MAX_IDLE_DISCONNECT_SECONDS, ), - } - ), + ), + }), ) diff --git a/custom_components/inlite/lib/inlite_ble/__init__.py b/custom_components/inlite/lib/inlite_ble/__init__.py index 2a4cb03bffacff9ff8e48cd7833f0113bcd5753f..5ceeff1fa7ef43602796fc7e6afd585aade270c6 100644 --- a/custom_components/inlite/lib/inlite_ble/__init__.py +++ b/custom_components/inlite/lib/inlite_ble/__init__.py @@ -1,32 +1,6 @@ -"""in-lite BLE mesh library β€” control in-lite outdoor lighting via Bluetooth.""" +"""in-lite BLE mesh library for local in-lite control.""" -from inlite_ble.cloud import ( - ApiError, - AuthenticationError, - Garden, - InliteCloudClient, - LightZone, - Transformer, - async_login, - async_send_code, - login, - send_code, -) from inlite_ble.crypto import CsrMeshCrypto from inlite_ble.hub import InliteHub, ZoneState -__all__ = [ - "ApiError", - "AuthenticationError", - "CsrMeshCrypto", - "Garden", - "InliteCloudClient", - "InliteHub", - "LightZone", - "Transformer", - "ZoneState", - "async_login", - "async_send_code", - "login", - "send_code", -] +__all__ = ["CsrMeshCrypto", "InliteHub", "ZoneState"] diff --git a/custom_components/inlite/lib/inlite_ble/cloud.py b/custom_components/inlite/lib/inlite_ble/cloud.py deleted file mode 100644 index c58e7022ee9a4017be7204ca6e753940d107feac..0000000000000000000000000000000000000000 --- a/custom_components/inlite/lib/inlite_ble/cloud.py +++ /dev/null @@ -1,223 +0,0 @@ -"""in-lite cloud API client β€” email+code login, fetches garden config and BLE passphrase. - -Provides both synchronous (urllib) and asynchronous (aiohttp) interfaces. -The HA integration uses the async versions; standalone scripts can use the sync ones. -""" - -from __future__ import annotations - -import json -import logging -from typing import Any -from urllib.error import HTTPError -from urllib.request import Request, urlopen - -try: - import aiohttp - - _HAS_AIOHTTP = True -except ImportError: - _HAS_AIOHTTP = False - -_LOGGER = logging.getLogger(__name__) - -API_BASE = "https://api.inlite.coffeeit.nl" -DEFAULT_HEADERS = { - "Content-Type": "application/json", - "Accept": "application/json", - "Accept-Language": "en", - "X-App-Version": "iOS/3.18.0", -} - - -class AuthenticationError(Exception): - """Raised on invalid or expired verification code.""" - - -class ApiError(Exception): - """Raised on unexpected API errors.""" - - -class LightZone: - """Represents a controllable light zone on a hub.""" - - def __init__(self, data: dict[str, Any]) -> None: - self.output_id: int = data.get("outputId", 0) - self.name: str = data.get("name", "Zone %d" % (self.output_id + 1)) - self.output_type: int = data.get("outputType", 0) - self.output_mode: int = data.get("outputMode", 0) - self.output_state: int = data.get("outputState", 0) - self.icon_id: int = data.get("iconId", 0) - - @property - def is_on(self) -> bool: - return (self.output_mode & 0x01) != 0 - - def __repr__(self) -> str: - state = "ON" if self.is_on else "OFF" - return "LightZone(%r, id=%d, %s)" % (self.name, self.output_id, state) - - -class Transformer: - """Represents a SMART HUB transformer.""" - - def __init__(self, data: dict[str, Any]) -> None: - self.id: str = data.get("_id", "") - self.device_id: int = data.get("deviceId", 0) - self.name: str = data.get("name", "SMART HUB %d" % self.device_id) - self.firmware_version: int = data.get("firmwareVersion", 0) - self.hardware_id: str = data.get("hardwareId", "") - self.light_zones: list[LightZone] = [ - LightZone(lz) for lz in data.get("lightZones", []) - ] - - def __repr__(self) -> str: - return "Transformer(%r, id=0x%04X, zones=%d)" % ( - self.name, self.device_id, len(self.light_zones) - ) - - -class Garden: - """Represents an in-lite garden with its network passphrase and devices.""" - - def __init__(self, data: dict[str, Any]) -> None: - self.id: str = data.get("_id", "") - self.name: str = data.get("name", "") - self.password: str = data.get("password", "") - self.timezone: str = data.get("timeZone", "") - self.transformers: list[Transformer] = [ - Transformer(t) for t in data.get("transformers", []) - ] - - def __repr__(self) -> str: - return "Garden(%r, hubs=%d)" % (self.name, len(self.transformers)) - - -# --------------------------------------------------------------------------- -# Synchronous API (urllib) β€” for standalone scripts -# --------------------------------------------------------------------------- - - -def _api_request( - method: str, - path: str, - body: dict[str, Any] | None = None, - token: str | None = None, -) -> Any: - """Make a synchronous API request and return parsed JSON.""" - url = API_BASE + path - headers = dict(DEFAULT_HEADERS) - if token: - headers["Authorization"] = token - - data = json.dumps(body).encode("utf-8") if body else None - req = Request(url, data=data, headers=headers, method=method) - - try: - with urlopen(req) as resp: - raw = resp.read() - return json.loads(raw) if raw else {} - except HTTPError as e: - body_text = e.read().decode("utf-8", errors="replace") if e.fp else "" - if e.code == 401: - raise AuthenticationError("Invalid or expired verification code") from e - raise ApiError("API %s %s β†’ %d: %s" % (method, path, e.code, body_text)) from e - - -def send_code(email: str, language: str = "en") -> None: - """Request a verification code be sent to the user's email (sync).""" - _api_request("POST", "/user/send-code", {"email": email, "language": language}) - _LOGGER.info("Verification code sent to %s", email) - - -def login(email: str, code: str) -> list[Garden]: - """Login with email + verification code, return list of gardens (sync).""" - result = _api_request("POST", "/user/login", {"email": email, "code": code}) - gardens = [Garden(g) for g in result.get("gardens", [])] - _LOGGER.info("Logged in as %s, found %d garden(s)", email, len(gardens)) - return gardens - - -# --------------------------------------------------------------------------- -# Asynchronous API (aiohttp) β€” for HA integration -# --------------------------------------------------------------------------- - - -async def _async_api_request( - session: aiohttp.ClientSession, - method: str, - path: str, - body: dict[str, Any] | None = None, - token: str | None = None, -) -> Any: - """Make an async API request and return parsed JSON.""" - url = API_BASE + path - headers = dict(DEFAULT_HEADERS) - if token: - headers["Authorization"] = token - - async with session.request(method, url, json=body, headers=headers) as resp: - if resp.status == 401: - raise AuthenticationError("Invalid or expired verification code") - if resp.status >= 400: - body_text = await resp.text() - raise ApiError("API %s %s β†’ %d: %s" % (method, path, resp.status, body_text)) - raw = await resp.read() - return json.loads(raw) if raw else {} - - -async def async_send_code( - session: aiohttp.ClientSession, email: str, language: str = "en" -) -> None: - """Request a verification code be sent to the user's email (async).""" - await _async_api_request(session, "POST", "/user/send-code", {"email": email, "language": language}) - _LOGGER.info("Verification code sent to %s", email) - - -async def async_login( - session: aiohttp.ClientSession, email: str, code: str -) -> list[Garden]: - """Login with email + verification code, return list of gardens (async).""" - result = await _async_api_request(session, "POST", "/user/login", {"email": email, "code": code}) - gardens = [Garden(g) for g in result.get("gardens", [])] - _LOGGER.info("Logged in as %s, found %d garden(s)", email, len(gardens)) - return gardens - - -# --------------------------------------------------------------------------- -# Convenience stateful client (sync, for standalone scripts) -# --------------------------------------------------------------------------- - - -class InliteCloudClient: - """Stateful client that holds login results for convenience.""" - - def __init__(self) -> None: - self._gardens: list[Garden] = [] - - @property - def gardens(self) -> list[Garden]: - return self._gardens - - def send_code(self, email: str) -> None: - """Request verification code.""" - send_code(email) - - def login(self, email: str, code: str) -> list[Garden]: - """Login and store gardens.""" - self._gardens = login(email, code) - return self._gardens - - def get_garden_by_id(self, garden_id: str) -> Garden | None: - """Find a garden by ID.""" - for g in self._gardens: - if g.id == garden_id: - return g - return None - - def get_garden_by_name(self, name: str) -> Garden | None: - """Find a garden by name (case-insensitive).""" - for g in self._gardens: - if g.name.lower() == name.lower(): - return g - return None diff --git a/custom_components/inlite/lib/inlite_ble/crypto.py b/custom_components/inlite/lib/inlite_ble/crypto.py index 1e91f98c27e46b2ba26e685c5dd1c2a8184ef606..ddbb41e2ad9bc41ca86f5e393ef3bc50174aeed8 100644 --- a/custom_components/inlite/lib/inlite_ble/crypto.py +++ b/custom_components/inlite/lib/inlite_ble/crypto.py @@ -26,11 +26,13 @@ def controller_address(self) -> int: return self._controller_address @staticmethod - def _derive_key(passphrase: str) -> bytes: + def derive_key(passphrase: str) -> bytes: """Derive 16-byte AES key: SHA-256(UTF-8(passphrase + '\\x00MCP')), reversed.""" raw = (passphrase + "\x00MCP").encode("utf-8") digest = hashlib.sha256(raw).digest() return bytes(digest[len(digest) - 1 - i] for i in range(16)) + + _derive_key = derive_key @staticmethod def _generate_iv(seq_nr: int, src_id: int) -> bytes: diff --git a/custom_components/inlite/lib/inlite_ble/hub.py b/custom_components/inlite/lib/inlite_ble/hub.py index 41e0873925c9d1eb5d8ba2bc373e43a82f0197c9..a5dc14fcd2bbde4fc82ef3a0996544082d06409f 100644 --- a/custom_components/inlite/lib/inlite_ble/hub.py +++ b/custom_components/inlite/lib/inlite_ble/hub.py @@ -21,6 +21,7 @@ PKT_BLOCK_ACK, PKT_BLOCK_DATA_BLK, PKT_BLOCK_STREAM, OPCODE_SET_OUTLET_MODE, + OPCODE_IDENTIFY, OPCODE_GET_INFO_DEVICES, OPCODE_OOB_ALL_OUTLETS, build_outlet_mode_data, @@ -28,6 +29,7 @@ build_block_data_payload, build_flush_payload, build_ack_payload, build_discovery_payload, + build_association_payload, ) _LOGGER = logging.getLogger(__name__) @@ -101,6 +103,9 @@ self._stream_event = asyncio.Event() self._last_ack_data = b"" self._last_stream_data = b"" self._zone_states: dict[int, ZoneState] = {} + self._firmware_version: int | None = None + self._discovered_device_ids: set[int] = set() + self._discovery_event = asyncio.Event() self._notification_callback: Callable[[dict[str, Any]], None] | None = None self._on_state_update = on_state_update @@ -120,6 +125,11 @@ @property def zone_states(self) -> dict[int, ZoneState]: return self._zone_states + @property + def firmware_version(self) -> int | None: + """Firmware byte reported by GET_INFO_DEVICES.""" + return self._firmware_version + async def scan(self, timeout: float = 10.0) -> BLEDevice | None: """Scan for the in-lite hub by name or address.""" _LOGGER.info("Scanning for %s...", self._ble_address) @@ -209,6 +219,9 @@ elif pkt_type == PKT_BLOCK_STREAM: self._last_stream_data = payload loop.call_soon_threadsafe(self._stream_event.set) elif pkt_type == PKT_BLOCK_DATA_BLK: + if len(payload) >= 3 and payload[1:3] == b"\x0c\x00": + self._discovered_device_ids.add(decrypted["src_id"]) + loop.call_soon_threadsafe(self._discovery_event.set) loop.call_soon_threadsafe(self._parse_oob_broadcast, payload) if self._notification_callback: @@ -285,15 +298,15 @@ 2. BLK_DATA(offset, opcode, data) β†’ wait ACK with byte count 3. BLK_FLUSH(byte_count) β†’ wait ACK with 'ef' suffix (done) 4. ACK the hub's response """ - dest = self._device_id + return await self._send_raw_stream(build_block_data_payload(opcode, command_data)) - # Step 1: Flush (start) β€” clear event BEFORE writing + async def _send_raw_stream(self, block_data: bytes) -> bool: + """Send a raw in-lite block stream and wait for its completion ACK.""" + dest = self._device_id self._ack_event.clear() await self._write_mesh(dest, PKT_BLOCK_FLUSH, build_flush_payload(0)) - ack = await self._wait_ack() + await self._wait_ack() - # Step 2: Send command data - block_data = build_block_data_payload(opcode, command_data) self._ack_event.clear() await self._write_mesh(dest, PKT_BLOCK_DATA, block_data) ack = await self._wait_ack() @@ -312,9 +325,9 @@ # Check for completion marker (0xef suffix) success = len(ack) >= 3 and ack[-1] == 0xEF if success: - _LOGGER.info("Command 0x%04X sent successfully", opcode) + _LOGGER.info("Block stream sent successfully") else: - _LOGGER.warning("Command 0x%04X: no completion ACK", opcode) + _LOGGER.warning("Block stream: no completion ACK") # ACK the hub's response flush await asyncio.sleep(WRITE_DELAY) @@ -435,7 +448,7 @@ return self._zone_states # vendor_id = stream[5] # product_id = stream[6] - # firmware = stream[7] + self._firmware_version = stream[7] # status = stream[8] num_zones = stream[9] @@ -497,3 +510,22 @@ """Send a discovery broadcast (also serves as keepalive).""" data = build_discovery_payload() await self._write_mesh(0x0000, PKT_BLOCK_DATA_BLK, data) + async def discover_hubs(self, timeout: float = 5.0) -> set[int]: + """Discover factory-network in-lite devices and return mesh IDs.""" + self._discovered_device_ids.clear() + self._discovery_event.clear() + await self.send_discovery() + # Discovery responses arrive independently; keep collecting for the + # complete window rather than stopping after the first hub responds. + await asyncio.sleep(timeout) + return self._discovered_device_ids.copy() + + async def associate(self, new_passphrase: str) -> bool: + """Move this factory-reset hub onto ``new_passphrase``'s local mesh.""" + return await self._send_raw_stream( + build_association_payload(CsrMeshCrypto.derive_key(new_passphrase)) + ) + + async def identify(self) -> bool: + """Ask a hub to briefly blink its identify light.""" + return await self._send_command(OPCODE_IDENTIFY, b"") diff --git a/custom_components/inlite/lib/inlite_ble/protocol.py b/custom_components/inlite/lib/inlite_ble/protocol.py index c0860e9e60e5c5bbfe92452856d6b6d4870df8c1..a43d55223e72e4cf6bb437858bc65862aad9fa5a 100644 --- a/custom_components/inlite/lib/inlite_ble/protocol.py +++ b/custom_components/inlite/lib/inlite_ble/protocol.py @@ -12,6 +12,8 @@ # Opcodes OPCODE_DISCOVER = 0x000C OPCODE_GET_INFO_DEVICES = 0x0005 +OPCODE_GET_OUTPUT_NAMES = 0x0019 +OPCODE_IDENTIFY = 0x0014 OPCODE_SET_OUTLET_MODE = 0x1007 # 4103 OPCODE_OOB_ALL_OUTLETS = 0x0021 # broadcast after state changes @@ -22,6 +24,8 @@ OUTPUT_ON_MANUAL = 0x03 # forced on # GATT UUIDs SERVICE_UUID = "0000fef1-0000-1000-8000-00805f9b34fb" +# Public factory-reset network constant used by the official app. +FACTORY_NETWORK_PASSPHRASE = "6DeNmnsD5XUsf4UD" # The "MTL Complete CP" characteristic β€” bidirectional (write + notify) CHAR_WRITE_UUID = "c4edc000-9daf-11e3-8004-00025b000b00" # The "MTL Continuation CP" characteristic β€” bidirectional (write + notify) @@ -64,3 +68,14 @@ def build_discovery_payload() -> bytes: """Build BLK_DATA_BLK discovery packet.""" return bytes([0x01, OPCODE_DISCOVER & 0xFF, (OPCODE_DISCOVER >> 8) & 0xFF]) + + +def build_association_payload(network_key: bytes) -> bytes: + """Build the local in-lite factory-network association request. + + This is a vendor MCP stream rather than a standard command stream. The + network key is the 16-byte MCP key derived from the new passphrase. + """ + if len(network_key) != 16: + raise ValueError("network_key must be 16 bytes") + return bytes([0x01, 0x24, 0x00]) + network_key diff --git a/custom_components/inlite/strings.json b/custom_components/inlite/strings.json index dc4a86df7a4a616f2f75e89b5a9e65579ecd669b..225b28a9007f117eddafc6858edaad0dce1e5ca9 100644 --- a/custom_components/inlite/strings.json +++ b/custom_components/inlite/strings.json @@ -2,56 +2,33 @@ { "config": { "step": { "user": { - "title": "in-lite Account", - "description": "Enter the email address associated with your in-lite account. A verification code will be sent to this address.", - "data": { - "email": "Email" - } - }, - "verify": { - "title": "Verification Code", - "description": "We sent a verification code to **{email}**. Enter the 6-digit code below.", - "data": { - "code": "Verification code" - } - }, - "select_garden": { - "title": "Select Garden", - "description": "Your account has multiple gardens. Select the one you want to set up.", - "data": { - "garden": "Garden" - } + "title": "Set up local in-lite control", + "description": "Select a nearby in-lite hub. No in-lite account or cloud connection is used." }, "bluetooth_confirm": { "title": "Confirm Device", - "description": "An in-lite hub was discovered nearby. Would you like to set it up?" + "description": "An in-lite gateway was found. Continue to discover factory-reset hubs on its local mesh." }, - "reauth_confirm": { - "title": "Re-authenticate", - "description": "The credentials for **{email}** need to be updated. Press submit to re-authenticate." + "identify": { + "title": "Identify hub", + "description": "Choose a factory-reset hub. It will blink briefly before any configuration changes are made.", + "data": {"device_id": "Hub", "garden_name": "Name"} + }, + "confirm_blink": { + "title": "Confirm hub", + "description": "Did the selected hub blink? Confirming will place that hub on a new local-only mesh.", + "data": {"confirmed": "The selected hub blinked"} } }, "error": { - "cannot_connect": "Unable to connect to the in-lite cloud service. Please try again.", - "invalid_auth": "Invalid or expired verification code. Please check and try again.", - "no_gardens": "No gardens found on this account.", + "cannot_connect": "Unable to connect to the nearby in-lite hub. Please try again.", + "no_factory_hubs": "No factory-reset in-lite hubs responded.", + "association_failed": "The selected hub could not be associated. It may not be factory reset.", "unknown": "An unexpected error occurred. Please try again." }, "abort": { - "already_configured": "This garden is already configured.", - "reauth_successful": "Re-authentication successful." - } - }, - "options": { - "step": { - "init": { - "title": "in-lite Options", - "description": "Configure polling and connection behavior. A longer idle disconnect keeps the BLE connection alive to receive real-time state updates from the hub (e.g., physical button presses).", - "data": { - "scan_interval": "Poll interval (seconds)", - "idle_disconnect": "Idle disconnect timeout (seconds)" - } - } + "already_configured": "This hub is already configured.", + "no_hub_found": "No nearby in-lite gateway was found." } } } diff --git a/custom_components/inlite/translations/en.json b/custom_components/inlite/translations/en.json index b4dde70918fec3b493f163c809a2db8e29cfc6c9..225b28a9007f117eddafc6858edaad0dce1e5ca9 100644 --- a/custom_components/inlite/translations/en.json +++ b/custom_components/inlite/translations/en.json @@ -2,44 +2,33 @@ { "config": { "step": { "user": { - "title": "in-lite Account", - "description": "Enter the email address associated with your in-lite account. A verification code will be sent to this address.", - "data": { - "email": "Email" - } - }, - "verify": { - "title": "Verification Code", - "description": "We sent a verification code to **{email}**. Enter the 6-digit code below.", - "data": { - "code": "Verification code" - } - }, - "select_garden": { - "title": "Select Garden", - "description": "Your account has multiple gardens. Select the one you want to set up.", - "data": { - "garden": "Garden" - } + "title": "Set up local in-lite control", + "description": "Select a nearby in-lite hub. No in-lite account or cloud connection is used." }, "bluetooth_confirm": { "title": "Confirm Device", - "description": "An in-lite hub was discovered nearby. Would you like to set it up?" + "description": "An in-lite gateway was found. Continue to discover factory-reset hubs on its local mesh." + }, + "identify": { + "title": "Identify hub", + "description": "Choose a factory-reset hub. It will blink briefly before any configuration changes are made.", + "data": {"device_id": "Hub", "garden_name": "Name"} }, - "reauth_confirm": { - "title": "Re-authenticate", - "description": "The credentials for **{email}** need to be updated. Press submit to re-authenticate." + "confirm_blink": { + "title": "Confirm hub", + "description": "Did the selected hub blink? Confirming will place that hub on a new local-only mesh.", + "data": {"confirmed": "The selected hub blinked"} } }, "error": { - "cannot_connect": "Unable to connect to the in-lite cloud service. Please try again.", - "invalid_auth": "Invalid or expired verification code. Please check and try again.", - "no_gardens": "No gardens found on this account.", + "cannot_connect": "Unable to connect to the nearby in-lite hub. Please try again.", + "no_factory_hubs": "No factory-reset in-lite hubs responded.", + "association_failed": "The selected hub could not be associated. It may not be factory reset.", "unknown": "An unexpected error occurred. Please try again." }, "abort": { - "already_configured": "This garden is already configured.", - "reauth_successful": "Re-authentication successful." + "already_configured": "This hub is already configured.", + "no_hub_found": "No nearby in-lite gateway was found." } } } diff --git a/pyproject.toml b/pyproject.toml index 0724effbc6bbd14a79b95d94c805c78e37a6e87a..c7e83bc8835402a6ba7a4f01ba0211ddc8e8d3a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,10 @@ [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["custom_components/inlite/lib"] + +[dependency-groups] +dev = [ + "bleak>=3.0.2", + "pycryptodomex>=3.23.0", + "pytest>=9.1.1", +] diff --git a/tests/test_cloud.py b/tests/test_cloud.py deleted file mode 100644 index b9d680f59fd77839b876c411b58b05300a66feac..0000000000000000000000000000000000000000 --- a/tests/test_cloud.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Tests for inlite_ble cloud module.""" - -from inlite_ble.cloud import Garden, Transformer, LightZone - - -class TestLightZone: - """Tests for LightZone data model.""" - - def test_basic_creation(self) -> None: - lz = LightZone({"outputId": 2, "name": "Front", "outputMode": 1}) - assert lz.output_id == 2 - assert lz.name == "Front" - assert lz.is_on is True - - def test_default_values(self) -> None: - lz = LightZone({}) - assert lz.output_id == 0 - assert lz.name == "Zone 1" - assert lz.is_on is False - - def test_repr(self) -> None: - lz = LightZone({"outputId": 0, "name": "Test", "outputMode": 0}) - assert "OFF" in repr(lz) - - -class TestTransformer: - """Tests for Transformer data model.""" - - def test_basic_creation(self) -> None: - t = Transformer({ - "_id": "abc", - "deviceId": 0x1234, - "name": "Hub 1", - "lightZones": [{"outputId": 0, "name": "Zone 1"}], - }) - assert t.device_id == 0x1234 - assert len(t.light_zones) == 1 - - def test_default_name(self) -> None: - t = Transformer({"deviceId": 5}) - assert "5" in t.name - - -class TestGarden: - """Tests for Garden data model.""" - - def test_basic_creation(self) -> None: - g = Garden({ - "_id": "garden1", - "name": "My Garden", - "password": "secret", - "transformers": [ - {"deviceId": 1, "lightZones": []}, - ], - }) - assert g.id == "garden1" - assert g.name == "My Garden" - assert g.password == "secret" - assert len(g.transformers) == 1 - - def test_async_functions_exist(self) -> None: - """Verify async API functions are importable.""" - from inlite_ble.cloud import async_send_code, async_login - assert callable(async_send_code) - assert callable(async_login) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index f7645bb6a3198d8dfdca8be7e63eab8886c52fcd..b417f05572d1657995f7258d88b4e51524d8fd4b 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -4,10 +4,12 @@ from inlite_ble.protocol import ( build_ack_payload, build_block_data_payload, build_discovery_payload, + build_association_payload, build_flush_payload, build_outlet_mode_data, OPCODE_SET_OUTLET_MODE, OPCODE_DISCOVER, + OPCODE_IDENTIFY, ) @@ -81,3 +83,18 @@ result = build_discovery_payload() assert result[0] == 0x01 assert result[1] == OPCODE_DISCOVER & 0xFF assert result[2] == (OPCODE_DISCOVER >> 8) & 0xFF + + +class TestBuildAssociationPayload: + def test_structure(self) -> None: + key = bytes(range(16)) + assert build_association_payload(key) == b"\x01\x24\x00" + key + + def test_rejects_wrong_key_size(self) -> None: + import pytest + with pytest.raises(ValueError): + build_association_payload(b"too short") + + +def test_identify_opcode_matches_official_app() -> None: + assert OPCODE_IDENTIFY == 0x0014 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..2d34233cc00ac17883db7cd52b096da5d0d57a02 --- /dev/null +++ b/uv.lock @@ -0,0 +1,432 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "bleak" +version = "3.0.2" +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-devices-radios", 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/16/df/05a3f80ca8e3f7f5b0dba68a9e618147c909ccdba1468f07487dc8d72a9d/bleak-3.0.2.tar.gz", hash = "sha256:c2229cb8238d5876b4bd05c74bf7a1aea1f88da39d2e51ac9dfd5cc319d5265f", size = 125293, upload-time = "2026-05-02T23:01:04.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/54/05aceb9cd80073805b3ed8522e3196e8cb22f70e741873fa51406c31f4e7/bleak-3.0.2-py3-none-any.whl", hash = "sha256:39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", size = 146490, upload-time = "2026-05-02T23:01:02.592Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dbus-fast" +version = "5.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/db/b621610e50b1bc46ff63534d75239553c1bf33256de6096b58214fd9808a/dbus_fast-5.0.22.tar.gz", hash = "sha256:34dc67d7d21a12399828dd13e63b352750580beea54ea7c729e708f2d2905fef", size = 83224, upload-time = "2026-06-05T18:47:59.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/06/233b0bc13919474f70320bf389cb81ce02811956d6cf86c45e84679b63c3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f0bcad7f71d2304a68a5b0bc0d24c3fcc14710a2ffcf5f2a27521e3aece71ca", size = 799464, upload-time = "2026-06-05T18:56:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/68/e9/77bc23a6f5aebfb8f2c34489795e8517aed7eca31738438e1a4c4a4891d3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", size = 852687, upload-time = "2026-06-05T18:56:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/56769d0936d1273d1801ef574ec426ccb3f61f4b0a7a0eeb9eb2b8ccafa5/dbus_fast-5.0.22-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98de6d2c200d8182e1fd0bdde3206fa556b8fa14ebb752a044cd8daa87b4658c", size = 833814, upload-time = "2026-06-05T18:56:05.87Z" }, + { url = "https://files.pythonhosted.org/packages/06/ca/964f0d39a3be03b12a98f39519d34ad95b74360c7e3adf4cd4907dc25fd6/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b013437b66dc22b8d9aca5e0b0d46bf1980208a143409469fe482d9684a2a717", size = 806891, upload-time = "2026-06-05T18:56:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/57fe6ab509ad9da2e190498fa9c37f868e38ac521d940a9edb9ba6b6c657/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:855f15b7f7805171da2b82de1c317d01cfbb9fb8ac61fcc1e8dec54d8c69fab7", size = 830867, upload-time = "2026-06-05T18:56:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/be/2b/da036e9f4aeb776833139575fe0774544aa6cd13ba997eea7fbc4ab99852/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d1c42963235cfc015a2d2b8c5fe42b65387493b4ad4ce0ec122601c805e6742", size = 860651, upload-time = "2026-06-05T18:56:10.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/fa81a6685c763ea488ad93228cb6e036adc9af6a560f4c31643691f4cfd8/dbus_fast-5.0.22-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de10ff3b3cb2acb1c09fe17158a470519000d37bb5ee5fd69c4075e81ce8dcf5", size = 798472, upload-time = "2026-06-05T18:56:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/6a/34/6b272e6df60be1aa4d575aa30220175a52c002a649c951d9950bfa3a72d6/dbus_fast-5.0.22-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:979761985fe343c701f2b7575285d6e370123f7231d4656209ef7824bb686bbb", size = 850312, upload-time = "2026-06-05T18:56:16.36Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5c/9045c3595ddcd4069e6b5d051df06bf11a2b022592f721c9acea1e0e4d22/dbus_fast-5.0.22-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb73f1d8374253b7c17d69e902cf2ded1bfb089cb6ae67c10b4e0bdfe1b8fe08", size = 828366, upload-time = "2026-06-05T18:56:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/a7/78/ca6881442b8fa29edbe6d99bec4b535b0b2e2f423075d015ff5b719c4e2c/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b67a02037eb58bcf9e445df60ea0d9d7346fd334abde3aa62e03c75823b53979", size = 806036, upload-time = "2026-06-05T18:56:19.501Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c6/458728eb1caa26171e6a8ae1d0d99bd29aaeac67ad7824bbd95d7f854a41/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:83940ea00d7ee2f0c5bcb5d19d7d05e7949e52d467616a0b735d72e7285402ec", size = 828353, upload-time = "2026-06-05T18:56:21.346Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/830b1569264780210d44898e5b0d95cffe2830b952c2ee21ea481274cd81/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:279d212e9fb262d595af2e4b5b9e951bc00c73a5c8eeb50f158caa13705b9c84", size = 857743, upload-time = "2026-06-05T18:56:22.9Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8c/4eefaabdf538882528164060ae83d9a34f1172b019c32c3254436834e9b1/dbus_fast-5.0.22-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:703e0f8f9af52e8e053394ee2b578042be0c3d8ea2b1488f9db8cb14393cc13f", size = 810835, upload-time = "2026-06-05T18:56:26.356Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cf/fd327dbb40ee67a9331fb587bf78aff2ab1500b35979978a5cacb10d7f8c/dbus_fast-5.0.22-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb1d7e8e65561d0fd438004fd9e0f981c8a862912fed58dd4e29db1936c39d73", size = 855498, upload-time = "2026-06-05T18:56:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/56/33/1709ebc16a4d353ddc4fcd29252e2b9d93bded6422a45fd6df170e0911c1/dbus_fast-5.0.22-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:959fab6420897ab99410e67d6f9f9a7f6f4cedb6014700768f5e2d71dbff5dc6", size = 833510, upload-time = "2026-06-05T18:56:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fd/89d7c34152900d986b9c78e39cc62aa73eefc22b57b3a8c946d945a85540/dbus_fast-5.0.22-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:eb31c5ff339a7071b914617a69d5b7c6ba7d411da4b01a5f9b5b2fe51e9d1301", size = 853669, upload-time = "2026-06-05T18:47:56.747Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a1/031cc4a89d947f1fe110f663f93dcce9230213b7accaf719790d813def04/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:856f0543c593f3480e93e67bcd1aa4ddc1d94a6076cfd3ad4e0f5e2b01b33dc3", size = 818486, upload-time = "2026-06-05T18:56:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/36/e2/de8b764fdb947314fb8c2e079b556510194fd100983776845e234a107cc9/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:96d231d128c1f46f263790335897195dde9dac2f38571782db8ae1d8647bd548", size = 833582, upload-time = "2026-06-05T18:56:33.325Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1f/e5f0dd28d07c4b3f7bafd3357bfa424c8dace355a3dad921fec05db4634b/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:595bd3ccfd8318cbafff79f33a15709fee3728724fd61d5fa220080d73b574cb", size = 862291, upload-time = "2026-06-05T18:56:35.102Z" }, + { url = "https://files.pythonhosted.org/packages/26/69/5b54654f598ef98e8f94fd5a40929668b1f8fcd76e7fb50de0db73d329da/dbus_fast-5.0.22-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04bac97d0cb754a4d13037d0132517f1df28192d6e0568a0bf6df06623062285", size = 1534804, upload-time = "2026-06-05T18:56:38.804Z" }, + { url = "https://files.pythonhosted.org/packages/24/b7/c00d01699dc87ffc35f143226d3b296372840e2e2bc15101d35df7c74949/dbus_fast-5.0.22-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3eb57d592d84b0bb90e0c077db7ecb61562f49cc9b86a3ef08cbe17243e9cc4f", size = 1613316, upload-time = "2026-06-05T18:56:40.461Z" }, + { url = "https://files.pythonhosted.org/packages/f3/94/ea0db4c1aa6409cb16551b50aa8573e72f64407ca5281b042919ef81ca1c/dbus_fast-5.0.22-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:de4d235d1282ebb3ab65b6cddab84e914c045d92ceb381ddcbdbaf66bf1fb132", size = 822053, upload-time = "2026-06-05T18:56:42.519Z" }, + { url = "https://files.pythonhosted.org/packages/40/e4/a3bb52185b8a8c76bd8aaba3ff4fa8395eea19fbc142122b43dc377b275c/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:048f34299fbe82d7b87c56f47e8bd83f62339a4517685abc6671d603a55d2c89", size = 1549996, upload-time = "2026-06-05T18:56:44.307Z" }, + { url = "https://files.pythonhosted.org/packages/37/2b/6e405ba92e87d78a689a387809d975f97f8c8748b98efccfacd2b4e1d9f5/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:92df9fb6d8adeb17b534621c2ee730295bbe1d0c2584d5c82b1db478e3f04e8f", size = 823004, upload-time = "2026-06-05T18:56:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8f/77135ab8d690030cdb0ebeca879640b5945c4cbf5344ecbc507b4628da24/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7be4271e38251f1ad726962dec60da887c8ed352d157352e4fc27f56aece5c5d", size = 1629160, upload-time = "2026-06-05T18:56:47.688Z" }, +] + +[[package]] +name = "ha-inlite" +version = "0.2.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "bleak" }, + { name = "pycryptodomex" }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "bleak", specifier = ">=3.0.2" }, + { name = "pycryptodomex", specifier = ">=3.23.0" }, + { name = "pytest", specifier = ">=9.1.1" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/78/abc4ce5920305780aeb36b4067a86253378b36e29ba96673a3deb02eb03a/pyobjc_core-12.2.2.tar.gz", hash = "sha256:3906452339cd06a3bb07df103c2511d4cb0f7a22d8771c0b802eba15d9a642b6", size = 1067701, upload-time = "2026-08-11T19:43:39.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/b2/bbf7f049880ab40d110e66f25122342a1f6c98d6fe3c59bb98985503c660/pyobjc_core-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:122e6ad302a2abf5d4d4adb0156db751600ddf2768441696cba17b31323085e7", size = 6427637, upload-time = "2026-08-11T14:51:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ed/a8bf040caf3704023d74086b7fb96cf4ed2e844e24bd94e5248ba214b700/pyobjc_core-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950bd2d9c74634398c4e3d24ef2f213d4e23d705083697464fa67afedc53c1ad", size = 6427113, upload-time = "2026-08-11T15:04:39.424Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/760f8b9e116edd43c57e33844dc17619158fbdd311250d4209910192d72d/pyobjc_core-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3772b406edb3ff78171530a17cda1c4a7817f87b87ded0d8715b3fa664df16db", size = 6666817, upload-time = "2026-08-11T19:30:17.01Z" }, + { url = "https://files.pythonhosted.org/packages/13/37/486d38a173b0b8dce973a3e13c74cf402ed1b8621586b5963bc9efd49a48/pyobjc_core-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2062e8ad30a310441cd022544a897553408bebeaa7820d5edba3c96fd7fd693b", size = 6421184, upload-time = "2026-08-11T19:30:21.081Z" }, + { url = "https://files.pythonhosted.org/packages/04/f1/d138fd9b9a66ea8db56a8138b77d3413b85da3defe13363a19f364f85529/pyobjc_core-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2c7ef3d2f865b4b3ebb14ec3556f7a3e8abb6d130c67275cd9daa08dbd6e4e4e", size = 6671824, upload-time = "2026-08-11T19:30:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/577e2265cccf59daf48c460f0a8deeaf7dbe2991227a8859ab1eeab4945e/pyobjc_core-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89acc6bc13aaa6e3f52b0ce652ede7e201edb6bf062741b246b0c5a44582f25f", size = 6477694, upload-time = "2026-08-11T19:30:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/bd9f830c64c6f334530831e75c01bfe0a770a3fbb00fddc70329223118b3/pyobjc_core-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59a77038ebe0ab1240f61c341e7fb67b8674f2b4cd41bc71a6472511a12b50f7", size = 6712233, upload-time = "2026-08-11T19:30:33.032Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/76/49c6da2c6a831020b4854ba20079d5a1030474bffc776b7b73c2eeff8c15/pyobjc_framework_cocoa-12.2.2.tar.gz", hash = "sha256:c96c0ef69a71afbbb0e6a7d594b455c5fe47d62e0db376ee7a2b4b828c16ace9", size = 3132831, upload-time = "2026-08-11T19:44:02.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/2f/b67e73d8bc367e03fe7861cd9c49fff9dcfa6db83bc0630c0adcfb25b7fa/pyobjc_framework_cocoa-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e106f395531e67694376b0f1184612cbeea3ec8b9bf56b55ef41d026171d2a2d", size = 388117, upload-time = "2026-08-11T19:32:43.161Z" }, + { url = "https://files.pythonhosted.org/packages/db/e1/5d9b04ebb60042b9cb49adc2d33115e2f2c2e4ff7d548017bfaff8b7f536/pyobjc_framework_cocoa-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:600b1723184ca094931330e79355274949965460e23de38628d601b5a967baf9", size = 388181, upload-time = "2026-08-11T19:32:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/2a343357d5fe09bbe9c0e294dc03450866a0d6c1792fad36b6bcc00174c0/pyobjc_framework_cocoa-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:875f2aad73963faa81a6b36ae674fd494a4658d6d999e1075e0e2aca3d2391df", size = 392275, upload-time = "2026-08-11T19:32:45.631Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1a/b99521999b9f54b89aad928ddff0faad507abfe33bc46599454bfa48a4b2/pyobjc_framework_cocoa-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:889d7bbd4ba2d4941078bfbbfb882138e51dbead27df006abfe0f2e0d49b5b2e", size = 388368, upload-time = "2026-08-11T19:32:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/6d/26/0c697dbc73dcc76bc0f68ea5aeed25bf7b05217df5102659e878501b2d5f/pyobjc_framework_cocoa-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de69c5933750f3a4599ed962eccd92b6a71914c7e4318dacc7895738a8ae60d7", size = 392404, upload-time = "2026-08-11T19:32:47.918Z" }, + { url = "https://files.pythonhosted.org/packages/df/82/502f740fd8f4e9ef741c9d40ba67467ab2c8196f2c09dcba12936d28a4fd/pyobjc_framework_cocoa-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0e8ace0d44a00d281281a723d17fcd05eea7544a38a6a512e1fd018ddb7aece2", size = 388585, upload-time = "2026-08-11T19:32:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/07ce3c0ab8d1e9e1bed74fea1bf1cce73527a365a7a23c755051d3be9865/pyobjc_framework_cocoa-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8fe5b2e79c9530f667b4e58a87a3a15ea62f86a5d19eec405517ecbd4f454868", size = 392693, upload-time = "2026-08-11T19:32:50.283Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/f6/424a21392b3290dab0e8a78bddbe4e71754b9f1cf70d0e1584c03d6df987/pyobjc_framework_corebluetooth-12.2.2.tar.gz", hash = "sha256:75aa13f5355be549252d3192864bb3b82eb74c1f667d0527c1388117efffd688", size = 37806, upload-time = "2026-08-11T19:44:08.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/32/83753504c8a76c2ad87f7fadc51576d804425fb7a9a043b0fe6c1d7a9287/pyobjc_framework_corebluetooth-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78bb5ac453f4bebec59b2c918c8fb71fe054ab8c69d88cd21d6c231cc9a39593", size = 13223, upload-time = "2026-08-11T19:33:26.833Z" }, + { url = "https://files.pythonhosted.org/packages/16/76/044737646efadf7aed6e92160bb52d37927c2729f5de8558abc423963e0a/pyobjc_framework_corebluetooth-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0c483a90fb7ebf269af5f235fed82e437d8ff892c133f064af25623bf43efe16", size = 13236, upload-time = "2026-08-11T19:33:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ae/5bec176e086eabd9a36cf2acfec98207586215dbfc9d924453a707e8d6f8/pyobjc_framework_corebluetooth-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ff95d28591e1b83b4c3e835279194ca03cb6fa6e0b7fa224c143311cc71ed66b", size = 13418, upload-time = "2026-08-11T19:33:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/39/f8/592c2fc205d5d2a80f49960c4576ef52f89bcd43afb481de0a7f0f85d2c7/pyobjc_framework_corebluetooth-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c8d748b3ece787c750cdfc0ab820bd0316a084015453ca48b7885fcf56d3f782", size = 13225, upload-time = "2026-08-11T19:33:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/40/70/48680ab2e817b805db661d17fa172d704541f873be593a6f4e9e56f3f030/pyobjc_framework_corebluetooth-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:289ea13ba2b7746235ef47e2d7e7606752d08384022197ccc0e6943b91d4c62d", size = 13416, upload-time = "2026-08-11T19:33:30.12Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4a/0116aae535191482e6f110a878b0004c7d4d381764b66c5e09337c4d605c/pyobjc_framework_corebluetooth-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e8a964e4d32335fd77dd6e9ec0e510208884988c6d63360f80c699405f0beef2", size = 13217, upload-time = "2026-08-11T19:33:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/960d2ea2e55bf3856211d07176ff228721ce1863777594065129353c9331/pyobjc_framework_corebluetooth-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:61f4ac689b3a505218b470866931e8ce67e59b70f0877085f9332b169b96d991", size = 13419, upload-time = "2026-08-11T19:33:31.612Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/76/e40db30142b551790e5ab208e74de04cfc70823177a702c8c9e3f5e73034/pyobjc_framework_libdispatch-12.2.2.tar.gz", hash = "sha256:7cb799a7c5766cc1b68b68655a02c950646adfd2a743b7cbe8e4a04a51c44ecb", size = 40625, upload-time = "2026-08-11T19:44:46.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/be/de3dca9546c8eee4eb35f538511e36acdf337f589a78c609f737d1d7ed81/pyobjc_framework_libdispatch-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57e004479c4a7b697c1cee2c6aad80051bf447beafd1d780cb7427d99cc92b64", size = 15656, upload-time = "2026-08-11T19:37:27.869Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e6/3c2c086f282457bb532721037b2548c67974cd425ded85d4325a72fe259e/pyobjc_framework_libdispatch-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9c62b8c03b9e8c8925fdbb9d5a9541c1e9a4023ec06f1312441edfd166f33398", size = 15678, upload-time = "2026-08-11T19:37:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/97a2ea574323d22a9812f3b28dc13ef55bb3528b7ef8f52db37dc1d8de1b/pyobjc_framework_libdispatch-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b67f7433d2fbc0d5ff2ce4cb81ef08e0d0717fe88c2a95e335512c2cdb52d722", size = 15945, upload-time = "2026-08-11T19:37:29.473Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/827dac269b152880889e0ab0e9eeebb0e006790edc1f32fa9470a947c2d1/pyobjc_framework_libdispatch-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2488964343f9282e3521ab2f136948a8eaf7438351dc45c39e34ba23b9f5bc6b", size = 15704, upload-time = "2026-08-11T19:37:30.252Z" }, + { url = "https://files.pythonhosted.org/packages/95/9a/0bf4c9342949cf64a4781be35bf4cf37416e99b45e88c47604e208e11ba9/pyobjc_framework_libdispatch-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:74cea0ec7803147496c8b7b62e7ad1b9eda525aa692d8856464cec489951aa5d", size = 15987, upload-time = "2026-08-11T19:37:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/fc/13/24d9a179b1ecfab1aee6fc6e1090890121c6163f33e16965620a468504bb/pyobjc_framework_libdispatch-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:436828e39577ec9a889c59efa1e8a85fa0ba43af5069060a67dfead1df029ea4", size = 15724, upload-time = "2026-08-11T19:37:32.016Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/b0edf8f0dc908437ff2359655d0cf6749839fb39944137855cbaf882bd9c/pyobjc_framework_libdispatch-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b64b150207552b67caf1061e013ae6a1989aef34a18f36d9a3a455c51cd210c2", size = 16011, upload-time = "2026-08-11T19:37:32.795Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[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/d3/54/3dd06f2341fab6abb06588a16b30e0b213b0125be7b79dafc3bdba3b334a/winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1", size = 210090, upload-time = "2025-06-06T06:44:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a1/1d7248d5c62ccbea5f3e0da64ca4529ce99c639c3be2485b6ed709f5c740/winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576", size = 241391, upload-time = "2025-06-06T06:44:09.442Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/6a205d8dafc79f7c242be7f940b1e0c1971fd64ab3079bda4b514aa3d714/winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195", size = 415242, upload-time = "2025-06-06T06:44:10.407Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/c8/87/88bd98419a9da77a68e030593fee41702925a7ad8a8aec366945258cbb31/winrt_runtime-3.2.1-cp314-cp314-win32.whl", hash = "sha256:9b6298375468ac2f6815d0c008a059fc16508c8f587e824c7936ed9216480dad", size = 210257, upload-time = "2025-09-20T07:06:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/e5c2a10d287edd9d3ee8dc24bf7d7f335636b92bf47119768b7dd2fd1669/winrt_runtime-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:e36e587ab5fd681ee472cd9a5995743f75107a1a84d749c64f7e490bc86bc814", size = 241873, upload-time = "2025-09-20T07:06:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/eb9e78397132175f70dd51dfa4f93e489c17d6b313ae9dce60369b8d84a7/winrt_runtime-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:35d6241a2ebd5598e4788e69768b8890ee1eee401a819865767a1fbdd3e9a650", size = 416222, upload-time = "2025-09-20T07:06:43.376Z" }, +] + +[[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/54/ff/c4a3de909a875b46fad5e9f4fd412bba48571405bfa802b878954abf128c/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c", size = 105752, upload-time = "2025-06-06T07:00:10.684Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/bfee1f0c8d188c561c5b946ab21f6a0037e60dea110e80b1d6a1d529639f/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca", size = 113356, upload-time = "2025-06-06T07:00:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/d9da9c29d36cabadef4e19c3e9ba6d2692f6a28224c81fcff757132ea0da/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148", size = 104724, upload-time = "2025-06-06T07:00:12.406Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/b7/95/91cfdf941a1ba791708ab3477fc4e46793c8fe9117fc3e0a8c5ac5d7a09c/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win32.whl", hash = "sha256:de36ded53ca3ba12fc6dd4deb14b779acc391447726543815df4800348aad63a", size = 109015, upload-time = "2025-09-20T07:09:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/7460655628d0f340a93524f5236bb9f8514eb0e1d334b38cba8a89f6c1a6/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3295d932cc93259d5ccb23a41e3a3af4c78ce5d6a6223b2b7638985f604fa34c", size = 115931, upload-time = "2025-09-20T07:09:51.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/70/e1248dea2ab881eb76b61ff1ad6cb9c07ac005faf99349e4af0b29bc3f1b/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1f61c178766a1bbce0669f44790c6161ff4669404c477b4aedaa576348f9e102", size = 109561, upload-time = "2025-09-20T07:09:52.733Z" }, +] + +[[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/c9/15/ad05c28e049208c97011728e2debdb45439175f75efe357b6faa4c9ba099/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4", size = 90033, upload-time = "2025-06-06T07:00:23.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/48/074779081841f6eba4987930c4e7adcec38a5985b7dffd9fecc41f39a89c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89", size = 95824, upload-time = "2025-06-06T07:00:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/aa/25/e01966033a02b2d0718710bb47ef4f6b9b5a619ca2c857e06eb5c8e3ed13/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff", size = 89311, upload-time = "2025-06-06T07:00:25.029Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/27/3d/421d04a20037370baf13de929bc1dc5438b306a76fe17275ec5d893aae6c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win32.whl", hash = "sha256:2985565c265b3f9eab625361b0e40e88c94b03d89f5171f36146f2e88b3ee214", size = 92264, upload-time = "2025-09-20T07:09:53.563Z" }, + { url = "https://files.pythonhosted.org/packages/07/c7/43601ab82fe42bcff430b8466d84d92b31be06cc45c7fd64e9aac40f7851/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d102f3fac64fde32332e370969dfbc6f37b405d8cc055d9da30d14d07449a3c2", size = 97517, upload-time = "2025-09-20T07:09:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/e3303f6a25a2d98e424b06580fc85bbfd068f383424c67fa47cb1b357a46/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:ffeb5e946cd42c32c6999a62e240d6730c653cdfb7b49c7839afba375e20a62a", size = 94122, upload-time = "2025-09-20T07:09:55.187Z" }, +] + +[[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/9c/a1/75ac783a5faee9b455fef2f53b7fef97b21ed60d52401b44c690202141e4/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557", size = 183326, upload-time = "2025-06-06T07:00:52.662Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/a9dcc15322d2f5c7dfd491bd7ab121e36437caf78ebfa92bc0dd0546e2ca/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d", size = 187810, upload-time = "2025-06-06T07:00:53.594Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fc/47d00af076f558267097af3050910beda6bf8a21ceaa5830bbd26fcaf85e/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984", size = 184516, upload-time = "2025-06-06T07:00:55.24Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/b7/32/cb447ca7730a1e05730272309b074da6a04af29a8c0f5121014db8a2fc02/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win32.whl", hash = "sha256:d5f83739ca370f0baf52b0400aebd6240ab80150081fbfba60fd6e7b2e7b4c5f", size = 185249, upload-time = "2025-09-20T07:09:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fa/f465d5d44dda166bf7ec64b7a950f57eca61f165bfe18345e9a5ea542def/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:13786a5853a933de140d456cd818696e1121c7c296ae7b7af262fc5d2cffb851", size = 193739, upload-time = "2025-09-20T07:09:59.893Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/51c53ac3c704cd92da5ed7e7b9b57159052f6e46744e4f7e447ed708aa22/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:5140682da2860f6a55eb6faf9e980724dc457c2e4b4b35a10e1cebd8fc97d892", size = 194836, upload-time = "2025-09-20T07:10:00.87Z" }, +] + +[[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/31/3e/81642208ecd6c6c936f35a39a433c54e3f68e09d316546b8f953581ae334/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3", size = 130249, upload-time = "2025-06-06T07:02:02.237Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/a9ede5f3f0d86abfc7590726cf711133d97419b49ced372fca532e4f0696/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc", size = 141512, upload-time = "2025-06-06T07:02:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/4fad07c03124bdc3acd64f80f3bd3cc4417ea641e07bb16a9503afd3e554/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca", size = 135383, upload-time = "2025-06-06T07:02:04.312Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/41/31/5785cd1ec54dc0f0e6f3e6a466d07a62b8014a6e2b782e80444ef87e83ab/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win32.whl", hash = "sha256:e087364273ed7c717cd0191fed4be9def6fdf229fe9b536a4b8d0228f7814106", size = 134252, upload-time = "2025-09-20T07:10:12.935Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/68d91068048410f49794c0b19c45759c63ca559607068cfe5affba2f211b/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:0da1ddb8285d97a6775c36265d7157acf1bbcb88bcc9a7ce9a4549906c822472", size = 145509, upload-time = "2025-09-20T07:10:13.797Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a4/898951d5bfc474aa9c7d133fe30870f0f2184f4ba3027eafb779d30eb7bc/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:09bf07e74e897e97a49a9275d0a647819254ddb74142806bbbcf4777ed240a22", size = 141334, upload-time = "2025-09-20T07:10:14.637Z" }, +] + +[[package]] +name = "winrt-windows-devices-radios" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/e0/4731a3c412318b2c5e74a8803a32e2fb9afc2c98368c6b61a422eb359e7e/winrt_windows_devices_radios-3.2.1-cp312-cp312-win32.whl", hash = "sha256:c3e683ce682338a5a5ed465f735e223ba7a22f16d0bbea2d070962bc7657edbb", size = 38606, upload-time = "2025-06-06T07:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/91464854dfc9e0be9ce8dcbe2bd6a67c19b68ab91584fc5de0f4f13e78f8/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a116e552a3f38607b9be558fb2e7de9b4450d1f9080069944d74d80cdda1873e", size = 40172, upload-time = "2025-06-06T07:08:02.214Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0d/1bd62f606b6c4dfa936fccc4712be5506a40fc5d1b7177c3d3cbcaf30972/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4c28822f9251c9d547324f596b5c2581f050254ded05e5b786c650a3502744c1", size = 36989, upload-time = "2025-06-06T07:08:03.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/94/c22a14fd424632f3f3c0b25672218db9e8f4ae9e1355e0b148f2fe6015b5/winrt_windows_devices_radios-3.2.1-cp313-cp313-win32.whl", hash = "sha256:ae4a0065927fcd2d10215223f8a46be6fb89bad71cb4edd25dae3d01c137b3a8", size = 38613, upload-time = "2025-06-06T07:08:04.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/24cec0cc228642554b48d436a7617d7162fb952919c55fc26e2d99c310bd/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:bf1a975f46a2aa271ffea1340be0c7e64985050d07433e701343dddc22a72290", size = 40180, upload-time = "2025-06-06T07:08:04.849Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/776453af26e78c0d0c0e1bfa89f86fd81322872f31a3e5dafb344dd47bf2/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:10b298ed154c5824cea2de174afce1694ed2aabfb58826de814074027ffef96f", size = 36989, upload-time = "2025-06-06T07:08:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/76/79/4627afae6b389ddd1e5f1d691663c6b14d6c8f98959082aed1217cc57ef9/winrt_windows_devices_radios-3.2.1-cp314-cp314-win32.whl", hash = "sha256:21452e1cae50e44cd1d5e78159e1b9986ac3389b66458ad89caa196ce5eca2d6", size = 39521, upload-time = "2025-09-20T07:11:17.992Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/c6aea91908ee7279ed51d12157bc8aeecb8850af2441073c3c91b261ad31/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:6a8413e586fe597c6849607885cca7e0549da33ae5699165d11f7911534c6eaf", size = 41121, upload-time = "2025-09-20T07:11:18.747Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/652f14e3c501452ad8e0723518d9bbd729219b47f4a4dbe2966c2f82dca8/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:39129fd9d09103adb003575f59881c1a5a70a43310547850150b46c6f4020312", size = 38114, upload-time = "2025-09-20T07:11:19.599Z" }, +] + +[[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/f3/f8/495e304ddedd5ff2f196efbde906265cb75ade4d79e2937837f72ef654a0/winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b", size = 112169, upload-time = "2025-06-06T07:11:01.438Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5e/b5059e4ece095351c496c9499783130c302d25e353c18031d5231b1b3b3c/winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9", size = 118668, upload-time = "2025-06-06T07:11:02.475Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/acbcb3ef07b1b67e2de4afab9176a5282cfd775afd073efe6828dfc65ace/winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37", size = 109671, upload-time = "2025-06-06T07:11:03.538Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/d77346e39fe0c81f718cde49f83fe77c368c0e14c6418f72dfa1e7ef22d0/winrt_windows_foundation-3.2.1-cp314-cp314-win32.whl", hash = "sha256:35e973ab3c77c2a943e139302256c040e017fd6ff1a75911c102964603bba1da", size = 114590, upload-time = "2025-09-20T07:11:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/4d2b545bea0f34f68df6d4d4ca22950ff8a935497811dccdc0ca58737a05/winrt_windows_foundation-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22a7ebcec0d262e60119cff728f32962a02df60471ded8b2735a655eccc0ef5", size = 122148, upload-time = "2025-09-20T07:11:50.826Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ed/b9d3a11cac73444c0a3703200161cd7267dab5ab85fd00e1f965526e74a8/winrt_windows_foundation-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3be7fbae829b98a6a946db4fbaf356b11db1fbcbb5d4f37e7a73ac6b25de8b87", size = 114360, upload-time = "2025-09-20T07:11:51.626Z" }, +] + +[[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/1d/0b/7802349391466d3f7e8f62f588f36a1a0b6560abfcdbdaa426fe21d322b4/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6", size = 60060, upload-time = "2025-06-06T07:11:16.173Z" }, + { url = "https://files.pythonhosted.org/packages/37/94/5b888713e472746635a382e523513ab1b8200af55c5b56bc70e1e4369115/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b", size = 69058, upload-time = "2025-06-06T07:11:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/829273622c9b37c67b97f187b92be318404f7d33db045e31d72b7d50f54c/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84", size = 58793, upload-time = "2025-06-06T07:11:17.837Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/e1/47/b3301d964422d4611c181348149a7c5956a2a76e6339de451a000d4ae8e7/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win32.whl", hash = "sha256:33188ed2d63e844c8adfbb82d1d3d461d64aaf78d225ce9c5930421b413c45ab", size = 62211, upload-time = "2025-09-20T07:11:52.411Z" }, + { url = "https://files.pythonhosted.org/packages/20/59/5f2c940ff606297129e93ebd6030c813e6a43a786de7fc33ccb268e0b06b/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d4cfece7e9c0ead2941e55a1da82f20d2b9c8003bb7a8853bb7f999b539f80a4", size = 70399, upload-time = "2025-09-20T07:11:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/2c8eb89062c71d4be73d618457ed68e7e2ba29a660ac26349d44fc121cbf/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3884146fea13727510458f6a14040b7632d5d90127028b9bfd503c6c655d0c01", size = 61392, upload-time = "2025-09-20T07:11:53.993Z" }, +] + +[[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/87/e7/7d3f2a4a442f264e05cab2bdf20ed1b95cb3f753bd1b0f277f2b49fb8335/winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78", size = 127787, upload-time = "2025-06-06T14:02:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2f/cc36f475f8af293f40e2c2a5d6c2e75a189c2c2d4d01ecb3551578518c79/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc", size = 131849, upload-time = "2025-06-06T14:02:03.09Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/896fb734f7456910ec412f3f3adfdc3f0dc3134864a496d5b120592f3bfd/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307", size = 128144, upload-time = "2025-06-06T14:02:03.946Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/55/70/2869ea2112c565caace73c9301afd1d7afcc49bdd37fac058f0178ba95d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win32.whl", hash = "sha256:5cd0dbad86fcc860366f6515fce97177b7eaa7069da261057be4813819ba37ee", size = 131701, upload-time = "2025-09-20T07:17:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/aae50b1d0e37b5a61055759aedd42c6c99d7c17ab8c3e568ab33c0288938/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5bf41d725369b9986e6d64bad7079372b95c329897d684f955d7028c7f27a0", size = 135566, upload-time = "2025-09-20T07:17:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c3/6d3ce7a58e6c828e0795c9db8790d0593dd7fdf296e513c999150deb98d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:293e09825559d0929bbe5de01e1e115f7a6283d8996ab55652e5af365f032987", size = 134393, upload-time = "2025-09-20T07:17:18.802Z" }, +]