diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d0084f0..4b308fb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ jobs: - name: "Set up Python" uses: actions/setup-python@main with: - python-version: "3.12" + python-version: "3.13.2" cache: "pip" - name: "Install requirements" diff --git a/README.md b/README.md index 92a4adf..077ac74 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom rep 1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Hunter Douglas PowerView (BLE)" ## Set the Encryption Key -Currently, there are three methods to optain the key: +Currently, there are three methods to obtain the key: 1. Via adopting a BLE shade: There is a [shade emulator](/emu/PV_BLE_cover) that works with Arduino IDE and an ESP32 device (≥ 2MiB flash, ≥ 128KiB required), e.g. [Adafruit QT Py ESP32-S3](https://www.adafruit.com/product/5426). Install and connect via serial port, then go to the PowerView app and add the shade `myPVcover` to your home. You will see a log message `set shade key: \xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx\xx` . Copy this key. You can delete the shade from the app when done. 2. Extracting from gateway: This [script](scripts/extract_gateway3_homekey.py) is able to extract the key from a working PowerView gateway. -3. Grabing from the app: Checkout this [post in the Home Assistant community forum](https://community.home-assistant.io/t/hunter-douglas-powerview-gen-3-integration/424836/228). +3. Grabbing from the app: Checkout this [post in the Home Assistant community forum](https://community.home-assistant.io/t/hunter-douglas-powerview-gen-3-integration/424836/228). Finally, you need to manually copy the key to [`const.py`](https://github.com/patman15/hdpv_ble/blob/main/custom_components/hunterdouglas_powerview_ble/const.py). diff --git a/custom_components/hunterdouglas_powerview_ble/__init__.py b/custom_components/hunterdouglas_powerview_ble/__init__.py index 51665fb..e999bac 100644 --- a/custom_components/hunterdouglas_powerview_ble/__init__.py +++ b/custom_components/hunterdouglas_powerview_ble/__init__.py @@ -4,7 +4,9 @@ @license: Apache-2.0 license """ +from bleak.backends.device import BLEDevice from bleak.exc import BleakError + from homeassistant.components.bluetooth import async_ble_device_from_address from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -26,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntryType) -> bool if entry.unique_id is None: raise ConfigEntryError("Missing unique ID for device.") - ble_device = async_ble_device_from_address( + ble_device: BLEDevice | None = async_ble_device_from_address( hass=hass, address=entry.unique_id, connectable=True ) diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index 69fe257..0c5ccd5 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -1,9 +1,9 @@ """Hunter Douglas PowerView BLE API.""" import asyncio -import time from dataclasses import dataclass from enum import Enum +import time from typing import Final from bleak import BleakClient @@ -12,7 +12,11 @@ from bleak.exc import BleakError from bleak.uuids import normalize_uuid_str from bleak_retry_connector import establish_connection from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.primitives.ciphers.base import CipherContext +from cryptography.hazmat.primitives.ciphers.base import ( + AEADDecryptionContext, + AEADEncryptionContext, +) + from homeassistant.components.cover import ATTR_CURRENT_POSITION from .const import LOGGER, TIMEOUT @@ -26,6 +30,7 @@ ATTR_ACTIVITY: Final[str] = "activity" SHADE_TYPE: Final[dict[int, str]] = { + # up down only 1: "Designer Roller", 4: "Roman", 5: "Bottom Up", @@ -39,6 +44,11 @@ SHADE_TYPE: Final[dict[int, str]] = { 52: "Banded Shades", 53: "Sonnette", 84: "Vignette", + # top down bottom up + 8: "Duette, Top Down Bottom Up", + 9: "Duette DuoLite, Top Down Bottom Up", + 33: "Duette Architella, Top Down Bottom Up", + 47: "Pleated, Top Down Bottom Up", } OPEN_POSITION: Final[int] = 100 @@ -93,13 +103,13 @@ class PowerViewBLE: ], ) self._data_event = asyncio.Event() - self._data: bytearray + self._data: bytes = b"" self._info: PVDeviceInfo = PVDeviceInfo() - self._cmd_lock: Final = asyncio.Lock() - self._cmd_next = None self._is_encrypted: bool = False - self._cipher: Final = ( - Cipher(algorithms.AES(home_key), modes.CTR(bytearray(16))) + self._cmd_lock: Final = asyncio.Lock() + self._cmd_next: tuple[ShadeCmd, bytes] + self._cipher: Final[Cipher | None] = ( + Cipher(algorithms.AES(home_key), modes.CTR(bytes(16))) if len(home_key) == 16 else None ) @@ -129,7 +139,7 @@ class PowerViewBLE: # general cmd: uint16_t cmd, uint8_t seqID, uint8_t data_len async def _cmd( - self, cmd: tuple[ShadeCmd, bytearray], disconnect: bool = True + self, cmd: tuple[ShadeCmd, bytes], disconnect: bool = True ) -> None: self._cmd_next = cmd if self._cmd_lock.locked(): @@ -139,17 +149,15 @@ class PowerViewBLE: async with self._cmd_lock: try: await self._connect() - cmd_run = self._cmd_next - tx_data = ( - bytearray( - int.to_bytes(cmd_run[0].value, 2, byteorder="little") - + bytes([self._seqcnt, len(cmd_run[1])]) - ) + cmd_run: tuple[ShadeCmd, bytes] = self._cmd_next + tx_data: bytes = bytes( + int.to_bytes(cmd_run[0].value, 2, byteorder="little") + + bytes([self._seqcnt, len(cmd_run[1])]) + cmd_run[1] ) LOGGER.debug("sending cmd: %s", tx_data.hex(" ")) if self._cipher is not None and self._is_encrypted: - enc = self._cipher.encryptor() + enc: AEADEncryptionContext = self._cipher.encryptor() tx_data = enc.update(tx_data) + enc.finalize() LOGGER.debug(" encrypted: %s", tx_data.hex(" ")) self._data_event.clear() @@ -174,8 +182,8 @@ class PowerViewBLE: if len(data) != 9: LOGGER.debug("not a V2 record!") return [] - pos = int.from_bytes(data[3:5], byteorder="little") - pos2 = (int(data[5]) << 4) + (int(data[4]) >> 4) + pos: int = int.from_bytes(data[3:5], byteorder="little") + pos2: int = (int(data[5]) << 4) + (int(data[4]) >> 4) return [ (ATTR_CURRENT_POSITION, ((pos >> 2) / 10)), ("position2", pos2 >> 2), @@ -198,7 +206,7 @@ class PowerViewBLE: await self._cmd( ( ShadeCmd.SET_POSITION, - bytearray( + bytes( int.to_bytes(value * 100, 2, byteorder="little") + bytes([0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x0]) ), @@ -214,7 +222,7 @@ class PowerViewBLE: async def stop(self) -> None: """Stop device movement.""" LOGGER.debug("%s stop", self.name) - await self._cmd((ShadeCmd.STOP, bytearray())) + await self._cmd((ShadeCmd.STOP, b"")) async def close(self) -> None: """Fully close cover.""" @@ -230,19 +238,19 @@ class PowerViewBLE: await self._cmd( ( ShadeCmd.ACTIVATE_SCENE, - bytearray(int.to_bytes(idx, 1, byteorder="little") + bytes([0xA2])), + int.to_bytes(idx, 1, byteorder="little") + bytes([0xA2]), ), ) async def identify(self, beeps: int = 0x3) -> None: """Identify device.""" LOGGER.debug("%s identify (%i)", self.name, beeps) - await self._cmd((ShadeCmd.IDENTIFY, bytearray([min(beeps, 0xFF)]))) + await self._cmd((ShadeCmd.IDENTIFY, bytes([min(beeps, 0xFF)]))) - def _verify_response(self, data: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: + def _verify_response(self, data: bytes, seq_nr: int, cmd: ShadeCmd) -> bool: """Verify shade response data.""" if len(data) < 4: - LOGGER.error("Reponse message too short") + LOGGER.error("Response message too short") return False if int.from_bytes(data[0:2], byteorder="little") != cmd.value & 0xFFEF: LOGGER.warning("Response to wrong command") @@ -298,10 +306,10 @@ class PowerViewBLE: def _notification_handler(self, _sender, data: bytearray) -> None: LOGGER.debug("%s received BLE data: %s", self.name, data.hex(" ")) - self._data = data + self._data = bytes(data) if self._cipher is not None and self._is_encrypted: - dec: CipherContext = self._cipher.decryptor() - self._data = bytearray(dec.update(data) + dec.finalize()) + dec: AEADDecryptionContext = self._cipher.decryptor() + self._data = bytes(dec.update(bytes(data)) + dec.finalize()) LOGGER.debug("%s %s", "decoded data: ".rjust(19+len(self.name)), self._data.hex(" ")) self._data_event.set() diff --git a/custom_components/hunterdouglas_powerview_ble/binary_sensor.py b/custom_components/hunterdouglas_powerview_ble/binary_sensor.py index 97d3365..dda9127 100644 --- a/custom_components/hunterdouglas_powerview_ble/binary_sensor.py +++ b/custom_components/hunterdouglas_powerview_ble/binary_sensor.py @@ -49,7 +49,7 @@ class PVBinarySensor(PassiveBluetoothCoordinatorEntity[PVCoordinator], BinarySen descr: BinarySensorEntityDescription, unique_id: str, ) -> None: - """Intialize PV binary sensor.""" + """Initialize PV binary sensor.""" self._attr_unique_id = f"{DOMAIN}-{unique_id}-{descr.key}" self._attr_device_info = coord.device_info self._attr_has_entity_name = True diff --git a/custom_components/hunterdouglas_powerview_ble/config_flow.py b/custom_components/hunterdouglas_powerview_ble/config_flow.py index 24c250a..7a38b35 100644 --- a/custom_components/hunterdouglas_powerview_ble/config_flow.py +++ b/custom_components/hunterdouglas_powerview_ble/config_flow.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Any import voluptuous as vol + from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, @@ -11,9 +12,11 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ADDRESS - -# from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) from .api import UUID_COV_SERVICE as UUID from .const import DOMAIN, LOGGER, MFCT_ID @@ -63,7 +66,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry( title=self._discovered_device.name, - data={"manufacturer_data": self._discovered_device.discovery_info.manufacturer_data[MFCT_ID].hex()}, + data={ + "manufacturer_data": self._discovered_device.discovery_info.manufacturer_data[ + MFCT_ID + ].hex() + }, ) self._set_confirm_only() @@ -89,7 +96,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self._discovered_device.name, - data={"manufacturer_data": self._discovered_device.discovery_info.manufacturer_data[MFCT_ID].hex()}, + data={ + "manufacturer_data": self._discovered_device.discovery_info.manufacturer_data[ + MFCT_ID + ].hex() + }, ) current_addresses = self._async_current_ids() @@ -111,7 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not self._discovered_devices: return self.async_abort(reason="no_devices_found") - titles = [] + titles: list[SelectOptionDict] = [] for address, discovery in self._discovered_devices.items(): titles.append({"value": address, "label": discovery.name}) diff --git a/custom_components/hunterdouglas_powerview_ble/const.py b/custom_components/hunterdouglas_powerview_ble/const.py index a4873fd..d723594 100644 --- a/custom_components/hunterdouglas_powerview_ble/const.py +++ b/custom_components/hunterdouglas_powerview_ble/const.py @@ -3,16 +3,6 @@ import logging from typing import Final -# from bleak.uuids import normalize_uuid_str - -# from homeassistant.const import ( # noqa: F401 -# ATTR_BATTERY_CHARGING, -# ATTR_BATTERY_LEVEL, -# ATTR_TEMPERATURE, -# ATTR_VOLTAGE, -# ) - - DOMAIN: Final[str] = "hunterdouglas_powerview_ble" LOGGER: Final = logging.getLogger(__package__) MFCT_ID: Final[int] = 2073 diff --git a/custom_components/hunterdouglas_powerview_ble/coordinator.py b/custom_components/hunterdouglas_powerview_ble/coordinator.py index f283fe7..68883cc 100644 --- a/custom_components/hunterdouglas_powerview_ble/coordinator.py +++ b/custom_components/hunterdouglas_powerview_ble/coordinator.py @@ -3,6 +3,7 @@ from typing import Any from bleak.backends.device import BLEDevice + from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -80,7 +81,7 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator): def _async_stop(self) -> None: """Shutdown coordinator and any connection.""" - LOGGER.debug("%s: shuting down BMS device", self.name) + LOGGER.debug("%s: shutting down BMS device", self.name) self.hass.async_create_task(self.api.disconnect()) super()._async_stop() diff --git a/custom_components/hunterdouglas_powerview_ble/cover.py b/custom_components/hunterdouglas_powerview_ble/cover.py index 7134592..5295fc1 100644 --- a/custom_components/hunterdouglas_powerview_ble/cover.py +++ b/custom_components/hunterdouglas_powerview_ble/cover.py @@ -3,6 +3,7 @@ from typing import Any, Final from bleak.exc import BleakError + from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) diff --git a/custom_components/hunterdouglas_powerview_ble/sensor.py b/custom_components/hunterdouglas_powerview_ble/sensor.py index ce275ba..d19333d 100644 --- a/custom_components/hunterdouglas_powerview_ble/sensor.py +++ b/custom_components/hunterdouglas_powerview_ble/sensor.py @@ -3,14 +3,8 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, -) -from homeassistant.components.sensor.const import ( - SensorDeviceClass, - SensorStateClass, -) +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass from homeassistant.const import ( ATTR_BATTERY_LEVEL, PERCENTAGE, @@ -67,7 +61,7 @@ class PVSensor(PassiveBluetoothCoordinatorEntity[PVCoordinator], SensorEntity): def __init__( self, pv_dev: PVCoordinator, descr: SensorEntityDescription, unique_id: str ) -> None: - """Intitialize the BMS sensor.""" + """Initialize the BMS sensor.""" self._attr_unique_id = f"{DOMAIN}-{unique_id}-{descr.key}" self._attr_device_info = pv_dev.device_info self.entity_description = descr diff --git a/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 0506c37..26614f4 100644 --- a/emu/PV_BLE_cover/PV_BLE_cover.ino +++ b/emu/PV_BLE_cover/PV_BLE_cover.ino @@ -166,7 +166,7 @@ void decode(BLECharacteristic *pChar) { memcpy((void *)&msg, data_dec, 4); Serial.printf("\t message: SRV: %02x, CMD %02x, SEQ %i, LEN %i\n", msg.serviceID, msg.cmdID, msg.sequence, msg.data_len); - // sepecial responses (static data!) + // special responses (static data!) const byte ret_valF1DD[] = { 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // product info const byte ret_valFFDD[] = { 0x00, 0x05, 0xd1, 0xa2, 0x9a, 0x42, 0x59, 0x5d, 0x5c, 0x52, 0x1b, 0x00, 0x00, 0x00, (uint8_t)(SW_VERSION & 0xFF), (uint8_t)(SW_VERSION >> 8), 0x00, 0x00, 0x5f, 0x9c, 0x02, 0x00, 0x5f, 0x9c, 0x02, 0x00, TYP_ID, MODEL_ID, 0x08 }; // HW diagnostics const byte ret_valFFDE[] = { 0x08, 0x00, 0x02, 0x26, 0x72, 0x01, 0x59, 0x01, 0x00 }; // power status @@ -217,7 +217,7 @@ void decode(BLECharacteristic *pChar) { // set shade key Serial.print("set shade key: "); print_hex(&data_raw[4], data_len - 4, "\\x", ""); - // set resonse before key, to acknowledge unencrypted + // set response before key, to acknowledge unencrypted resp_size = set_response(&response, (const message *)data_dec); if (msg.data_len == 16) { memcpy(home_key, &data_raw[4], 16); diff --git a/pyproject.toml b/pyproject.toml index 59f0065..5762d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,17 @@ # pyproject.toml [project] -requires-python = ">=3.12.0" +name = "hunterdouglas_powerview_ble" +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.13" +] +readme = "README.md" +requires-python = ">=3.13.2" + +[project.urls] +"Source Code" = "https://github.com/patman15/hdpv_ble/" +"Bug Reports" = "https://github.com/patman15/hdpv_ble/issues" [tool.pytest.ini_options] minversion = "8.0" @@ -14,12 +24,9 @@ testpaths = [ ] asyncio_mode = "auto" -# ruff configuration taken from HA 2024.11.2 (less ignores) +# ruff settings from HA 2025.2.2 [tool.ruff] -required-version = ">=0.6.8" - -[tool.ruff.lint.per-file-ignores] -"scripts/*" = ["T201"] +required-version = ">=0.9.1" [tool.ruff.lint] select = [ @@ -38,8 +45,10 @@ select = [ "B017", # pytest.raises(BaseException) should be considered evil "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} + "B024", # `{name}` is an abstract base class, but it has no abstract methods or properties "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B035", # Dictionary comprehension uses static key "B904", # Use raise from to specify exception cause "B905", # zip() without an explicit strict= parameter "BLE", @@ -73,12 +82,27 @@ select = [ "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs + "RUF008", # Do not use mutable default values for dataclass attributes "RUF010", # Use explicit conversion flag "RUF013", # PEP 484 prohibits implicit Optional + "RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer "RUF017", # Avoid quadratic list summation "RUF018", # Avoid assignment expressions in assert statements "RUF019", # Unnecessary key check before dictionary access - # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up + "RUF020", # {never_like} | T is equivalent to T + "RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear + "RUF022", # Sort __all__ + "RUF023", # Sort __slots__ + "RUF024", # Do not pass mutable objects as values to dict.fromkeys + "RUF026", # default_factory is a positional-only argument to defaultdict + "RUF030", # print() call in assert statement is likely unintentional + "RUF032", # Decimal() called with float literal argument + "RUF033", # __post_init__ method with argument defaults + "RUF034", # Useless if-else condition + "RUF100", # Unused `noqa` directive + "RUF101", # noqa directives that use redirected rule codes + "RUF200", # Failed to parse pyproject.toml: {message} "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file @@ -91,7 +115,7 @@ select = [ "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage - "S320", # suspicious-xmle-tree-usage +# "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true @@ -102,7 +126,7 @@ select = [ "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "TID", # Tidy imports "TRY", # tryceratops "UP", # pyupgrade @@ -122,13 +146,12 @@ ignore = [ # "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives # "PLR0911", # Too many return statements ({returns} > {max_returns}) # "PLR0912", # Too many branches ({branches} > {max_branches}) -# "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) # "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable # "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target -# "PT004", # Fixture {fixture} does not return anything, add leading underscore # "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception -# "PT018", # Assertion should be broken down into multiple parts + "PT018", # Assertion should be broken down into multiple parts # "RUF001", # String contains ambiguous unicode character. # "RUF002", # Docstring contains ambiguous unicode character. # "RUF003", # Comment contains ambiguous unicode character. @@ -139,14 +162,12 @@ ignore = [ # "SIM115", # Use context handler for opening files # Moving imports into type-checking blocks can mess with pytest.patch() - "TCH001", # Move application import {} into a type-checking block - "TCH002", # Move third-party import {} into a type-checking block - "TCH003", # Move standard library import {} into a type-checking block + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` - # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 - "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", @@ -163,3 +184,15 @@ ignore = [ # Disabled because ruff does not understand type of __all__ generated by a function "PLE0605" ] + + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [ + "homeassistant", +] +combine-as-imports = true +split-on-trailing-comma = false + +[tool.ruff.lint.per-file-ignores] +"scripts/*" = ["T201"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bf8d48b..4619487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -homeassistant==2024.11.0 +homeassistant==2025.11.0 pip>=21.3.1 -ruff==0.6.8 +ruff>=0.9.1,<=0.15.0 diff --git a/requirements_test.txt b/requirements_test.txt index 3ca9df3..495ede7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,9 +2,9 @@ wheel home-assistant-bluetooth -habluetooth>=3.6.0 +habluetooth>=5.3.0 bluetooth-adapters -pytest>=8.3.3 +pytest>=8.4.1 pytest-cov>=5.0.0 pytest-socket>=0.7.0 pytest-asyncio>=0.24.0 @@ -16,10 +16,10 @@ aiohttp aiohttp_cors aiohttp-fast-url-dispatcher aiohttp-zlib-ng -bleak>=0.22.3 -bleak-retry-connector>=3.6.0 +bleak>=1.0.1 +bleak-retry-connector>=4.4.3 bluetooth-data-tools pyserial-asyncio pyudev -pytest-homeassistant-custom-component==0.13.181 +pytest-homeassistant-custom-component==0.13.294 diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py index b52b1e3..0058c3d 100644 --- a/scripts/extract_gateway3_homekey.py +++ b/scripts/extract_gateway3_homekey.py @@ -66,7 +66,7 @@ def get_shade_key(hub: str, ble_name) -> bytes: return dec_resp["data"] -def main(hub: str) -> None: +def main(hub: str) -> int: """Extract the homekeys from all shades.""" try: shades_resp: requests.Response = requests.get( @@ -75,7 +75,7 @@ def main(hub: str) -> None: shades_resp.raise_for_status() except requests.exceptions.RequestException as ex: print(f"Unable to get list of shades:\n\t{ex!s}") - return + return -1 shades = json.loads(shades_resp.content) print(f"Found {len(shades)} shades, interrogating") @@ -87,6 +87,8 @@ def main(hub: str) -> None: print(f"\tBLE name: '{shade['bleName']}'") print(f"\tHomeKey: {key.hex()}") + return 0 + if __name__ == "__main__": import argparse