pax_global_header00006660000000000000000000000064150265721260014520gustar00rootroot0000000000000052 comment=5a4ef36b0e8cf6819da525204f84bbb73d39d6be airthings-ble-1.1.0/000077500000000000000000000000001502657212600142475ustar00rootroot00000000000000airthings-ble-1.1.0/.github/000077500000000000000000000000001502657212600156075ustar00rootroot00000000000000airthings-ble-1.1.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001502657212600177725ustar00rootroot00000000000000airthings-ble-1.1.0/.github/ISSUE_TEMPLATE/1-bug_report.md000066400000000000000000000004221502657212600226200ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve labels: bug --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Additional context** Add any other context about the problem here. airthings-ble-1.1.0/.github/ISSUE_TEMPLATE/2-feature-request.md000066400000000000000000000006721502657212600236010ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project labels: enhancement --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. airthings-ble-1.1.0/.github/auto_assign-issues.yml000066400000000000000000000004611502657212600221600ustar00rootroot00000000000000# If enabled, auto-assigns users when a new issue is created # Defaults to true, allows you to install the app globally, and disable on a per-repo basis addAssignees: true # The list of users to assign to new issues. # If empty or not provided, the repository owner is assigned assignees: - LaStrada airthings-ble-1.1.0/.github/labels.toml000066400000000000000000000035151502657212600177520ustar00rootroot00000000000000[breaking] color = "ffcc00" name = "breaking" description = "Breaking change." [bug] color = "d73a4a" name = "bug" description = "Something isn't working" [dependencies] color = "0366d6" name = "dependencies" description = "Pull requests that update a dependency file" [github_actions] color = "000000" name = "github_actions" description = "Update of github actions" [documentation] color = "1bc4a5" name = "documentation" description = "Improvements or additions to documentation" [duplicate] color = "cfd3d7" name = "duplicate" description = "This issue or pull request already exists" [enhancement] color = "a2eeef" name = "enhancement" description = "New feature or request" ["good first issue"] color = "7057ff" name = "good first issue" description = "Good for newcomers" ["help wanted"] color = "008672" name = "help wanted" description = "Extra attention is needed" [invalid] color = "e4e669" name = "invalid" description = "This doesn't seem right" [nochangelog] color = "555555" name = "nochangelog" description = "Exclude pull requests from changelog" [question] color = "d876e3" name = "question" description = "Further information is requested" [removed] color = "e99695" name = "removed" description = "Removed piece of functionalities." [tests] color = "bfd4f2" name = "tests" description = "CI, CD and testing related changes" [wontfix] color = "ffffff" name = "wontfix" description = "This will not be worked on" [discussion] color = "c2e0c6" name = "discussion" description = "Some discussion around the project" [hacktoberfest] color = "ffa663" name = "hacktoberfest" description = "Good issues for Hacktoberfest" [answered] color = "0ee2b6" name = "answered" description = "Automatically closes as answered after a delay" [waiting] color = "5f7972" name = "waiting" description = "Automatically closes if no answer after a delay" airthings-ble-1.1.0/.github/workflows/000077500000000000000000000000001502657212600176445ustar00rootroot00000000000000airthings-ble-1.1.0/.github/workflows/cd.yml000066400000000000000000000050361502657212600207610ustar00rootroot00000000000000name: Publish to PyPI and GitHub on: workflow_dispatch: inputs: tag: description: New version tag required: true prerelease: description: Is this a pre-release? required: false default: false type: boolean jobs: final-linting: uses: ./.github/workflows/ci.yml build-publish: name: Build and publish to PyPI runs-on: ubuntu-latest needs: final-linting environment: name: release url: https://pypi.org/p/airthings-ble permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing contents: write steps: - name: Checkout source uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Update pyproject.toml Version run: | pip install tomlkit python -c " import tomlkit tag = '${{ github.event.inputs.tag }}' with open('pyproject.toml', 'r') as file: content = tomlkit.parse(file.read()) content['project']['version'] = tag with open('pyproject.toml', 'w') as file: file.write(tomlkit.dumps(content)) " - name: Update __init__.py version run: | version="${{ github.event.inputs.tag }}" file="airthings_ble/__init__.py" sed -i "s/__version__\\s*=\\s*[\"'].*[\"']/__version__ = \"$version\"/" $file - name: Commit and Push Changes run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "GitHub Actions" git add pyproject.toml airthings_ble/__init__.py git commit -m "Update version to ${{ github.event.inputs.tag }}" git push git tag ${{ github.event.inputs.tag }} git push --tags - name: Build source and wheel distributions run: | python -m pip install --upgrade build twine python -m build twine check --strict dist/* - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - name: Release uses: softprops/action-gh-release@v2 with: files: dist/${{ env.name }}/* tag_name: ${{ github.event.inputs.tag }} generate_release_notes: true prerelease: ${{ github.event.inputs.prerelease }} airthings-ble-1.1.0/.github/workflows/ci.yml000066400000000000000000000025551502657212600207710ustar00rootroot00000000000000name: CI on: pull_request: workflow_call: # Python Versions Reference: https://github.com/home-assistant/architecture/blob/master/adr/0002-minimum-supported-python-version.md jobs: test: strategy: matrix: python-version: [3.12, 3.13] name: Run tests for ${{ matrix.python-version }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 - name: Install Dependencies run: poetry install - name: Run tests run: poetry run pytest lint: strategy: matrix: python-version: [3.13] # Linting is only required for the latest Python version name: Lint code for ${{ matrix.python-version }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 - name: Install Dependencies run: poetry install - name: Check formatting run: poetry run black --check airthings_ble - name: Check code style run: poetry run pylint airthings_ble - name: Check types run: poetry run mypy airthings_ble airthings-ble-1.1.0/.github/workflows/labels.yml000066400000000000000000000010311502657212600216240ustar00rootroot00000000000000name: Sync Github labels on: push: branches: - main paths: - ".github/**" jobs: labels: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install labels run: pip install labels - name: Sync config with Github run: labels -u ${{ github.repository_owner }} -t ${{ secrets.GITHUB_TOKEN }} sync -f .github/labels.toml airthings-ble-1.1.0/.gitignore000066400000000000000000000006511502657212600162410ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Cache .mypy_cache .pytest_cache # Installer logs pip-log.txt pip-delete-this-directory.txt # pyenv .python-version # Environments .env* # Misc .vscode .idea # Tests .coverage airthings-ble-1.1.0/LICENSE000066400000000000000000000020561502657212600152570ustar00rootroot00000000000000MIT License Copyright (c) 2024 Airthings ASA Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. airthings-ble-1.1.0/README.md000066400000000000000000000033171502657212600155320ustar00rootroot00000000000000# airthings-ble Library to control Airthings devices through BLE, primarily meant to be used in the [Home Assistant integration](https://www.home-assistant.io/integrations/airthings_ble/). ## Supported devices This library supports the following Airthings devices: - [Corentium Home 2](https://www.airthings.com/products/corentium-home-2) - [Wave Enhance](https://www.airthings.com/wave-enhance) - Wave Gen 1 - [Wave Mini](https://www.airthings.com/wave-mini) - [Wave Plus](https://www.airthings.com/wave-plus) - [Wave Radon](https://www.airthings.com/wave-radon) ## Unsupported devices Although some other devices have BLE capabilities, they use BLE only for onboarding and configuration. It is **not** possible to fetch sensor data using this library from, for example: - Hub - [Renew](https://www.airthings.com/renew) - [View Plus](https://www.airthings.com/view-plus) - View Pollution - [View Radon](https://www.airthings.com/view-radon) ## Getting Started Prerequisites: - [Python](https://www.python.org/downloads/) with version 3.12 that is required by Home Assistant ([docs](https://developers.home-assistant.io/docs/development_environment?_highlight=python&_highlight=versi#manual-environment) or [reference](https://github.com/home-assistant/architecture/blob/master/adr/0002-minimum-supported-python-version.md)) - [Poetry](https://python-poetry.org/docs/#installation) Install dependencies: ```bash poetry install ``` Run tests: ```bash poetry run pytest ``` See [this wiki page](https://github.com/Airthings/airthings-ble/wiki/Testing-with-Home-Assistant) for more details on how to test the library with HA. ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. airthings-ble-1.1.0/airthings_ble/000077500000000000000000000000001502657212600170615ustar00rootroot00000000000000airthings-ble-1.1.0/airthings_ble/__init__.py000066400000000000000000000006061502657212600211740ustar00rootroot00000000000000"""Parser for Airthings BLE advertisements.""" from __future__ import annotations from .device_type import AirthingsDeviceType from .parser import ( AirthingsBluetoothDeviceData, AirthingsDevice, UnsupportedDeviceError, ) __version__ = "1.1.0" __all__ = [ "AirthingsBluetoothDeviceData", "AirthingsDevice", "AirthingsDeviceType", "UnsupportedDeviceError", ] airthings-ble-1.1.0/airthings_ble/airthings_firmware.py000066400000000000000000000041241502657212600233200ustar00rootroot00000000000000"""Airthings firmware version handling.""" import re class AirthingsFirmwareVersion: """Firmware information for the Airthings device.""" current_version: tuple[int, int, int] | None = None required_version: tuple[int, int, int] | None = None def __init__( self, current_version: str | None = None, required_version: str | None = None, ) -> None: self.current_version = self._version_as_tuple(current_version) self.required_version = self._version_as_tuple(required_version) def update_current_version(self, current_version: str | None) -> None: """Update the current version.""" self.current_version = self._version_as_tuple(current_version) def update_required_version(self, required_version: str | None) -> None: """Update the required version.""" self.required_version = self._version_as_tuple(required_version) def _version_as_tuple(self, version: str | None) -> tuple[int, int, int] | None: """Convert version string to tuple.""" if not version: return None # Semantic version format: "X.Y.Z" if re.match(r"^\d+\.\d+\.\d+$", version): semantic_version = re.compile(r"(\d+)\.(\d+)\.(\d+)") match_obj = semantic_version.match(version) if not match_obj: return None major, minor, patch = match_obj.groups() return int(major), int(minor), int(patch) # Airthings version format: # "T-SUB-2.6.0-master+0" # "R-SUB-1.3.5-master+0" airthings_version = re.compile(r"^[A-Z]-SUB-(\d+)\.(\d+)\.(\d+)-.*") match_obj = airthings_version.match(version) if not match_obj: return None major, minor, patch = match_obj.groups() return int(major), int(minor), int(patch) @property def need_firmware_upgrade(self) -> bool: """Check if the device needs an update.""" if not self.current_version or not self.required_version: return False return self.current_version < self.required_version airthings-ble-1.1.0/airthings_ble/atom/000077500000000000000000000000001502657212600200215ustar00rootroot00000000000000airthings-ble-1.1.0/airthings_ble/atom/request.py000066400000000000000000000015201502657212600220610ustar00rootroot00000000000000import os from airthings_ble.atom.request_path import AtomRequestPath class AtomRequest: """Request for Airthings BLE Atom API""" url: AtomRequestPath random_bytes: bytes def __init__(self, url: AtomRequestPath, random_bytes: bytes | None = None) -> None: self.url = url if random_bytes is not None: if len(random_bytes) != 2: raise ValueError("Random bytes must be exactly 2 bytes long") self.random_bytes = random_bytes else: self.random_bytes = os.urandom(2) def as_bytes(self) -> bytes: """Get request as bytes""" bytes = bytearray() bytes.extend(bytes.fromhex("0301")) bytes.extend(self.random_bytes) bytes.extend(bytes.fromhex("81A100")) bytes.extend(self.url.as_cbor()) return bytes airthings-ble-1.1.0/airthings_ble/atom/request_path.py000066400000000000000000000005631502657212600231030ustar00rootroot00000000000000from enum import StrEnum import cbor2 class AtomRequestPath(StrEnum): """Request paths for Airthings BLE Atom API""" LATEST_VALUES = "29999/0/31012" def as_cbor(self) -> bytes: """Get URL as bytes""" return cbor2.dumps(self.value) def as_bytes(self) -> bytes: """Get URL as bytes""" return bytes(self.value, "utf-8") airthings-ble-1.1.0/airthings_ble/atom/response.py000066400000000000000000000066051502657212600222400ustar00rootroot00000000000000from logging import Logger import cbor2 from airthings_ble.atom.request import AtomRequestPath class AtomResponse: """Response for Airthings BLE Atom API""" _header = bytearray.fromhex("1001000345") response: bytes random_bytes: bytes path: AtomRequestPath def __init__( self, logger: Logger, response: bytes | None, random_bytes: bytes, path: AtomRequestPath, ) -> None: self.logger = logger if response is None: raise ValueError("Response cannot be None") self.response = response self.random_bytes = random_bytes self.path = path def parse(self) -> dict[str, float | str | None] | None: if self.response[0:5] != self._header: self.logger.error( "Invalid response header, expected %s, but got %s", self._header.hex(), self.response[0:5].hex(), ) raise ValueError("Invalid response header") if self.response[5:7] != self.random_bytes: self.logger.debug( "Invalid response checksum, expected %s, but got %s", self.random_bytes.hex(), self.response[5:7].hex(), ) raise ValueError("Invalid response checksum") if self.response[7] != 0x81: self.logger.debug( "Invalid response type, expected 81, but got %s", self.response[7] ) raise ValueError("Invalid response type") if self.response[8] != 0xA2: self.logger.debug( "Invalid response array length, expected 2, but got %s", self.response[8], ) raise ValueError("Invalid response array length, expected 2") if self.response[9] != 0x00: self.logger.debug( "Invalid response data type, expected 00, but got %s", self.response[9] ) raise ValueError("Invalid response element") path_bytes = self.path.as_bytes() path_length = self.response[10] - 0x60 if path_length != len(self.path.value): self.logger.debug( "Invalid path length, expected %d, but got %d", len(path_bytes), path_length, ) raise ValueError( f"Invalid response path length, expected {len(path_bytes)}, " f"got {path_length}" ) if self.response[11 : 11 + path_length] != path_bytes: self.logger.debug( "Invalid response path, expected %s, but got %s", path_bytes, self.response[11 : 11 + path_length], ) raise ValueError("Invalid response path") try: self.logger.debug("Response: %s", self.response.hex()) data_bytes = self.response[27:] decoded_data = cbor2.loads(data_bytes) self.logger.debug("Decoded data: %s", decoded_data) if not isinstance(decoded_data, dict): self.logger.error( "Parsed data is not a dictionary, but a %s", type(decoded_data) ) raise ValueError("Invalid response data type") return decoded_data except ValueError as e: self.logger.error(f"Failed to parse response: {e}") return None airthings-ble-1.1.0/airthings_ble/const.py000066400000000000000000000044321502657212600205640ustar00rootroot00000000000000"""Constants for Airthings BLE parser""" from uuid import UUID MFCT_ID = 820 UPDATE_TIMEOUT = 15 # Use full UUID since we do not use UUID from bluetooth library CHAR_UUID_MANUFACTURER_NAME = UUID("00002a29-0000-1000-8000-00805f9b34fb") CHAR_UUID_SERIAL_NUMBER_STRING = UUID("00002a25-0000-1000-8000-00805f9b34fb") CHAR_UUID_MODEL_NUMBER_STRING = UUID("00002a24-0000-1000-8000-00805f9b34fb") CHAR_UUID_DEVICE_NAME = UUID("00002a00-0000-1000-8000-00805f9b34fb") CHAR_UUID_FIRMWARE_REV = UUID("00002a26-0000-1000-8000-00805f9b34fb") CHAR_UUID_HARDWARE_REV = UUID("00002a27-0000-1000-8000-00805f9b34fb") CHAR_UUID_DATETIME = UUID("00002a08-0000-1000-8000-00805f9b34fb") CHAR_UUID_TEMPERATURE = UUID("00002a6e-0000-1000-8000-00805f9b34fb") CHAR_UUID_HUMIDITY = UUID("00002a6f-0000-1000-8000-00805f9b34fb") CHAR_UUID_RADON_1DAYAVG = UUID("b42e01aa-ade7-11e4-89d3-123b93f75cba") CHAR_UUID_RADON_LONG_TERM_AVG = UUID("b42e0a4c-ade7-11e4-89d3-123b93f75cba") CHAR_UUID_ILLUMINANCE_ACCELEROMETER = UUID("b42e1348-ade7-11e4-89d3-123b93f75cba") CHAR_UUID_WAVE_PLUS_DATA = UUID("b42e2a68-ade7-11e4-89d3-123b93f75cba") CHAR_UUID_WAVE_2_DATA = UUID("b42e4dcc-ade7-11e4-89d3-123b93f75cba") CHAR_UUID_WAVEMINI_DATA = UUID("b42e3b98-ade7-11e4-89d3-123b93f75cba") COMMAND_UUID_WAVE_2 = UUID("b42e50d8-ade7-11e4-89d3-123b93f75cba") COMMAND_UUID_WAVE_PLUS = UUID("b42e2d06-ade7-11e4-89d3-123b93f75cba") COMMAND_UUID_WAVE_MINI = UUID("b42e3ef4-ade7-11e4-89d3-123b93f75cba") # Atom COMMAND_UUID_ATOM_NOTIFY = UUID("b42ebc9e-ade7-11e4-89d3-123b93f75cba") COMMAND_UUID_ATOM = UUID("b42eb73a-ade7-11e4-89d3-123b93f75cba") """ 0 - 49 Bq/m3 (0 - 1.3 pCi/L): No action necessary. 50 - 99 Bq/m3 (1.4 - 2.6 pCi/L): Experiment with ventilation and sealing cracks to reduce levels. 100 Bq/m3 - 299 Bq/m3 (2.7 - 8 pCi/L): Keep measuring. If levels are maintained for more than 3 months, contact a professional radon mitigator. 300 Bq/m3 (8.1 pCi/L) and up: Keep measuring. If levels are maintained for more than 1 month, contact a professional radon mitigator. """ VERY_LOW = (0, 49, "very low") LOW = (50, 99, "low") MODERATE = (100, 299, "moderate") HIGH = (300, None, "high") BQ_TO_PCI_MULTIPLIER = 0.027 CO2_MAX = 65534 VOC_MAX = 65534 PERCENTAGE_MAX = 100 PRESSURE_MAX = 1310 RADON_MAX = 16383 TEMPERATURE_MAX = 100 DEFAULT_MAX_UPDATE_ATTEMPTS = 1 airthings-ble-1.1.0/airthings_ble/device_type.py000066400000000000000000000124651502657212600217430ustar00rootroot00000000000000"""Airthings device types.""" import logging from enum import Enum from airthings_ble.airthings_firmware import AirthingsFirmwareVersion _LOGGER = logging.getLogger(__name__) class AirthingsDeviceType(Enum): """Airthings device types.""" UNKNOWN = "0" WAVE_GEN_1 = "2900" WAVE_MINI = "2920" WAVE_PLUS = "2930" WAVE_RADON = "2950" WAVE_ENHANCE_EU = "3210" WAVE_ENHANCE_US = "3220" CORENTIUM_HOME_2 = "3250" raw_value: str # pylint: disable=invalid-name def __new__(cls, value: str) -> "AirthingsDeviceType": """Create new device type.""" obj = object.__new__(cls) obj.raw_value = value return obj @classmethod def atom_devices(cls) -> list["AirthingsDeviceType"]: """Get list of Airthings Atom devices.""" return [ AirthingsDeviceType.WAVE_ENHANCE_EU, AirthingsDeviceType.WAVE_ENHANCE_US, AirthingsDeviceType.CORENTIUM_HOME_2, ] @classmethod def from_raw_value(cls, value: str) -> "AirthingsDeviceType": """Get device type from raw value.""" for device_type in cls: if device_type.value == value: device_type.raw_value = value return device_type unknown_device = AirthingsDeviceType.UNKNOWN unknown_device.raw_value = value return unknown_device @property # pylint: disable=too-many-return-statements def product_name(self) -> str: """Get product name.""" if self == AirthingsDeviceType.WAVE_GEN_1: return "Wave Gen 1" if self == AirthingsDeviceType.WAVE_MINI: return "Wave Mini" if self == AirthingsDeviceType.WAVE_PLUS: return "Wave Plus" if self == AirthingsDeviceType.WAVE_RADON: return "Wave Radon" if self in ( AirthingsDeviceType.WAVE_ENHANCE_EU, AirthingsDeviceType.WAVE_ENHANCE_US, ): return "Wave Enhance" if self == AirthingsDeviceType.CORENTIUM_HOME_2: return "Corentium Home 2" return "Unknown" def battery_percentage(self, voltage: float) -> int: """Calculate battery percentage based on voltage.""" if self == AirthingsDeviceType.WAVE_MINI: return round(self._three_batteries(voltage)) return round(self._two_batteries(voltage)) # pylint: disable=too-many-return-statements def _two_batteries(self, voltage: float) -> float: if voltage >= 3.00: return 100 if 2.80 <= voltage < 3.00: return self._interpolate( voltage=voltage, voltage_range=(2.80, 3.00), percentage_range=(81, 100) ) if 2.60 <= voltage < 2.80: return self._interpolate( voltage=voltage, voltage_range=(2.60, 2.80), percentage_range=(53, 81) ) if 2.50 <= voltage < 2.60: return self._interpolate( voltage=voltage, voltage_range=(2.50, 2.60), percentage_range=(28, 53) ) if 2.20 <= voltage < 2.50: return self._interpolate( voltage=voltage, voltage_range=(2.20, 2.50), percentage_range=(5, 28) ) if 2.10 <= voltage < 2.20: return self._interpolate( voltage=voltage, voltage_range=(2.10, 2.20), percentage_range=(0, 5) ) return 0 # pylint: disable=too-many-return-statements def _three_batteries(self, voltage: float) -> float: if voltage >= 4.50: return 100 if 4.20 <= voltage < 4.50: return self._interpolate( voltage=voltage, voltage_range=(4.20, 4.50), percentage_range=(85, 100) ) if 3.90 <= voltage < 4.20: return self._interpolate( voltage=voltage, voltage_range=(3.90, 4.20), percentage_range=(62, 85) ) if 3.75 <= voltage < 3.90: return self._interpolate( voltage=voltage, voltage_range=(3.75, 3.90), percentage_range=(42, 62) ) if 3.30 <= voltage < 3.75: return self._interpolate( voltage=voltage, voltage_range=(3.30, 3.75), percentage_range=(23, 42) ) if 2.40 <= voltage < 3.30: return self._interpolate( voltage=voltage, voltage_range=(2.40, 3.30), percentage_range=(0, 23) ) return 0 def _interpolate( self, voltage: float, voltage_range: tuple[float, float], percentage_range: tuple[int, int], ) -> float: return (voltage - voltage_range[0]) / (voltage_range[1] - voltage_range[0]) * ( percentage_range[1] - percentage_range[0] ) + percentage_range[0] def need_firmware_upgrade(self, current_version: str) -> "AirthingsFirmwareVersion": """Check if the device needs an update.""" version = AirthingsFirmwareVersion(current_version=current_version) if self in ( AirthingsDeviceType.WAVE_ENHANCE_EU, AirthingsDeviceType.WAVE_ENHANCE_US, ): version.update_required_version("T-SUB-2.6.1-master+0") return version if self == AirthingsDeviceType.CORENTIUM_HOME_2: version.update_required_version("R-SUB-1.3.4-master+0") return version airthings-ble-1.1.0/airthings_ble/parser.py000066400000000000000000000767621502657212600207510ustar00rootroot00000000000000"""Parser for Airthings BLE devices""" from __future__ import annotations import asyncio import dataclasses import re import struct import sys from collections import namedtuple from datetime import datetime from functools import partial from logging import Logger from typing import Any, Callable, Optional, Tuple from async_interrupt import interrupt from bleak import BleakClient, BleakError from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTService from bleak_retry_connector import BleakClientWithServiceCache, establish_connection from airthings_ble.airthings_firmware import AirthingsFirmwareVersion from airthings_ble.atom.request import AtomRequest from airthings_ble.atom.request_path import AtomRequestPath from airthings_ble.atom.response import AtomResponse from .const import ( BQ_TO_PCI_MULTIPLIER, CHAR_UUID_DATETIME, CHAR_UUID_DEVICE_NAME, CHAR_UUID_FIRMWARE_REV, CHAR_UUID_HARDWARE_REV, CHAR_UUID_HUMIDITY, CHAR_UUID_ILLUMINANCE_ACCELEROMETER, CHAR_UUID_MANUFACTURER_NAME, CHAR_UUID_MODEL_NUMBER_STRING, CHAR_UUID_RADON_1DAYAVG, CHAR_UUID_RADON_LONG_TERM_AVG, CHAR_UUID_SERIAL_NUMBER_STRING, CHAR_UUID_TEMPERATURE, CHAR_UUID_WAVE_2_DATA, CHAR_UUID_WAVE_PLUS_DATA, CHAR_UUID_WAVEMINI_DATA, CO2_MAX, COMMAND_UUID_ATOM, COMMAND_UUID_ATOM_NOTIFY, COMMAND_UUID_WAVE_2, COMMAND_UUID_WAVE_MINI, COMMAND_UUID_WAVE_PLUS, DEFAULT_MAX_UPDATE_ATTEMPTS, HIGH, LOW, MODERATE, PERCENTAGE_MAX, PRESSURE_MAX, RADON_MAX, TEMPERATURE_MAX, UPDATE_TIMEOUT, VERY_LOW, VOC_MAX, ) from .device_type import AirthingsDeviceType if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout else: from asyncio import timeout as asyncio_timeout Characteristic = namedtuple("Characteristic", ["uuid", "name", "format"]) wave_gen_1_device_info_characteristics = [ Characteristic(CHAR_UUID_MANUFACTURER_NAME, "manufacturer", "utf-8"), Characteristic(CHAR_UUID_SERIAL_NUMBER_STRING, "serial_nr", "utf-8"), Characteristic(CHAR_UUID_DEVICE_NAME, "device_name", "utf-8"), Characteristic(CHAR_UUID_FIRMWARE_REV, "firmware_rev", "utf-8"), ] device_info_characteristics = wave_gen_1_device_info_characteristics + [ Characteristic(CHAR_UUID_HARDWARE_REV, "hardware_rev", "utf-8"), ] _CHARS_BY_MODELS = { "2900": wave_gen_1_device_info_characteristics, } sensors_characteristics_uuid = [ CHAR_UUID_DATETIME, CHAR_UUID_TEMPERATURE, CHAR_UUID_HUMIDITY, CHAR_UUID_RADON_1DAYAVG, CHAR_UUID_RADON_LONG_TERM_AVG, CHAR_UUID_ILLUMINANCE_ACCELEROMETER, CHAR_UUID_WAVE_PLUS_DATA, CHAR_UUID_WAVE_2_DATA, CHAR_UUID_WAVEMINI_DATA, COMMAND_UUID_WAVE_2, COMMAND_UUID_WAVE_PLUS, COMMAND_UUID_WAVE_MINI, COMMAND_UUID_ATOM, ] sensors_characteristics_uuid_str = [str(x) for x in sensors_characteristics_uuid] class DisconnectedError(Exception): """Disconnected from device.""" class UnsupportedDeviceError(Exception): """Unsupported device.""" def _decode_base( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, Tuple[float, ...]]]: def handler(raw_data: bytearray) -> dict[str, Tuple[float, ...]]: val = struct.unpack(format_type, raw_data) if len(val) == 1: res = val[0] * scale else: res = val return {name: res} return handler def _decode_attr( name: str, format_type: str, scale: float, max_value: Optional[float] = None ) -> Callable[[bytearray], dict[str, float | None | str]]: """same as base decoder, but expects only one value.. for real""" def handler(raw_data: bytearray) -> dict[str, float | None | str]: val = struct.unpack(format_type, raw_data) res: float | None = None if len(val) == 1: res = val[0] * scale if res is not None and max_value is not None: # Verify that the result is not above the maximum allowed value if res > max_value: res = None data: dict[str, float | None | str] = {name: res} return data return handler def _decode_wave_plus( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, float | None | str]]: def handler(raw_data: bytearray) -> dict[str, float | None | str]: vals = _decode_base(name, format_type, scale)(raw_data) val = vals[name] data: dict[str, float | None | str] = {} data["date_time"] = str(datetime.isoformat(datetime.now())) data["humidity"] = validate_value(value=val[1] / 2.0, max_value=PERCENTAGE_MAX) data["illuminance"] = illuminance_converter(value=val[2]) data["radon_1day_avg"] = validate_value(value=val[4], max_value=RADON_MAX) data["radon_longterm_avg"] = validate_value(value=val[5], max_value=RADON_MAX) data["temperature"] = validate_value( value=val[6] / 100.0, max_value=TEMPERATURE_MAX ) data["pressure"] = validate_value(val[7] / 50.0, max_value=PRESSURE_MAX) data["co2"] = validate_value(value=val[8] * 1.0, max_value=CO2_MAX) data["voc"] = validate_value(value=val[9] * 1.0, max_value=VOC_MAX) return data return handler def _decode_wave_radon( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, float | None | str]]: def handler(raw_data: bytearray) -> dict[str, float | None | str]: vals = _decode_base(name, format_type, scale)(raw_data) val = vals[name] data: dict[str, float | None | str] = {} data["date_time"] = str(datetime.isoformat(datetime.now())) data["illuminance"] = illuminance_converter(value=val[2]) data["humidity"] = validate_value(value=val[1] / 2.0, max_value=PERCENTAGE_MAX) data["radon_1day_avg"] = validate_value(value=val[4], max_value=RADON_MAX) data["radon_longterm_avg"] = validate_value(value=val[5], max_value=RADON_MAX) data["temperature"] = validate_value( value=val[6] / 100.0, max_value=TEMPERATURE_MAX ) return data return handler def _decode_wave_mini( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, float | None | str]]: def handler(raw_data: bytearray) -> dict[str, float | None | str]: vals = _decode_base(name, format_type, scale)(raw_data) val = vals[name] data: dict[str, float | None | str] = {} data["date_time"] = str(datetime.isoformat(datetime.now())) data["temperature"] = validate_value( value=round(val[2] / 100.0 - 273.15, 2), max_value=TEMPERATURE_MAX ) data["pressure"] = float(val[3] / 50.0) data["humidity"] = validate_value( value=val[4] / 100.0, max_value=PERCENTAGE_MAX ) data["voc"] = validate_value(value=val[5] * 1.0, max_value=VOC_MAX) return data return handler def _decode_wave( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, float | None | str]]: def handler(raw_data: bytearray) -> dict[str, float | None | str]: vals = _decode_base(name, format_type, scale)(raw_data) val = vals[name] data: dict[str, float | None | str] = { name: str( datetime( int(val[0]), int(val[1]), int(val[2]), int(val[3]), int(val[4]), int(val[5]), ).isoformat() ) } return data return handler def _decode_wave_illum_accel( name: str, format_type: str, scale: float ) -> Callable[[bytearray], dict[str, float | None | str]]: def handler(raw_data: bytearray) -> dict[str, float | None | str]: vals = _decode_base(name, format_type, scale)(raw_data) val = vals[name] data: dict[str, float | None | str] = {} data["illuminance"] = illuminance_converter(val[0] * scale) data["accelerometer"] = str(val[1] * scale) return data return handler def validate_value(value: float, max_value: float) -> Optional[float]: """Validate if the given 'value' is within the specified range [min, max]""" min_value = 0 if min_value <= value <= max_value: return value return None def illuminance_converter(value: float) -> Optional[int]: """Convert illuminance from a 8-bit value to percentage.""" if (validated := validate_value(value, max_value=255)) is not None: return int(validated / 255 * PERCENTAGE_MAX) return None class CommandDecode: """Decoder for the command response""" cmd: bytes = b"\x6d" format_type: str def decode_data( self, logger: Logger, raw_data: bytearray | None, # pylint: disable=unused-argument ) -> dict[str, float | str | None] | None: """Decoder returns dict with battery""" logger.debug("Command decoder not implemented") return {} def validate_data( self, logger: Logger, raw_data: bytearray | None ) -> Optional[Any]: """Validate data. Make sure the data is for the command.""" if raw_data is None: logger.debug("Validate data: No data received") return None cmd = raw_data[0:1] if cmd != self.cmd: logger.warning( "Result for wrong command received, expected %s got %s", self.cmd.hex(), cmd.hex(), ) return None if len(raw_data[2:]) != struct.calcsize(self.format_type): logger.warning( "Wrong length data received (%s) versus expected (%s)", len(raw_data[2:]), struct.calcsize(self.format_type), ) return None return struct.unpack(self.format_type, raw_data[2:]) def make_data_receiver(self) -> _NotificationReceiver: """Creates a notification receiver for the command.""" return _NotificationReceiver(struct.calcsize(self.format_type)) class WaveRadonAndPlusCommandDecode(CommandDecode): """Decoder for the Wave Plus command response""" def __init__(self) -> None: """Initialize command decoder""" self.format_type = " dict[str, float | str | None] | None: """Decoder returns dict with battery""" if val := self.validate_data(logger, raw_data): res = {} res["battery"] = val[13] / 1000.0 return res return None class WaveMiniCommandDecode(CommandDecode): """Decoder for the Wave Radon command response""" def __init__(self) -> None: """Initialize command decoder""" self.format_type = "<2L4B2HL4HL" def decode_data( self, logger: Logger, raw_data: bytearray | None ) -> dict[str, float | str | None] | None: """Decoder returns dict with battery""" if val := self.validate_data(logger, raw_data): res = {} res["battery"] = val[11] / 1000.0 return res return None class AtomCommandDecode(CommandDecode): """Decoder for the Atom command response""" def __init__(self) -> None: """Initialize command decoder""" self.format_type = "" self.request = AtomRequest(url=AtomRequestPath.LATEST_VALUES) self.cmd = self.request.as_bytes() def decode_data( self, logger: Logger, raw_data: bytearray | None ) -> dict[str, float | str | None] | None: """Decoder returns dict with battery""" try: response = AtomResponse( logger=logger, response=raw_data, random_bytes=self.request.random_bytes, path=self.request.url, ) return response.parse() except ValueError as err: logger.error("Failed to decode command response: %s", err) return None class _NotificationReceiver: """Receiver for a single notification message. A notification message that is larger than the MTU can get sent over multiple packets. This receiver knows how to reconstruct it. """ message: bytearray | None def __init__(self, message_size: int): self.message = None self._message_size = message_size self._loop = asyncio.get_running_loop() self._future: asyncio.Future[None] = self._loop.create_future() def _full_message_received(self) -> bool: return self.message is not None and len(self.message) >= self._message_size def __call__(self, _: Any, data: bytearray) -> None: if self.message is None: self.message = data elif not self._full_message_received(): self.message += data if self._full_message_received(): self._future.set_result(None) def _on_timeout(self) -> None: if not self._future.done(): self._future.set_exception( asyncio.TimeoutError("Timeout waiting for message") ) async def wait_for_message(self, timeout: float) -> None: """Waits until the full message is received. If the full message has already been received, this method returns immediately. """ if not self._full_message_received(): timer_handle = self._loop.call_later(timeout, self._on_timeout) try: await self._future finally: timer_handle.cancel() def get_radon_level(data: float) -> str: """Returns the applicable radon level""" if data <= VERY_LOW[1]: radon_level = VERY_LOW[2] elif data <= LOW[1]: radon_level = LOW[2] elif data <= MODERATE[1]: radon_level = MODERATE[2] else: radon_level = HIGH[2] return radon_level sensor_decoders: dict[ str, Callable[[bytearray], dict[str, float | None | str]], ] = { str(CHAR_UUID_DATETIME): _decode_wave(name="date_time", format_type="H5B", scale=0), str(CHAR_UUID_HUMIDITY): _decode_attr( name="humidity", format_type="H", scale=1.0 / 100.0, max_value=PERCENTAGE_MAX, ), str(CHAR_UUID_RADON_1DAYAVG): _decode_attr( name="radon_1day_avg", format_type="H", scale=1.0 ), str(CHAR_UUID_RADON_LONG_TERM_AVG): _decode_attr( name="radon_longterm_avg", format_type="H", scale=1.0 ), str(CHAR_UUID_ILLUMINANCE_ACCELEROMETER): _decode_wave_illum_accel( name="illuminance_accelerometer", format_type="BB", scale=1.0 ), str(CHAR_UUID_TEMPERATURE): _decode_attr( name="temperature", format_type="h", scale=1.0 / 100.0 ), str(CHAR_UUID_WAVE_2_DATA): _decode_wave_radon( name="Wave2", format_type="<4B8H", scale=1.0 ), str(CHAR_UUID_WAVE_PLUS_DATA): _decode_wave_plus( name="Plus", format_type="<4B8H", scale=0 ), str(CHAR_UUID_WAVEMINI_DATA): _decode_wave_mini( name="WaveMini", format_type="<2B5HLL", scale=1.0 ), } command_decoders: dict[str, CommandDecode] = { str(COMMAND_UUID_WAVE_2): WaveRadonAndPlusCommandDecode(), str(COMMAND_UUID_WAVE_PLUS): WaveRadonAndPlusCommandDecode(), str(COMMAND_UUID_WAVE_MINI): WaveMiniCommandDecode(), str(COMMAND_UUID_ATOM): AtomCommandDecode(), } def short_address(address: str) -> str: """Convert a Bluetooth address to a short address.""" return address.replace("-", "").replace(":", "")[-6:].upper() # pylint: disable=too-many-instance-attributes @dataclasses.dataclass class AirthingsDeviceInfo: """Response data with information about the Airthings device without sensors.""" manufacturer: str = "" hw_version: str = "" sw_version: str = "" model: AirthingsDeviceType = AirthingsDeviceType.UNKNOWN name: str = "" identifier: str = "" address: str = "" did_first_sync: bool = False def friendly_name(self) -> str: """Generate a name for the device.""" return f"Airthings {self.model.product_name}" @dataclasses.dataclass class AirthingsDevice(AirthingsDeviceInfo): """Response data with information about the Airthings device""" firmware = AirthingsFirmwareVersion() sensors: dict[str, str | float | None] = dataclasses.field( default_factory=lambda: {} ) def friendly_name(self) -> str: """Generate a name for the device.""" return f"Airthings {self.model.product_name}" # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-few-public-methods class AirthingsBluetoothDeviceData: """Data for Airthings BLE sensors.""" def __init__( self, logger: Logger, is_metric: bool = True, max_attempts: int = DEFAULT_MAX_UPDATE_ATTEMPTS, ) -> None: """Initialize the Airthings BLE sensor data object.""" self.logger = logger self.is_metric = is_metric self.device_info = AirthingsDeviceInfo() self.max_attempts = max_attempts def set_max_attempts(self, max_attempts: int) -> None: """Set the number of attempts.""" self.max_attempts = max_attempts async def _get_device_characteristics( self, client: BleakClient, device: AirthingsDevice ) -> None: device_info = self.device_info device_info.address = client.address did_first_sync = device_info.did_first_sync device.firmware.update_current_version(device_info.sw_version) # We need to fetch model to determ what to fetch. if not did_first_sync: try: data = await client.read_gatt_char(CHAR_UUID_MODEL_NUMBER_STRING) except BleakError as err: self.logger.debug("Get device characteristics exception: %s", err) return device_info.model = AirthingsDeviceType.from_raw_value(data.decode("utf-8")) if device_info.model == AirthingsDeviceType.UNKNOWN: raise UnsupportedDeviceError( f"Model {data.decode('utf-8')} is not supported" ) characteristics = _CHARS_BY_MODELS.get( device_info.model.raw_value, device_info_characteristics ) self.logger.debug("Fetching device info characteristics: %s", characteristics) for characteristic in characteristics: if did_first_sync and characteristic.name != "firmware_rev": # Only the sw_version can change once set, so we can skip the rest. continue try: data = await client.read_gatt_char(characteristic.uuid) except BleakError as err: self.logger.debug("Get device characteristics exception: %s", err) continue if characteristic.name == "manufacturer": device_info.manufacturer = data.decode(characteristic.format) elif characteristic.name == "hardware_rev": device_info.hw_version = data.decode(characteristic.format) elif characteristic.name == "firmware_rev": device_info.sw_version = data.decode(characteristic.format) elif characteristic.name == "device_name": device_info.name = data.decode(characteristic.format) elif characteristic.name == "serial_nr": identifier = data.decode(characteristic.format) # Some devices return `Serial Number` on Mac instead of # the actual serial number. if identifier != "Serial Number": device_info.identifier = identifier else: self.logger.debug( "Characteristics not handled: %s", characteristic.uuid ) if ( device_info.model == AirthingsDeviceType.WAVE_GEN_1 and device_info.name and not device_info.identifier ): # For the Wave gen. 1 we need to fetch the identifier in the device name. # Example: From `AT#123456-2900Radon` we need to fetch `123456`. wave1_identifier = re.search(r"(?<=\#)[0-9]{1,6}", device_info.name) if wave1_identifier is not None and len(wave1_identifier.group()) == 6: device_info.identifier = wave1_identifier.group() # In some cases the device name will be empty, for example when using a Mac. if not device_info.name: device_info.name = device_info.friendly_name() if device_info.model: device_info.did_first_sync = True # Copy the cached device_info to device for field in dataclasses.fields(device_info): name = field.name setattr(device, name, getattr(device_info, name)) async def _get_service_characteristics( self, client: BleakClient, device: AirthingsDevice ) -> None: svcs = client.services sensors = device.sensors for service in svcs: if ( ( str(COMMAND_UUID_ATOM) in (str(x.uuid) for x in service.characteristics) ) and ( str(COMMAND_UUID_ATOM_NOTIFY) in (str(x.uuid) for x in service.characteristics) ) and device.model in AirthingsDeviceType.atom_devices() ): await self._atom_sensor_data(client, device, sensors, service) else: await self._wave_sensor_data(client, device, sensors, service) async def _wave_sensor_data( self, client: BleakClient, device: AirthingsDevice, sensors: dict[str, str | float | None], service: BleakGATTService, ) -> None: for characteristic in service.characteristics: uuid = characteristic.uuid uuid_str = str(uuid) if uuid in sensors_characteristics_uuid_str and uuid_str in sensor_decoders: try: data = await client.read_gatt_char(characteristic) except BleakError as err: self.logger.debug("Get service characteristics exception: %s", err) continue sensor_data = sensor_decoders[uuid_str](data) # Skipping for now if "date_time" in sensor_data: sensor_data.pop("date_time") sensors.update(sensor_data) # Manage radon values if (d := sensor_data.get("radon_1day_avg")) is not None: sensors["radon_1day_level"] = get_radon_level(float(d)) if not self.is_metric: sensors["radon_1day_avg"] = float(d) * BQ_TO_PCI_MULTIPLIER if (d := sensor_data.get("radon_longterm_avg")) is not None: sensors["radon_longterm_level"] = get_radon_level(float(d)) if not self.is_metric: sensors["radon_longterm_avg"] = float(d) * BQ_TO_PCI_MULTIPLIER if uuid_str in command_decoders: decoder = command_decoders[uuid_str] command_data_receiver = decoder.make_data_receiver() # Set up the notification handlers await client.start_notify(characteristic, command_data_receiver) # send command to this 'indicate' characteristic await client.write_gatt_char(characteristic, bytearray(decoder.cmd)) # Wait for up to one second to see if a callback comes in. try: await command_data_receiver.wait_for_message(5) except asyncio.TimeoutError: self.logger.warning("Timeout getting command data.") command_sensor_data = decoder.decode_data( logger=self.logger, raw_data=command_data_receiver.message ) if command_sensor_data is not None: new_values: dict[str, float | str | None] = {} if (bat_data := command_sensor_data.get("battery")) is not None: new_values["battery"] = device.model.battery_percentage( float(bat_data) ) if illuminance := command_sensor_data.get("illuminance"): new_values["illuminance"] = illuminance sensors.update(new_values) # Stop notification handler await client.stop_notify(characteristic) # pylint: disable=too-many-statements async def _atom_sensor_data( self, client: BleakClient, device: AirthingsDevice, sensors: dict[str, str | float | None], service: BleakGATTService, ) -> None: """Get sensor data from the device.""" device.firmware = device.model.need_firmware_upgrade( self.device_info.sw_version ) if device.firmware.need_firmware_upgrade: self.logger.warning( "The firmware for this device (%s) is not up to date, " "please update to %s or newer using the Airthings app.", self.device_info.address, device.firmware.required_version or "N/A", ) decoder = command_decoders[str(COMMAND_UUID_ATOM)] command_data_receiver = decoder.make_data_receiver() atom_write = service.get_characteristic(COMMAND_UUID_ATOM) atom_notify = service.get_characteristic(COMMAND_UUID_ATOM_NOTIFY) if atom_write is None or atom_notify is None: raise ValueError("Missing characteristics for device") # Set up the notification handlers await client.start_notify( char_specifier=atom_notify, callback=command_data_receiver ) # send command to this 'indicate' characteristic await client.write_gatt_char(atom_write, bytearray(decoder.cmd)) # Wait for up to one second to see if a callback comes in. try: await command_data_receiver.wait_for_message(5) except asyncio.TimeoutError: self.logger.warning("Timeout getting command data.") command_sensor_data = decoder.decode_data( logger=self.logger, raw_data=command_data_receiver.message, ) if command_sensor_data is not None: new_values: dict[str, float | str | None] = {} if (bat_data := command_sensor_data.get("BAT")) is not None: new_values["battery"] = device.model.battery_percentage( float(bat_data) / 1000.0 ) if (lux := command_sensor_data.get("LUX")) is not None: new_values["lux"] = lux if (co2 := command_sensor_data.get("CO2")) is not None: new_values["co2"] = co2 if (voc := command_sensor_data.get("VOC")) is not None: new_values["voc"] = voc if (hum := command_sensor_data.get("HUM")) is not None: new_values["humidity"] = float(hum) / 100.0 if (temperature := command_sensor_data.get("TMP")) is not None: # Temperature reported as kelvin new_values["temperature"] = round( float(temperature) / 100.0 - 273.15, 2 ) if (noise := command_sensor_data.get("NOI")) is not None: new_values["noise"] = noise if (pressure := command_sensor_data.get("PRS")) is not None: new_values["pressure"] = float(pressure) / (64 * 100) if (radon_1day_avg := command_sensor_data.get("R24")) is not None: new_values["radon_1day_avg"] = radon_1day_avg new_values["radon_1day_level"] = get_radon_level(float(radon_1day_avg)) if (radon_week_avg := command_sensor_data.get("R7D")) is not None: new_values["radon_week_avg"] = radon_week_avg new_values["radon_week_level"] = get_radon_level(float(radon_week_avg)) if (radon_month_avg := command_sensor_data.get("R30D")) is not None: new_values["radon_month_avg"] = radon_month_avg new_values["radon_month_level"] = get_radon_level( float(radon_month_avg) ) if (radon_year_avg := command_sensor_data.get("R1Y")) is not None: new_values["radon_year_avg"] = radon_year_avg new_values["radon_year_level"] = get_radon_level(float(radon_year_avg)) self.logger.debug("Sensor values: %s", new_values) sensors.update(new_values) # Stop notification handler await client.stop_notify(atom_notify) def _handle_disconnect( self, disconnect_future: asyncio.Future[bool], client: BleakClient ) -> None: """Handle disconnect from device.""" self.logger.debug("Disconnected from %s", client.address) if not disconnect_future.done(): disconnect_future.set_result(True) async def update_device(self, ble_device: BLEDevice) -> AirthingsDevice: """Connects to the device through BLE and retrieves relevant data""" # Try to abort early if the device name indicates it is not supported. # In some cases we only get the mac address, so we need to connect to # the device to get the name. if name := ble_device.name: if "Renew" in name or "View" in name: raise UnsupportedDeviceError(f"Model {name} is not supported") for attempt in range(self.max_attempts): is_final_attempt = attempt == self.max_attempts - 1 try: return await self._update_device(ble_device) except DisconnectedError: if is_final_attempt: raise self.logger.debug( "Unexpectedly disconnected from %s", ble_device.address ) except BleakError as err: if is_final_attempt: raise self.logger.debug("Bleak error: %s", err) raise RuntimeError("Should not reach this point") async def _update_device(self, ble_device: BLEDevice) -> AirthingsDevice: """Connects to the device through BLE and retrieves relevant data""" device = AirthingsDevice() loop = asyncio.get_running_loop() disconnect_future = loop.create_future() client: BleakClientWithServiceCache = ( await establish_connection( # pylint: disable=line-too-long BleakClientWithServiceCache, ble_device, ble_device.address, disconnected_callback=partial( self._handle_disconnect, disconnect_future ), ) ) try: async with ( interrupt( disconnect_future, DisconnectedError, f"Disconnected from {client.address}", ), asyncio_timeout(UPDATE_TIMEOUT), ): await self._get_device_characteristics(client, device) await self._get_service_characteristics(client, device) except BleakError as err: if "not found" in str(err): # In future bleak this is a named exception # Clear the char cache since a char is likely # missing from the cache await client.clear_cache() raise except UnsupportedDeviceError: await client.disconnect() raise finally: await client.disconnect() return device airthings-ble-1.1.0/airthings_ble/py.typed000066400000000000000000000000001502657212600205460ustar00rootroot00000000000000airthings-ble-1.1.0/poetry.lock000066400000000000000000002544451502657212600164610ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiooui" version = "0.1.9" description = "Async OUI lookups" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "aiooui-0.1.9-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:64d904b43f14dd1d8d9fcf1684d9e2f558bc5e0bd68dc10023c93355c9027907"}, {file = "aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5"}, {file = "aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8"}, ] [[package]] name = "astroid" version = "3.3.10" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, ] [[package]] name = "async-interrupt" version = "1.2.2" description = "Context manager to raise an exception when a future is done" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c"}, {file = "async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7"}, ] [[package]] name = "async-timeout" version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "black" version = "25.1.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleak" version = "0.22.3" description = "Bluetooth Low Energy platform Agnostic Klient" optional = false python-versions = "<3.14,>=3.8" groups = ["main"] files = [ {file = "bleak-0.22.3-py3-none-any.whl", hash = "sha256:1e62a9f5e0c184826e6c906e341d8aca53793e4596eeaf4e0b191e7aca5c461c"}, {file = "bleak-0.22.3.tar.gz", hash = "sha256:3149c3c19657e457727aa53d9d6aeb89658495822cd240afd8aeca4dd09c045c"}, ] [package.dependencies] bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\" and python_version < \"3.12\""} dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""} pyobjc-core = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} pyobjc-framework-CoreBluetooth = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} pyobjc-framework-libdispatch = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} winrt-runtime = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Enumeration" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Foundation" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Foundation.Collections" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Storage.Streams" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} [[package]] name = "bleak-retry-connector" version = "3.10.0" description = "A connector for Bleak Clients that handles transient connection failures" optional = false python-versions = ">=3.10" groups = ["main"] files = [ {file = "bleak_retry_connector-3.10.0-py3-none-any.whl", hash = "sha256:caaf976320ef280f1145b557bf3b13697f71ef2c1070e1dc643709eb2d29fb1f"}, {file = "bleak_retry_connector-3.10.0.tar.gz", hash = "sha256:a95172bd56d2af677fb9e250291cde8c70d8f72381d423f64e48c828dffbc93b"}, ] [package.dependencies] bleak = {version = ">=0.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.14\""} bluetooth-adapters = {version = ">=0.15.2", markers = "python_version >= \"3.10\" and python_version < \"3.14\" and platform_system == \"Linux\""} dbus-fast = {version = ">=1.14.0", markers = "platform_system == \"Linux\""} [[package]] name = "bleak-winrt" version = "1.2.0" description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" groups = ["main"] markers = "platform_system == \"Windows\" and python_version == \"3.11\"" files = [ {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, ] [[package]] name = "bluetooth-adapters" version = "0.21.4" description = "Tools to enumerate and find Bluetooth Adapters" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "bluetooth_adapters-0.21.4-py3-none-any.whl", hash = "sha256:ce2e8139cc9d7b103c21654c6309507979e469aae3efebcaeee9923080b0569b"}, {file = "bluetooth_adapters-0.21.4.tar.gz", hash = "sha256:a5a809ef7ba95ee673a78704f90ce34612deb3696269d1a6fd61f98642b99dd3"}, ] [package.dependencies] aiooui = ">=0.1.1" bleak = ">=0.21.1" dbus-fast = {version = ">=1.21.0", markers = "platform_system == \"Linux\""} uart-devices = ">=0.1.0" usb-devices = ">=0.4.5" [package.extras] docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "cbor2" version = "5.6.5" description = "CBOR (de)serializer with extensive tag support" optional = false python-versions = ">=3.8" groups = ["main"] files = [ {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, {file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"}, {file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"}, {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"}, {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"}, {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"}, {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"}, {file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"}, {file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"}, {file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"}, {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"}, {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"}, {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"}, {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"}, {file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"}, {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, ] [package.extras] benchmarks = ["pytest-benchmark (==4.0.0)"] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions ; python_version < \"3.12\""] test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] name = "click" version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.9.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca"}, {file = "coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509"}, {file = "coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b"}, {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3"}, {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3"}, {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5"}, {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187"}, {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce"}, {file = "coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70"}, {file = "coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe"}, {file = "coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582"}, {file = "coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86"}, {file = "coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed"}, {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d"}, {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338"}, {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875"}, {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250"}, {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c"}, {file = "coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32"}, {file = "coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125"}, {file = "coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e"}, {file = "coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626"}, {file = "coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb"}, {file = "coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300"}, {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8"}, {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5"}, {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd"}, {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898"}, {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d"}, {file = "coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74"}, {file = "coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e"}, {file = "coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342"}, {file = "coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631"}, {file = "coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f"}, {file = "coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd"}, {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86"}, {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43"}, {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1"}, {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751"}, {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67"}, {file = "coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643"}, {file = "coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a"}, {file = "coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d"}, {file = "coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0"}, {file = "coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d"}, {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f"}, {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029"}, {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece"}, {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683"}, {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f"}, {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10"}, {file = "coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363"}, {file = "coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7"}, {file = "coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c"}, {file = "coverage-7.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951"}, {file = "coverage-7.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58"}, {file = "coverage-7.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71"}, {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55"}, {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b"}, {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7"}, {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385"}, {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed"}, {file = "coverage-7.9.1-cp39-cp39-win32.whl", hash = "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d"}, {file = "coverage-7.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244"}, {file = "coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514"}, {file = "coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c"}, {file = "coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec"}, ] [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dbus-fast" version = "2.44.1" description = "A faster version of dbus-next" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "dbus_fast-2.44.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c78a004ba43aeaf203a19169d2b4be238375905645999da30cb0da730df80cf2"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a634286651398f3f1326e8200fc54289d52c2c00249d29cacfc691660a5da1"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0c4a128f8b29941307fc5722f37a1bb87ddcf733188d917ab374d9da0c6e1ce7"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adaf459fbce22a63d3578f3ec782c6978edf975eb06d71fb5b7a690496cf6bbe"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de871cf722c436bdcceb96b2a3af7084e1fa468f7916ae278ec8ec49a6fa7eef"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b40863de172031bcc02f54c6f05cccb0b882dc2e1b09e11314a8ccf38c558760"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b7ae16555df6b56d3befcc51e036779ef47c0e954fdb9fb0821ac25212aefe9"}, {file = "dbus_fast-2.44.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a220a28e88062a2548f0c6da9eb15fb7e3af70eae56729fc3795ce3e3fba057d"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ec5db912bd4cfeadf7134163d6dde684271cd44cf26e3b4720107f3de406623"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6ad99f626837753b39a39e09facd2091ee4851ee1eb6ebec5fa9a9a231734254"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7aa157f689a114bfb5367c55884d35e25d57cf25202a6590ce05010f929e7df"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f961d8bcad80359f24c0156b3094f58a87d583d56139ee50922fe5894b6797cf"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1f38fb5c31846c3ada8fc2b693d8d19953d376a9ea21079e3686e93faa1f8a0f"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35e3cde53cc9180ce95c6c84a1e8d1ded429031e4a0a182606e8d22cf57d3294"}, {file = "dbus_fast-2.44.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f30fb09f1ea13658fb4316511e27d6b94f8363b16f2d093efe73e6e289b740"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dd0f8d41f6ab9d4a782c116470bc319d690f9b50c97b6debc6d1fef08e4615a"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9d6e386658343db380b9e4e81b3bf4e3c17135dbb5889173b1f2582b675b9a8c"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bd27563c11219b6fde7a5458141d860d8445c2defb036bab360d1f9bf1dfae0"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0272784aceac821dd63c8187a8860179061a850269617ff5c5bd25ca37bf9307"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eed613a909a45f0e0a415c88b373024f007a9be56b1316812ed616d69a3b9161"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d4288f2cba4f8309dcfd9f4392e0f4f2b5be6c796dfdb0c5e03228b1ab649b1"}, {file = "dbus_fast-2.44.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50a9a4c6921f4b7446717fb4869750f54b561ce486b25b36550cb2a910c988d9"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dc5db158bf9838979f732acc39e0e1ecd7e3295a09fa8adb93b09c097615a4"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f11878c0c089d278861e48c02db8002496c2233b0f605b5630ef61f0b7fb0ea3"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd81f483b3ffb71e88478cfabccc1fab8d7154fccb1c661bfafcff9b0cfd996"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ad499de96a991287232749c98a59f2436ed260f6fd9ad4cb3b04a4b1bbbef148"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36c44286b11e83977cd29f9551b66b446bb6890dff04585852d975aa3a038ca2"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:89f2f6eccbb0e464b90e5a8741deb9d6a91873eeb41a8c7b963962b39eb1e0cd"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb74a227b071e1a7c517bf3a3e4a5a0a2660620084162e74f15010075534c9d5"}, {file = "dbus_fast-2.44.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e3719399e687359b0ef66af1b720661dd4f12059db1c4f506e678569a2256b4"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:806450623ef3f8df846524da7e448edc8174261a01cfd5dfda92e3df89c0de10"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:55ad499b7ef08cb76fce9c9fdcdd6589d2ebfc7e53b3d261d8f40c6d97a8d901"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55d717865219ec2ae9977b6d067c05261cdc3ef6205c687c8bb92b3437886e58"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39d4cc61e491e11912f76d70cc1c47387ab4f2e5b71f34bfa13eb11aa6026268"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9b3b10151f1140f7b6dd47a89fc37edd05d6213be0a1748eadba82fc144c05c2"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33772c223f5cef1bacc298e83dc04b27b3a47065b245fde766fcc126e761dca7"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e3f42f982af45bcfa0ff23e808f3aa54a45fe4bf43aadd3beb5ace816fba76"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f29a81d86c9ce3020a5df8c1e5557edaa00e1e00c9804ec874d46c99d967a686"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5dec134715457601c0fa8df3040a56d319de1a152464ae4d4bfc53bbb5c02e04"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893509b516f2f24b4e3f09a6b1f3a30f856cf237cd773cdc505ea7ab4fa3c863"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:db81275d708774f6a17c89f2e063398c0deb358c4d22b663a3dd99861f6683a4"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:161a3e6fc8783c30c9feb072e09604d96ec0c465b06bd35b6acc1a0316bd2a27"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:67febe6454e714d85a532bd84969001ed948bbaf1699a7e1e4c6abb5508c9522"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890f0fc046d5db66524ddedeca8c14b65739fbbf32d6488175c07428362bf250"}, {file = "dbus_fast-2.44.1.tar.gz", hash = "sha256:b027e96c39ed5622bb54d811dcdbbe9d9d6edec3454808a85a1ceb1867d9e25c"}, ] [[package]] name = "dill" version = "0.4.0" description = "serialize all of Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "isort" version = "6.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, ] [package.extras] colors = ["colorama"] plugins = ["setuptools"] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] name = "mypy" version = "1.16.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pygments" version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" version = "3.3.7" description = "python code static checker" optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, ] [package.dependencies] astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomlkit = ">=0.10.1" [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] name = "pyobjc-core" version = "10.3.2" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_core-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb40672d682851a5c7fd84e5041c4d069b62076168d72591abb5fcc871bb039"}, {file = "pyobjc_core-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea5e77659619ad93c782ca07644b6efe7d7ec6f59e46128843a0a87c1af511a"}, {file = "pyobjc_core-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:16644a92fb9661de841ba6115e5354db06a1d193a5e239046e840013c7b3874d"}, {file = "pyobjc_core-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:76b8b911d94501dac89821df349b1860bb770dce102a1a293f524b5b09dd9462"}, {file = "pyobjc_core-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8c6288fdb210b64115760a4504efbc4daffdc390d309e9318eb0e3e3b78d2828"}, {file = "pyobjc_core-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87901e9f7032f33eb4fa884e407bf2744d5a0791b379bfca783982a02be3f7fb"}, {file = "pyobjc_core-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:636971ab48a4198ca129e149fe58ccf85a7b4a9b93d27f5ae920d88eb2655431"}, {file = "pyobjc_core-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:48e9ac3af42b2340dae709a8b894f5ef7e5132d8546adcd1797cffcc449dabdc"}, {file = "pyobjc_core-10.3.2.tar.gz", hash = "sha256:dbf1475d864ce594288ce03e94e3a98dc7f0e4639971eb1e312bdf6661c21e0e"}, ] [[package]] name = "pyobjc-framework-cocoa" version = "10.3.2" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_Cocoa-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:61f44c2adab28fdf3aa3d593c9497a2d9ceb9583ed9814adb48828c385d83ff4"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7caaf8b260e81b27b7b787332846f644b9423bfc1536f6ec24edbde59ab77a87"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c49e99fc4b9e613fb308651b99d52a8a9ae9916c8ef27aa2f5d585b6678a59bf"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1161b5713f9b9934c12649d73a6749617172e240f9431eff9e22175262fdfda"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:08e48b9ee4eb393447b2b781d16663b954bd10a26927df74f92e924c05568d89"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7faa448d2038ae0e0287a326d390002e744bb6470e45995e2dbd16c892e4495a"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:fcd53fee2be9708576617994b107aedc2c40824b648cd51e780e8399c0a447b6"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:838fcf0d10674bde9ff64a3f20c0e188f2dc5e804476d80509b81c4ac1dabc59"}, {file = "pyobjc_framework_cocoa-10.3.2.tar.gz", hash = "sha256:673968e5435845bef969bfe374f31a1a6dc660c98608d2b84d5cae6eafa5c39d"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" [[package]] name = "pyobjc-framework-corebluetooth" version = "10.3.2" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:af3e2f935a6a7e5b009b4cf63c64899592a7b46c3ddcbc8f2e28848842ef65f4"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:973b78f47c7e2209a475e60bcc7d1b4a87be6645d39b4e8290ee82640e1cc364"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:4bafdf1be15eae48a4878dbbf1bf19877ce28cbbba5baa0267a9564719ee736e"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4d7dc7494de66c850bda7b173579df7481dc97046fa229d480fe9bf90b2b9651"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:62e09e730f4d98384f1b6d44718812195602b3c82d5c78e09f60e8a934e7b266"}, {file = "pyobjc_framework_corebluetooth-10.3.2.tar.gz", hash = "sha256:c0a077bc3a2466271efa382c1e024630bc43cc6f9ab8f3f97431ad08b1ad52bb"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" pyobjc-framework-Cocoa = ">=10.3.2" [[package]] name = "pyobjc-framework-libdispatch" version = "10.3.2" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_libdispatch-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35233a8b1135567c7696087f924e398799467c7f129200b559e8e4fa777af860"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:061f6aa0f88d11d993e6546ec734303cb8979f40ae0f5cd23541236a6b426abd"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6bb528f34538f35e1b79d839dbfc398dd426990e190d9301fe2d811fddc3da62"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1357729d5fded08fbf746834ebeef27bee07d6acb991f3b8366e8f4319d882c4"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:210398f9e1815ceeff49b578bf51c2d6a4a30d4c33f573da322f3d7da1add121"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e7ae5988ac0b369ad40ce5497af71864fac45c289fa52671009b427f03d6871f"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:f9d51d52dff453a4b19c096171a6cd31dd5e665371c00c1d72d480e1c22cd3d4"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ef755bcabff2ea8db45603a8294818e0eeae85bf0b7b9d59e42f5947a26e33b9"}, {file = "pyobjc_framework_libdispatch-10.3.2.tar.gz", hash = "sha256:e9f4311fbf8df602852557a98d2a64f37a9d363acf4d75634120251bbc7b7304"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" pyobjc-framework-Cocoa = ">=10.3.2" [[package]] name = "pytest" version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} iniconfig = ">=1" packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "1.0.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} pluggy = ">=1.2" pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-rerunfailures" version = "15.1" description = "pytest plugin to re-run tests to eliminate flaky failures" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_rerunfailures-15.1-py3-none-any.whl", hash = "sha256:f674c3594845aba8b23c78e99b1ff8068556cc6a8b277f728071fdc4f4b0b355"}, {file = "pytest_rerunfailures-15.1.tar.gz", hash = "sha256:c6040368abd7b8138c5b67288be17d6e5611b7368755ce0465dda0362c8ece80"}, ] [package.dependencies] packaging = ">=17.1" pytest = ">=7.4,<8.2.2 || >8.2.2" [[package]] name = "ruff" version = "0.12.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848"}, {file = "ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6"}, {file = "ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2"}, {file = "ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4"}, {file = "ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514"}, {file = "ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88"}, {file = "ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51"}, {file = "ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a"}, {file = "ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb"}, {file = "ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0"}, {file = "ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b"}, {file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"}, ] [[package]] name = "tomlkit" version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "typing-extensions" version = "4.14.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] markers = {main = "python_version == \"3.11\""} [[package]] name = "uart-devices" version = "0.1.1" description = "UART Devices for Linux" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123"}, {file = "uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34"}, ] [[package]] name = "usb-devices" version = "0.4.5" description = "Tools for mapping, describing, and resetting USB devices" optional = false python-versions = ">=3.9,<4.0" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf"}, {file = "usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d"}, ] [[package]] name = "winrt-runtime" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_runtime-2.3.0-cp310-cp310-win32.whl", hash = "sha256:5c22ed339b420a6026134e28281b25078a9e6755eceb494dce5d42ee5814e3fd"}, {file = "winrt_runtime-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3ef0d6b281a8d4155ea14a0f917faf82a004d4996d07beb2b3d2af191503fb1"}, {file = "winrt_runtime-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:93ce23df52396ed89dfe659ee0e1a968928e526b9c577942d4a54ad55b333644"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win32.whl", hash = "sha256:352d70864846fd7ec89703845b82a35cef73f42d178a02a4635a38df5a61c0f8"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:286e6036af4903dd830398103c3edd110a46432347e8a52ba416d937c0e1f5f9"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:44d0f0f48f2f10c02b885989e8bbac41d7bf9c03550b20ddf562100356fca7a9"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win32.whl", hash = "sha256:03d3e4aedc65832e57c0dbf210ec2a9d7fb2819c74d420ba889b323e9fa5cf28"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0dc636aec2f4ee6c3849fa59dae10c128f4a908f0ce452e91af65d812ea66dcb"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d9f140c71e4f3bf7bf7d6853b246eab2e1632c72f218ff163aa41a74b576736f"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win32.whl", hash = "sha256:77f06df6b7a6cb536913ae455e30c1733d31d88dafe2c3cd8c3d0e2bcf7e2a20"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:7388774b74ea2f4510ab3a98c95af296665ebe69d9d7e2fd7ee2c3fc5856099e"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:0d3a4ac7661cad492d51653054e63328b940a6083c1ee1dd977f90069cb8afaa"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win32.whl", hash = "sha256:cd7bce2c7703054e7f64d11be665e9728e15d9dae0d952a51228fe830e0c4b55"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2da01af378ab9374a3a933da97543f471a676a3b844318316869bffeff811e8a"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:1c6bbfcc7cbe1c8159ed5d776b30b7f1cbc2c6990803292823b0788c22d75636"}, {file = "winrt_runtime-2.3.0.tar.gz", hash = "sha256:bb895a2b8c74b375781302215e2661914369c625aa1f8df84f8d37691b22db77"}, ] [[package]] name = "winrt-windows-devices-bluetooth" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win32.whl", hash = "sha256:554aa6d0ca4bebc22a45f19fa60db1183a2b5643468f3c95cf0ebc33fbc1b0d0"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:cec2682e10431f027c1823647772671fb09bebc1e8a00021a3651120b846d36f"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b4d42faef99845de2aded4c75c906f03cc3ba3df51fb4435e4cc88a19168cf99"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win32.whl", hash = "sha256:64e0992175d4d5a1160179a8c586c2202a0edbd47a5b6da4efdbc8bb601f2f99"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:0830111c077508b599062fbe2d817203e4efa3605bd209cf4a3e03388ec39dda"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:3943d538cb7b6bde3fd8741591eb6e23487ee9ee6284f05428b205e7d10b6d92"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win32.whl", hash = "sha256:544ed169039e6d5e250323cc18c87967cfeb4d3d09ce354fd7c5fd2283f3bb98"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:f7becf095bf9bc999629fcb6401a88b879c3531b3c55c820e63259c955ddc06c"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:a6a2980409c855b4e5dab0be9bde9f30236292ac1fc994df959fa5a518cd6690"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win32.whl", hash = "sha256:82f443be43379d4762e72633047c82843c873b6f26428a18855ca7b53e1958d7"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:8b407da87ab52315c2d562a75d824dcafcae6e1628031cdb971072a47eb78ff0"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e36d0b487bc5b64662b8470085edf8bfa5a220d7afc4f2e8d7faa3e3ac2bae80"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win32.whl", hash = "sha256:6553023433edf5a75767e8962bf492d0623036975c7d8373d5bbccc633a77bbc"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:77bdeadb043190c40ebbad462cd06e38b6461bc976bc67daf587e9395c387aae"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c588ab79b534fedecce48f7082b419315e8d797d0120556166492e603e90d932"}, {file = "winrt_windows_devices_bluetooth-2.3.0.tar.gz", hash = "sha256:a1204b71c369a0399ec15d9a7b7c67990dd74504e486b839bf81825bd381a837"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.3.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.3.0)", "winrt-Windows.Devices.Enumeration[all] (==2.3.0)", "winrt-Windows.Devices.Radios[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Networking[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win32.whl", hash = "sha256:4386498e7794ed383542ea868f0aa2dd8fb5f09f12bdffde024d12bd9f5a3756"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6fa25b2541d2898ae17982e86e0977a639b04f75119612cb46e1719474513fd"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b200ff5acd181353f61f5b6446176faf78a61867d8c1d21e77a15e239d2cdf6b"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win32.whl", hash = "sha256:e56ad277813b48e35a3074f286c55a7a25884676e23ef9c3fc12349a42cb8fa4"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d6533fef6a5914dc8d519b83b1841becf6fd2f37163d6e07df318a6a6118f194"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:8f4369cb0108f8ee0cace559f9870b00a4dde3fc1abd52f84adba08bc733825c"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win32.whl", hash = "sha256:d729d989acd7c1d703e2088299b6e219089a415db4a7b80cd52fdc507ec3ce95"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d3d258d4388a2b46f2e46f2fbdede1bf327eaa9c2dd4605f8a7fe454077c49e"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8c12457b00a79f8f1058d7a51bd8e7f177fb66e31389469e75b1104f6358921"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win32.whl", hash = "sha256:ac1e55a350881f82cb51e162cb7a4b5d9359e9e5fbde922de802404a951d64ec"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0fc339340fb8be21c1c829816a49dc31b986c6d602d113d4a49ee8ffaf0e2396"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:da63d9c56edcb3b2d5135e65cc8c9c4658344dd480a8a2daf45beb2106f17874"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win32.whl", hash = "sha256:e98c6ae4b0afd3e4f3ab4fa06e84d6017ff9242146a64e3bad73f7f34183a076"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc485f4143fbbb3ae0c9c9ad03b1021a5cb233c6df65bf56ac14f8e22c918c3"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:7af519cc895be84d6974e9f70d102545a5e8db05e065903b0fd84521218e60a9"}, {file = "winrt_windows_devices_bluetooth_advertisement-2.3.0.tar.gz", hash = "sha256:c8adbec690b765ca70337c35efec9910b0937a40a0a242184ea295367137f81c"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win32.whl", hash = "sha256:1ec75b107370827874d8435a47852d0459cb66d5694e02a833e0a75c4748e847"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a178aa936abbc56ae1cc54a222dee4a34ce6c09506a5b592d4f7d04dbe76b95"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7067b8578e19ad17b28694090d5b000fee57db5b219462155961b685d71fba5"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win32.whl", hash = "sha256:e0aeba201e20b6c4bc18a4336b5b07d653d4ab4c9c17a301613db680a346cd5e"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f87b3995de18b98075ec2b02afc7252873fa75e7c840eb770d7bfafb4fda5c12"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:7dccce04ec076666001efca8e2484d0ec444b2302ae150ef184aa253b8cfba09"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win32.whl", hash = "sha256:1b97ef2ab9c9f5bae984989a47565d0d19c84969d74982a2664a4a3485cb8274"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:5fac2c7b301fa70e105785d7504176c76e4d824fc3823afed4d1ab6a7682272c"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:353fdccf2398b2a12e0835834cff8143a7efd9ba877fb5820fdcce531732b500"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win32.whl", hash = "sha256:f414f793767ccc56d055b1c74830efb51fa4cbdc9163847b1a38b1ee35778f49"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ef35d9cda5bbdcc55aa7eaf143ab873227d6ee467aaf28edbd2428f229e7c94"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a9e7308ba264175c2a9ee31f6cf1d647cb35ee9a1da7350793d8fe033a6b9b8"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win32.whl", hash = "sha256:aea58f7e484cf3480ab9472a3e99b61c157b8a47baae8694bc7400ea5335f5dc"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:992b792a9e7f5771ccdc18eec4e526a11f23b75d9be5de3ec552ff719333897a"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:66b030a9cc6099dafe4253239e8e625cc063bb9bb115bebed6260d92dd86f6b1"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.3.0.tar.gz", hash = "sha256:f40f94bf2f7243848dc10e39cfde76c9044727a05e7e5dfb8cb7f062f3fd3dda"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.3.0)", "winrt-Windows.Devices.Enumeration[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-enumeration" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win32.whl", hash = "sha256:461360ab47967f39721e71276fdcfe87ad2f71ba7b09d721f2f88bcdf16a6924"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a7d7b01d43d5dcc1f3846db12f4c552155efae75469f36052623faed7f0f74a8"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6478fbe6f45172a9911c15b061ec9b0f30c9f4845ba3fd1e9e1bb78c1fb691c4"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win32.whl", hash = "sha256:30be5cba8e9e81ea8dd514ba1300b5bb14ad7cc4e32efe908ddddd14c73e7f61"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86c2a1865e0a0146dd4f51f17e3d773d3e6732742f61838c05061f28738c6dbd"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:1b50d9304e49a9f04bc8139831b75be968ff19a1f50529d5eb0081dae2103d92"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win32.whl", hash = "sha256:42ed0349f0290a1b0a101425a06196c5d5db1240db6f8bd7d2204f23c48d727b"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:83e385fbf85b9511699d33c659673611f42b98bd3a554a85b377a34cc3b68b2e"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:26f855caee61c12449c6b07e22ea1ad470f8daa24223d8581e1fe622c70b48a8"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win32.whl", hash = "sha256:a5f2cff6ee584e5627a2246bdbcd1b3a3fd1e7ae0741f62c59f7d5a5650d5791"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:7516171521aa383ccdc8f422cc202979a2359d0d1256f22852bfb0b55d9154f0"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:80d01dfffe4b548439242f3f7a737189354768b203cca023dc29b267dfe5595a"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win32.whl", hash = "sha256:990a375cd8edc2d30b939a49dcc1349ede3a4b8e4da78baf0de5e5711d3a4f00"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7bedf0eac2066d7d37b1d34071b95bb57024e9e083867be1d24e916e012ac0"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c53b673b80ba794f1c1320a5e0a14d795193c3f64b8132ebafba2f49c7301c2f"}, {file = "winrt_windows_devices_enumeration-2.3.0.tar.gz", hash = "sha256:a14078aac41432781acb0c950fcdcdeb096e2f80f7591a3d46435f30221fc3eb"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.ApplicationModel.Background[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Security.Credentials[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)", "winrt-Windows.UI.Popups[all] (==2.3.0)", "winrt-Windows.UI[all] (==2.3.0)"] [[package]] name = "winrt-windows-foundation" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win32.whl", hash = "sha256:ea7b0e82be5c05690fedaf0dac5aa5e5fefd7ebf90b1497e5993197d305d916d"}, {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:6807dd40f8ecd6403679f6eae0db81674fdcf33768d08fdee66e0a17b7a02515"}, {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:0a861815e97ace82583210c03cf800507b0c3a97edd914bfffa5f88de1fbafcc"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win32.whl", hash = "sha256:c79b3d9384128b6b28c2483b4600f15c5d32c1f6646f9d77fdb3ee9bbaef6f81"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fdd9c4914070dc598f5961d9c7571dd7d745f5cc60347603bf39d6ee921bd85c"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:62bbb0ffa273551d33fd533d6e09b6f9f633dc214225d483722af47d2525fb84"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win32.whl", hash = "sha256:d36f472ac258e79eee6061e1bb4ce50bfd200f9271392d23479c800ca6aee8d1"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8de9b5e95a3fdabdb45b1952e05355dd5a678f80bf09a54d9f966dccc805b383"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:37da09c08c9c772baedb1958e5ee116fe63809f33c6820c69750f340b3dda292"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win32.whl", hash = "sha256:2b00fad3f2a3859ccae41eee12ab44434813a371c2f3003b4f2419e5eecb4832"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:686619932b2a2c689cbebc7f5196437a45fd2056656ef130bb10240bb111086a"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b38dcb83fe82a7da9a57d7d5ad5deb09503b5be6d9357a9fd3016ca31673805d"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win32.whl", hash = "sha256:2d6922de4dc38061b86d314c7319d7c6bd78a52d64ee0c93eb81474bddb499bc"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1513e43adff3779d2f611d8bdf9350ac1a7c04389e9e6b1d777c5cd54f46e4fc"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c811e4a4f79b947fbbb50f74d34ef6840dd2dd26e0199bd61a4185e48c6a84a8"}, {file = "winrt_windows_foundation-2.3.0.tar.gz", hash = "sha256:c5766f011c8debbe89b460af4a97d026ca252144e62d7278c9c79c5581ea0c02"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.3.0)"] [[package]] name = "winrt-windows-foundation-collections" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win32.whl", hash = "sha256:d2fca59eef9582a33c2797b1fda1d5757d66827cc34e6fc1d1c94a5875c4c043"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d14b47d9137aebad71aa4fde5892673f2fa326f5f4799378cb9f6158b07a9824"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:cca5398a4522dffd76decf64a28368cda67e81dc01cad35a9f39cc351af69bdd"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win32.whl", hash = "sha256:3808af64c95a9b464e8e97f6bec57a8b22168185f1c893f30de69aaf48c85b17"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e9a3842a39feb965545124abfe79ed726adc5a1fc6a192470a3c5d3ec3f7a74"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:751c2a68fef080dfe0af892ef4cebf317844e4baa786e979028757fe2740fba4"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win32.whl", hash = "sha256:498c1fc403d3dc7a091aaac92af471615de4f9550d544347cb3b169c197183b5"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:4d1b1cacc159f38d8e6b662f6e7a5c41879a36aa7434c1580d7f948c9037419e"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:398d93b76a2cf70d5e75c1f802e1dd856501e63bc9a31f4510ac59f718951b9e"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win32.whl", hash = "sha256:1e5f1637e0919c7bb5b11ba1eebbd43bc0ad9600cf887b59fcece0f8a6c0eac3"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:c809a70bc0f93d53c7289a0a86d8869740e09fff0c57318a14401f5c17e0b912"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:269942fe86af06293a2676c8b2dcd5cb1d8ddfe1b5244f11c16e48ae0a5d100f"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win32.whl", hash = "sha256:936b1c5720b564ec699673198addee97f3bdb790622d24c8fd1b346a9767717c"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:905a6ac9cd6b51659a9bba08cf44cfc925f528ef34cdd9c3a6c2632e97804a96"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:1d6eac85976bd831e1b8cc479d7f14afa51c27cec5a38e2540077d3400cbd3ef"}, {file = "winrt_windows_foundation_collections-2.3.0.tar.gz", hash = "sha256:15c997fd6b64ef0400a619319ea3c6851c9c24e31d51b6448ba9bac3616d25a0"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation[all] (==2.3.0)"] [[package]] name = "winrt-windows-storage-streams" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win32.whl", hash = "sha256:2c0901aee1232e92ed9320644b853d7801a0bdb87790164d56e961cd39910f07"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba07dc25decffd29aa8603119629c167bd03fa274099e3bad331a4920c292b78"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:5b60b48460095c50a00a6f7f9b3b780f5bdcb1ec663fc09458201499f93e23ea"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win32.whl", hash = "sha256:8388f37759df64ceef1423ae7dd9275c8a6eb3b8245d400173b4916adc94b5ad"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:e5783dbe3694cc3deda594256ebb1088655386959bb834a6bfb7cd763ee87631"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:0a487d19c73b82aafa3d5ef889bb35e6e8e2487ca4f16f5446f2445033d5219c"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win32.whl", hash = "sha256:272e87e6c74cb2832261ab33db7966a99e7a2400240cc4f8bf526a80ca054c68"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:997bf1a2d52c5f104b172947e571f27d9916a4409b4da592ec3e7f907848dd1a"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d56daa00205c24ede6669d41eb70d6017e0202371d99f8ee2b0b31350ab59bd5"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win32.whl", hash = "sha256:7ac4e46fc5e21d8badc5d41779273c3f5e7196f1cf2df1959b6b70eca1d5d85f"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1460027c94c107fcee484997494f3a400f08ee40396f010facb0e72b3b74c457"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e4553a70f5264a7733596802a2991e2414cdcd5e396b9d11ee87be9abae9329e"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win32.whl", hash = "sha256:28e1117e23046e499831af16d11f5e61e6066ed6247ef58b93738702522c29b0"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:5511dc578f92eb303aee4d3345ee4ffc88aa414564e43e0e3d84ff29427068f0"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6f5b3f8af4df08f5bf9329373949236ffaef22d021070278795e56da5326a876"}, {file = "winrt_windows_storage_streams-2.3.0.tar.gz", hash = "sha256:d2c010beeb1dd7c135ed67ecfaea13440474a7c469e2e9aa2852db27d2063d44"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage[all] (==2.3.0)", "winrt-Windows.System[all] (==2.3.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <3.14" content-hash = "3698dfd1cbcfea8fc721e2b14ccec1b450af188740b347847571a61a7e633cdd" airthings-ble-1.1.0/pyproject.toml000066400000000000000000000046541502657212600171740ustar00rootroot00000000000000[project] name = "airthings-ble" version = "1.1.0" description = "Manage Airthings BLE devices" authors = [ { "name" = "Vincent Giorgi" }, { "name" = "Ståle Storø Hauknes" }, ] readme = "README.md" classifiers = [ "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: MIT License", ] requires-python = ">=3.11, <3.14" dependencies = [ "async-interrupt>=1.2.2", "async-timeout>=4.0.1", "bleak>=0.22.0", "bleak-retry-connector>=3.9.0", "cbor2>=5.6.5", ] [project.urls] "Homepage" = "https://www,airthings.com" "Source Code" = "https://github.com/Airthings/airthings-ble" "Bug Reports" = "https://github.com/Airthings/airthings-ble/issues" [tool.poetry] version = "1.0.0" [tool.poetry.dependencies] python = ">=3.11, <3.14" bleak = ">=0.22.0" bleak-retry-connector = ">=3.9.0" async-interrupt = ">=1.2.2" async-timeout = ">=4.0.1" cbor2 = ">=5.6.5" [tool.poetry.group.dev.dependencies] pytest = "^8.4.0" pytest-cov = "^6.2.0" black = "^25.1.0" mypy = "^1.16.0" pylint = "^3.3.7" pytest-asyncio = "^1.0.0" pytest-rerunfailures = "^15.1.0" ruff = ">=0.12.0" [tool.semantic_release] branch = "main" version_toml = "pyproject.toml:tool.poetry.version" version_variable = "airthings_ble/__init__.py:__version__" build_command = "pip install poetry && poetry build" [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=airthings_ble --cov-report=term-missing:skip-covered" pythonpath = ["airthings_ble"] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@overload", "if TYPE_CHECKING", "raise NotImplementedError", ] [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true mypy_path = "src/" no_implicit_optional = true show_error_codes = true warn_unreachable = true warn_unused_ignores = true exclude = [ 'tests/.*', 'setup.py', ] [tool.ruff] line-length = 88 target-version = "py311" extend-exclude = ["docs", "build"] src = ["src", "tests"] [tool.ruff.lint] select = ["E", "F", "I"] # pycodestyle, pyflakes, isort ignore = ["E203", "E501"] # For black-compatibilitet [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101"] # Tillat bruk av assert i tester [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" airthings-ble-1.1.0/setup.py000066400000000000000000000003631502657212600157630ustar00rootroot00000000000000#!/usr/bin/env python # This is a shim to allow GitHub to detect the package, build is done with poetry # Taken from https://github.com/Textualize/rich import setuptools if __name__ == "__main__": setuptools.setup(name="airthings-ble") airthings-ble-1.1.0/tests/000077500000000000000000000000001502657212600154115ustar00rootroot00000000000000airthings-ble-1.1.0/tests/test_airthings_firmware.py000066400000000000000000000040171502657212600227100ustar00rootroot00000000000000from airthings_ble.airthings_firmware import AirthingsFirmwareVersion from pytest import mark def test_airthings_firmware_need_upgrade() -> None: fw = AirthingsFirmwareVersion( current_version="T-SUB-2.6.0-master+0", required_version="T-SUB-2.6.1-master+0", ) assert fw.need_firmware_upgrade is True def test_airthings_firmware_need_upgrade_semantic() -> None: fw = AirthingsFirmwareVersion( current_version="1.3.4", required_version="1.3.5", ) assert fw.need_firmware_upgrade is True assert fw.current_version == (1, 3, 4) assert fw.required_version == (1, 3, 5) def test_airthings_firmware_no_upgrade() -> None: fw = AirthingsFirmwareVersion( current_version="R-SUB-1.3.5-master+0", required_version="R-SUB-1.3.5-master+0", ) assert fw.need_firmware_upgrade is False assert fw.current_version == (1, 3, 5) assert fw.required_version == (1, 3, 5) def test_airthings_firmware_no_required_firmware() -> None: fw = AirthingsFirmwareVersion( current_version="T-SUB-1.0.1-master+0", required_version=None, ) assert fw.need_firmware_upgrade is False assert fw.current_version == (1, 0, 1) assert fw.required_version is None @mark.parametrize( "invalid_version", [ "a.b.c", "1.2", "1.a.3", "invalid", None, ], ) def test_airthings_firmware_invalid_current_version(invalid_version: str) -> None: fw = AirthingsFirmwareVersion( current_version=invalid_version, required_version="T-SUB-1.0.1-master+0", ) assert fw.current_version is None assert fw.required_version == (1, 0, 1) assert fw.need_firmware_upgrade is False def test_airthings_firmware_invalid_required_version() -> None: fw = AirthingsFirmwareVersion( current_version="G-SUB-1.0.1-master+0", required_version="invalid", ) assert fw.need_firmware_upgrade is False assert fw.current_version == (1, 0, 1) assert fw.required_version is None airthings-ble-1.1.0/tests/test_atom_request.py000066400000000000000000000016251502657212600215360ustar00rootroot00000000000000from airthings_ble.atom.request import AtomRequest from airthings_ble.atom.request_path import AtomRequestPath from pytest import mark @mark.parametrize( "random_bytes", [ None, bytes.fromhex("E473"), ], ) def test_atom_request(random_bytes: bytes | None) -> None: """Test the Wave Enhance request.""" request = AtomRequest(url=AtomRequestPath.LATEST_VALUES, random_bytes=random_bytes) assert request.url == AtomRequestPath.LATEST_VALUES assert len(request.random_bytes) == 2 if random_bytes is not None: assert request.random_bytes == random_bytes else: random_bytes = request.random_bytes request_bytes = request.as_bytes() assert request_bytes[0:2] == bytes.fromhex("0301") assert request_bytes[2:4] == random_bytes assert request_bytes[4:8] == bytes.fromhex("81A1006D") assert request_bytes[8:] == request.url.as_bytes() airthings-ble-1.1.0/tests/test_atom_response.py000066400000000000000000000037731502657212600217120ustar00rootroot00000000000000import logging from airthings_ble.atom.request_path import AtomRequestPath from airthings_ble.atom.response import AtomResponse _LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) def test_atom_response_wave_enhance() -> None: """Test the Wave Enhance request.""" random_bytes = bytes.fromhex("A1B2") response = AtomResponse( logger=_LOGGER, response=bytes.fromhex( "1001000345a1b281a2006d32393939392f302f333130313202583ea9634e4f49" + "182763544d501972f06348554d190d2f63434f321902dc63564f43190115634c5" + "55801635052531a005f364663424154190b346354494d1876" ), random_bytes=random_bytes, path=AtomRequestPath.LATEST_VALUES, ) sensor_data = response.parse() assert sensor_data is not None assert sensor_data["TMP"] == 29424 assert sensor_data["HUM"] == 3375 assert sensor_data["CO2"] == 732 assert sensor_data["VOC"] == 277 assert sensor_data["LUX"] == 1 assert sensor_data["PRS"] == 6239814 assert sensor_data["BAT"] == 2868 assert sensor_data["TIM"] == 118 assert sensor_data["NOI"] == 39 def test_atom_response_corentium_home_2() -> None: """Test the Wave Enhance request.""" random_bytes = bytes.fromhex("CCA4") response = AtomResponse( logger=_LOGGER, response=bytes.fromhex( "1001000345CCA481A2006D32393939392F302F3331303132025831A863523234" + "0363523744076352333007635231591263544D501973D76348554D190D8C63424" + "154190B816354494D19061D" ), random_bytes=random_bytes, path=AtomRequestPath.LATEST_VALUES, ) sensor_data = response.parse() assert sensor_data is not None assert sensor_data["TMP"] == 29655 assert sensor_data["HUM"] == 3468 assert sensor_data["BAT"] == 2945 assert sensor_data["TIM"] == 1565 assert sensor_data["R24"] == 3 assert sensor_data["R7D"] == 7 assert sensor_data["R30"] == 7 assert sensor_data["R1Y"] == 18 airthings-ble-1.1.0/tests/test_device_type.py000066400000000000000000000121421502657212600213220ustar00rootroot00000000000000import pytest from airthings_ble.device_type import AirthingsDeviceType def test_device_type() -> None: """Test device type.""" assert AirthingsDeviceType.WAVE_MINI.product_name == "Wave Mini" assert AirthingsDeviceType.WAVE_PLUS.product_name == "Wave Plus" assert AirthingsDeviceType.WAVE_RADON.product_name == "Wave Radon" assert AirthingsDeviceType.WAVE_GEN_1.product_name == "Wave Gen 1" assert AirthingsDeviceType.WAVE_ENHANCE_EU.product_name == "Wave Enhance" assert AirthingsDeviceType.WAVE_ENHANCE_US.product_name == "Wave Enhance" assert AirthingsDeviceType("2900") == AirthingsDeviceType.WAVE_GEN_1 assert AirthingsDeviceType("2920") == AirthingsDeviceType.WAVE_MINI assert AirthingsDeviceType("2930") == AirthingsDeviceType.WAVE_PLUS assert AirthingsDeviceType("2950") == AirthingsDeviceType.WAVE_RADON assert AirthingsDeviceType("3210") == AirthingsDeviceType.WAVE_ENHANCE_EU assert AirthingsDeviceType("3220") == AirthingsDeviceType.WAVE_ENHANCE_US with pytest.raises(ValueError): AirthingsDeviceType("1234") assert AirthingsDeviceType.from_raw_value("2900") == AirthingsDeviceType.WAVE_GEN_1 assert AirthingsDeviceType.from_raw_value("2920") == AirthingsDeviceType.WAVE_MINI assert AirthingsDeviceType.from_raw_value("2930") == AirthingsDeviceType.WAVE_PLUS assert AirthingsDeviceType.from_raw_value("2950") == AirthingsDeviceType.WAVE_RADON assert ( AirthingsDeviceType.from_raw_value("3210") == AirthingsDeviceType.WAVE_ENHANCE_EU ) assert ( AirthingsDeviceType.from_raw_value("3220") == AirthingsDeviceType.WAVE_ENHANCE_US ) unknown_device = AirthingsDeviceType.from_raw_value("1234") assert unknown_device == AirthingsDeviceType.UNKNOWN assert unknown_device.product_name == "Unknown" assert unknown_device.raw_value == "1234" def test_battery_calculation() -> None: """Test battery calculation for all devices.""" # Starting with the Wave Mini, since it has a different battery voltage range. # Max voltage is 4.5V, min voltage is 2.4V. # Starting with 5V, which is more than the max voltage assert AirthingsDeviceType.WAVE_MINI.battery_percentage(5.0) == 100 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(4.5) == 100 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(4.2) == 85 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(3.9) == 62 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(3.75) == 42 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(3.3) == 23 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(2.4) == 0 assert AirthingsDeviceType.WAVE_MINI.battery_percentage(2.3) == 0 # Repeat for the rest of the devices since they have the same amount of batteries. # Max voltage is 3.0V, min voltage is 2.1V. assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(3.2) == 100 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(3.0) == 100 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.8) == 81 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.6) == 53 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.5) == 28 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.2) == 5 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.1) == 0 assert AirthingsDeviceType.WAVE_PLUS.battery_percentage(2.0) == 0 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(3.2) == 100 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(3.0) == 100 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.8) == 81 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.6) == 53 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.5) == 28 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.2) == 5 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.1) == 0 assert AirthingsDeviceType.WAVE_RADON.battery_percentage(2.0) == 0 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(3.2) == 100 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(3.0) == 100 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.8) == 81 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.6) == 53 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.5) == 28 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.2) == 5 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.1) == 0 assert AirthingsDeviceType.WAVE_GEN_1.battery_percentage(2.0) == 0 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(3.2) == 100 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(3.0) == 100 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.8) == 81 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.6) == 53 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.5) == 28 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.2) == 5 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.1) == 0 assert AirthingsDeviceType.WAVE_ENHANCE_EU.battery_percentage(2.0) == 0 airthings-ble-1.1.0/tests/test_radon_level.py000066400000000000000000000006551502657212600213220ustar00rootroot00000000000000from airthings_ble.parser import get_radon_level def test_radon_level() -> None: assert get_radon_level(0) == "very low" assert get_radon_level(49) == "very low" assert get_radon_level(50) == "low" assert get_radon_level(99) == "low" assert get_radon_level(100) == "moderate" assert get_radon_level(299) == "moderate" assert get_radon_level(300) == "high" assert get_radon_level(1000) == "high" airthings-ble-1.1.0/tests/test_validate_value.py000066400000000000000000000034151502657212600220120ustar00rootroot00000000000000from airthings_ble.const import CO2_MAX, PERCENTAGE_MAX, PRESSURE_MAX, RADON_MAX from airthings_ble.parser import illuminance_converter, validate_value def test_validate_value_humidity() -> None: valid_humidity_values = [0, 50, 100.0] for value in valid_humidity_values: assert validate_value(value=value, max_value=PERCENTAGE_MAX) == value invalid_humidity_values = [-1, 100.1, 101] for value in invalid_humidity_values: assert validate_value(value=value, max_value=PERCENTAGE_MAX) is None def test_validate_value_radon() -> None: valid_radon_values = [0, 100, 1000.0, 16383] for value in valid_radon_values: assert validate_value(value=value, max_value=RADON_MAX) == value invalid_radon_values = [-1, 16384, 65535] for value in invalid_radon_values: assert validate_value(value=value, max_value=RADON_MAX) is None def test_validate_value_co2() -> None: valid_co2_values = [0, 100, 1000.0, 65534] for value in valid_co2_values: assert validate_value(value=value, max_value=CO2_MAX) == value invalid_co2_values = [-1, 65535] for value in invalid_co2_values: assert validate_value(value=value, max_value=CO2_MAX) is None def test_validate_value_illuminance() -> None: assert illuminance_converter(0) == 0 assert illuminance_converter(255) == 100 assert illuminance_converter(256) is None def test_validata_value_pressure() -> None: assert validate_value(value=0.0, max_value=PRESSURE_MAX) == 0 assert validate_value(value=1310.0, max_value=PRESSURE_MAX) == 1310 assert validate_value(value=1311.0, max_value=PRESSURE_MAX) is None assert validate_value(value=-1.0, max_value=PRESSURE_MAX) is None assert validate_value(value=65535.0, max_value=PRESSURE_MAX) is None airthings-ble-1.1.0/tests/test_wave_gen_1.py000066400000000000000000000006151502657212600210370ustar00rootroot00000000000000from airthings_ble.parser import _decode_wave_illum_accel def test_wave_gen_1_illuminance_and_accelerometer() -> None: """Test Wave Gen 1 illuminance and accelerometer.""" raw_data = bytearray.fromhex("b20c") decoded_data = _decode_wave_illum_accel( name="illuminance_accelerometer", format_type="BB", scale=1.0 )(raw_data) assert decoded_data["illuminance"] == 69 airthings-ble-1.1.0/tests/test_wave_mini.py000066400000000000000000000021121502657212600207740ustar00rootroot00000000000000import logging from airthings_ble.parser import WaveRadonAndPlusCommandDecode, _decode_wave_plus _LOGGER = logging.getLogger(__name__) def test_wave_plus_command_decode() -> None: """Test wave plus command decode.""" decode = WaveRadonAndPlusCommandDecode() assert decode.decode_data( logger=_LOGGER, raw_data=bytearray.fromhex( "6d00600c04000100008211ff00000000c04c20001f3560007006B80B0900" ), ) == {"battery": 3.0} def test_wave_plus_sensor_data() -> None: """Test wave plus sensor data.""" raw_data = bytearray.fromhex("01380d800b002200bd094cc31d036c0000007d05") decoded_data = _decode_wave_plus(name="Plus", format_type="<4B8H", scale=1.0)( raw_data ) assert decoded_data["humidity"] == 28.0 assert decoded_data["radon_1day_avg"] == 11 assert decoded_data["radon_longterm_avg"] == 34 assert decoded_data["temperature"] == 24.93 assert decoded_data["voc"] == 108 assert decoded_data["co2"] == 797 assert decoded_data["illuminance"] == 5 assert decoded_data["pressure"] == 999.92 airthings-ble-1.1.0/tests/test_wave_plus.py000066400000000000000000000021121502657212600210230ustar00rootroot00000000000000import logging from airthings_ble.parser import WaveRadonAndPlusCommandDecode, _decode_wave_plus _LOGGER = logging.getLogger(__name__) def test_wave_plus_command_decode() -> None: """Test wave plus command decode.""" decode = WaveRadonAndPlusCommandDecode() assert decode.decode_data( logger=_LOGGER, raw_data=bytearray.fromhex( "6d00600c04000100008211ff00000000c04c20001f3560007006B80B0900" ), ) == {"battery": 3.0} def test_wave_plus_sensor_data() -> None: """Test wave plus sensor data.""" raw_data = bytearray.fromhex("01380d800b002200bd094cc31d036c0000007d05") decoded_data = _decode_wave_plus(name="Plus", format_type="<4B8H", scale=1.0)( raw_data ) assert decoded_data["humidity"] == 28.0 assert decoded_data["radon_1day_avg"] == 11 assert decoded_data["radon_longterm_avg"] == 34 assert decoded_data["temperature"] == 24.93 assert decoded_data["voc"] == 108 assert decoded_data["co2"] == 797 assert decoded_data["illuminance"] == 5 assert decoded_data["pressure"] == 999.92 airthings-ble-1.1.0/tests/test_wave_radon.py000066400000000000000000000010701502657212600211450ustar00rootroot00000000000000import logging from airthings_ble.parser import _decode_wave_radon _LOGGER = logging.getLogger(__name__) def test_wave_radon_sensor_data() -> None: """Test wave plus sensor data.""" raw_data = bytearray.fromhex("013860f009001100a709ffffffffffff0000ffff") decoded_data = _decode_wave_radon(name="Wave2", format_type="<4B8H", scale=1.0)( raw_data ) assert decoded_data["humidity"] == 28.0 assert decoded_data["radon_1day_avg"] == 9 assert decoded_data["radon_longterm_avg"] == 17 assert decoded_data["temperature"] == 24.71