diff --git a/.github/workflows/_validate.bak b/.github/workflows/_validate.bak new file mode 100644 index 0000000..df59ded --- /dev/null +++ b/.github/workflows/_validate.bak @@ -0,0 +1,19 @@ +name: Validate with HACS + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 5 * * 6' + +jobs: + validate-hacs: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" + diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 0000000..21a14df --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,16 @@ +name: Validate with hassfest + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 6 * * 6' + +jobs: + validate-hassfest: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + - name: HA validation + uses: "home-assistant/actions/hassfest@master" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d0084f0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: "Lint the code" + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 5 * * 6' + +jobs: + ruff: + name: "Ruff" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@main" + + - name: "Set up Python" + uses: actions/setup-python@main + with: + python-version: "3.12" + cache: "pip" + + - name: "Install requirements" + run: python3 -m pip install -r requirements.txt + + - name: "Run Ruff" + run: python3 -m ruff check . diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..963cb60 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,22 @@ +name: 'Close stale issues and PR' +on: + schedule: + - cron: '01 2 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 8 days.' + stale-pr-message: 'This PR is stale because it has been open 45 days with no activity.' + close-issue-message: 'This issue was closed because it has been stalled for 15 days with no activity.' + exempt-issue-labels: 'bug' + exempt-all-assignees: true + days-before-stale: 45 + days-before-close: 15 + days-before-pr-close: -1 diff --git a/.gitignore b/.gitignore index d9005f2..b68bb3b 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +aes.py +*.bak diff --git a/README.md b/README.md index fc55a17..6af5195 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@ A Home Assistant integration to support Hunter Douglas Powerview devices via Bluetooth -## :warning: Limitations -- This integration is under development! -- Test coverage is low, malfunction might occur. -- Only devices that are **not** added to the app are controlable. It is possible to add them to the app if you just want to monitor the status (position, battery) in Home Assistant. -- Currently only position change is supported (e.g., no tilt) +> [!WARNING] +> - This integration is under development! +> - Test coverage is low, malfunction might occur. +> - Currently only position change is supported (e.g., no tilt) ## Features - Zero configuration @@ -18,7 +17,7 @@ ### Supported Devices Type* | Description --- | -- +-- | -- 1 | Designer Roller 4 | Roman 5 | Bottom Up @@ -39,10 +38,14 @@ The integration provides the following information about the battery Platform | Description | Unit | Details -- | -- | -- | -- `binary_sensor` | battery charging indicator | `bool` | true if battery is charging +`button` | identify shade | - | identify shade by LED and 3 beeps `cover` | view/control position | `%` | percentage cover is open (100% is open) `sensor` | SoC (state of charge) | `%` | range 100% (full), 50%, 20%, 0% (battery empty) ## Installation +> [!IMPORTANT] +> In case you added your shades to the app or a gateway, you need to [set the encryption key](#set-the-encryption-key) manually in the [`const.py`](https://github.com/patman15/hdpv_ble/blob/main/custom_components/hunterdouglas_powerview_ble/const.py) file after **each** update! + ### Automatic Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom repository](https://hacs.xyz/docs/faq/custom_repositories/). @@ -57,6 +60,17 @@ Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom rep 1. Restart Home Assistant 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: + +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 [PR](https://github.com/patman15/hdpv_ble/pull/2) proposes a script 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). + +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). + +> [!IMPORTANT] +> You need to update the file after **each** update! ## Outlook - Add support for encryption diff --git a/custom_components/hunterdouglas_powerview_ble/__init__.py b/custom_components/hunterdouglas_powerview_ble/__init__.py index abbdce4..51665fb 100644 --- a/custom_components/hunterdouglas_powerview_ble/__init__.py +++ b/custom_components/hunterdouglas_powerview_ble/__init__.py @@ -5,7 +5,6 @@ """ 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 @@ -15,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from .const import DOMAIN, LOGGER from .coordinator import PVCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR, Platform.BUTTON] type ConfigEntryType = ConfigEntry[PVCoordinator] diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index e45fca2..70bb7f2 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -1,28 +1,28 @@ """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 from bleak.backends.device import BLEDevice from bleak.exc import BleakError from bleak.uuids import normalize_uuid_str -from bleak_retry_connector import close_stale_connections, establish_connection +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 homeassistant.components.cover import ATTR_CURRENT_POSITION from .const import LOGGER, TIMEOUT -UUID_COV_SERVICE: Final = normalize_uuid_str("fdc1") -UUID_TX: Final = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0" -UUID_DEV_SERVICE: Final = normalize_uuid_str("180a") -UUID_BAT_SERVICE: Final = normalize_uuid_str("180f") +UUID_COV_SERVICE: Final[str] = normalize_uuid_str("fdc1") +UUID_TX: Final[str] = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0" +UUID_DEV_SERVICE: Final[str] = normalize_uuid_str("180a") +UUID_BAT_SERVICE: Final[str] = normalize_uuid_str("180f") -ATTR_ACTIVITY: Final = "activity" +ATTR_ACTIVITY: Final[str] = "activity" SHADE_TYPE: Final[dict[int, str]] = { @@ -59,6 +59,7 @@ class ShadeCmd(Enum): SET_POSITION = 0x01F7 STOP = 0xB8F7 ACTIVATE_SCENE = 0xBAF7 + IDENTIFY = 0x11F7 @dataclass @@ -82,12 +83,21 @@ class PowerViewBLE: self._ble_device: Final[BLEDevice] = ble_device self.name: Final[str] = self._ble_device.name or "unknown" self._seqcnt: int = 1 - self._client: BleakClient | None = None + self._client: BleakClient = BleakClient( + self._ble_device, + disconnected_callback=self._on_disconnect, + services=[ + UUID_COV_SERVICE, + # self.UUID_DEV_SERVICE, + # self.UUID_BAT_SERVICE, + ], + ) self._data_event = asyncio.Event() self._data: bytearray 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))) if len(home_key) == 16 @@ -98,6 +108,15 @@ class PowerViewBLE: await self._data_event.wait() self._data_event.clear() + @property + def encrypted(self) -> bool: + """Return whether communication with this shade is encrypted.""" + return self._is_encrypted + + @encrypted.setter + def encrypted(self, value: bool) -> None: + self._is_encrypted = value + @property def info(self) -> PVDeviceInfo: """Return device information, e.g. SW version.""" @@ -106,7 +125,7 @@ class PowerViewBLE: @property def is_connected(self) -> bool: """Return whether remote device is connected.""" - return self._client is not None and self._client.is_connected + return self._client.is_connected # general cmd: uint16_t cmd, uint8_t seqID, uint8_t data_len async def _cmd( @@ -120,7 +139,6 @@ class PowerViewBLE: async with self._cmd_lock: try: await self._connect() - assert self._client is not None, "missing BT client" cmd_run = self._cmd_next tx_data = ( bytearray( @@ -129,11 +147,12 @@ class PowerViewBLE: ) + cmd_run[1] ) - if self._cipher is not None: + LOGGER.debug("sending cmd: %s", tx_data.hex(" ")) + if self._cipher is not None and self._is_encrypted: enc = self._cipher.encryptor() tx_data = enc.update(tx_data) + enc.finalize() + LOGGER.debug(" encrypted: %s", tx_data.hex(" ")) self._data_event.clear() - LOGGER.debug("sending cmd: %s", tx_data) await self._client.write_gatt_char(UUID_TX, tx_data, False) self._seqcnt += 1 LOGGER.debug("waiting for response") @@ -156,14 +175,14 @@ class PowerViewBLE: 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)) + pos2 = (int(data[5]) << 4) + (int(data[4]) >> 4) return [ (ATTR_CURRENT_POSITION, ((pos >> 2) / 10)), ("position2", pos2 >> 2), ("position3", int(data[6])), ("tilt", int(data[7])), ("home_id", int.from_bytes(data[0:2], byteorder="little")), - ("type_id", int.from_bytes(data[2:3])), + ("type_id", int(data[2])), ("is_opening", bool(pos & 0x3 == 0x2)), ("is_closing", bool(pos & 0x3 == 0x1)), ("battery_charging", bool(pos & 0x3 == 0x3)), # observed @@ -195,7 +214,7 @@ class PowerViewBLE: async def stop(self) -> None: """Stop device movement.""" LOGGER.debug("%s stop", self.name) - await self._cmd((ShadeCmd.STOP, bytearray(b""))) + await self._cmd((ShadeCmd.STOP, bytearray())) async def close(self) -> None: """Fully close cover.""" @@ -215,12 +234,13 @@ class PowerViewBLE: ), ) - def _verify_response(self, input: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: + 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)]))) + + def _verify_response(self, data: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: """Verify shade response data.""" - data = input - if self._cipher is not None: - dec = self._cipher.decryptor() - data = dec.update(input) + dec.finalize() if len(data) < 4: LOGGER.error("Reponse message too short") return False @@ -229,14 +249,14 @@ class PowerViewBLE: return False if int(data[2]) != seq_nr: LOGGER.warning( - f"Response sequence id {int(data[2])} wrong, expected {seq_nr}" + "Response sequence id %i wrong, expected %d", int(data[2]), seq_nr ) return False if int(data[3]) != 1: LOGGER.error("Wrong response data length") return False if int(data[4] != 0): - LOGGER.error(f"Command {cmd.value} returned error #{int(data[4])}") + LOGGER.error("Command %X returned error #%d", cmd.value, int(data[4])) return False return True @@ -255,7 +275,6 @@ class PowerViewBLE: async with self._cmd_lock: try: await self._connect() - assert self._client is not None for key, uuid in uuids.items(): LOGGER.debug("querying %s(%s)", key, uuid) @@ -265,7 +284,7 @@ class PowerViewBLE: .decode("UTF-8") ) finally: - await self._disconnect() + await self.disconnect() LOGGER.debug("%s device data: %s", self.name, data) return data.copy() @@ -275,8 +294,13 @@ class PowerViewBLE: LOGGER.debug("Disconnected from %s", client.address) def _notification_handler(self, _sender, data: bytearray) -> None: - LOGGER.debug("%s received BLE data: %s", self.name, data) + LOGGER.debug("%s received BLE data: %s", self.name, data.hex(" ")) self._data = data + if self._cipher is not None and self._is_encrypted: + dec: CipherContext = self._cipher.decryptor() + self._data = bytearray(dec.update(data) + dec.finalize()) + LOGGER.debug("%s %s", "decoded data: ".rjust(19+len(self.name)), self._data.hex(" ")) + self._data_event.set() async def _connect(self) -> None: @@ -288,9 +312,7 @@ class PowerViewBLE: LOGGER.debug("%s already connected", self.name) return - start = time.time() - await close_stale_connections(self._ble_device) - + start: float = time.time() self._client = await establish_connection( BleakClient, self._ble_device, @@ -308,10 +330,10 @@ class PowerViewBLE: # await self._query_dev_info() - async def _disconnect(self) -> None: + async def disconnect(self) -> None: """Disconnect the device and stop notifications.""" - if self._client is not None and self.is_connected: + if self.is_connected: LOGGER.debug("Disconnecting device %s", self.name) try: self._data_event.clear() diff --git a/custom_components/hunterdouglas_powerview_ble/button.py b/custom_components/hunterdouglas_powerview_ble/button.py new file mode 100644 index 0000000..bf7649b --- /dev/null +++ b/custom_components/hunterdouglas_powerview_ble/button.py @@ -0,0 +1,71 @@ +"""Hunter Douglas Powerview cover.""" + +from typing import Final + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, LOGGER +from .coordinator import PVCoordinator + +BUTTONS_SHADE: Final = [ + ButtonEntityDescription( + key="identify", + device_class=ButtonDeviceClass.IDENTIFY, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + _hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo cover platform.""" + + coordinator: PVCoordinator = config_entry.runtime_data + for descr in BUTTONS_SHADE: + async_add_entities([PowerViewButton(coordinator, descr)]) + + +class PowerViewButton(PassiveBluetoothCoordinatorEntity[PVCoordinator], ButtonEntity): # type: ignore[reportIncompatibleVariableOverride] + """Representation of a powerview shade.""" + + _attr_has_entity_name = True + _attr_device_class = ButtonDeviceClass.IDENTIFY + + def __init__( + self, + coordinator: PVCoordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize the shade.""" + self.entity_description = description + self._coord: PVCoordinator = coordinator + self._attr_device_info = self._coord.device_info + self._attr_unique_id = ( + f"{DOMAIN}_{format_mac(self._coord.address)}_{ButtonDeviceClass.IDENTIFY}" + ) + super().__init__(coordinator) + + @property + def device_info(self) -> DeviceInfo: # type: ignore[reportIncompatibleVariableOverride] + """Return the device_info of the device.""" + return self._coord.device_info + + async def async_press(self) -> None: + """Handle the button press.""" + LOGGER.debug("identify cover") + await self._coord.api.identify() diff --git a/custom_components/hunterdouglas_powerview_ble/config_flow.py b/custom_components/hunterdouglas_powerview_ble/config_flow.py index 5879d97..24c250a 100644 --- a/custom_components/hunterdouglas_powerview_ble/config_flow.py +++ b/custom_components/hunterdouglas_powerview_ble/config_flow.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from typing import Any import voluptuous as vol - from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, diff --git a/custom_components/hunterdouglas_powerview_ble/const.py b/custom_components/hunterdouglas_powerview_ble/const.py index 455c3c4..a4873fd 100644 --- a/custom_components/hunterdouglas_powerview_ble/const.py +++ b/custom_components/hunterdouglas_powerview_ble/const.py @@ -13,12 +13,15 @@ from typing import Final # ) -DOMAIN: Final = "hunterdouglas_powerview_ble" +DOMAIN: Final[str] = "hunterdouglas_powerview_ble" LOGGER: Final = logging.getLogger(__package__) -MFCT_ID: Final = 2073 -TIMEOUT: Final = 5 -HOME_KEY: Final = b"" +MFCT_ID: Final[int] = 2073 +TIMEOUT: Final[int] = 5 + +# put the key here, needs to be 16 bytes long, e.g. +# HOME_KEY: Final[bytes] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" +HOME_KEY: Final[bytes] = b"" # attributes (do not change) -ATTR_RSSI = "rssi" +ATTR_RSSI: Final[str] = "rssi" diff --git a/custom_components/hunterdouglas_powerview_ble/coordinator.py b/custom_components/hunterdouglas_powerview_ble/coordinator.py index 59198ce..f283fe7 100644 --- a/custom_components/hunterdouglas_powerview_ble/coordinator.py +++ b/custom_components/hunterdouglas_powerview_ble/coordinator.py @@ -3,9 +3,8 @@ from typing import Any from bleak.backends.device import BLEDevice - from homeassistant.components import bluetooth -from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN +from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothDataUpdateCoordinator, ) @@ -79,6 +78,12 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator): """Check if a device is present.""" return bluetooth.async_address_present(self.hass, self._mac, connectable=True) + def _async_stop(self) -> None: + """Shutdown coordinator and any connection.""" + LOGGER.debug("%s: shuting down BMS device", self.name) + self.hass.async_create_task(self.api.disconnect()) + super()._async_stop() + @callback def _async_handle_bluetooth_event( self, @@ -98,6 +103,7 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator): bytearray(service_info.manufacturer_data.get(2073, b"")) ) ) + self.api.encrypted = bool(self.data.get("home_id")) LOGGER.debug("data sample %s", self.data) super()._async_handle_bluetooth_event(service_info, change) diff --git a/custom_components/hunterdouglas_powerview_ble/cover.py b/custom_components/hunterdouglas_powerview_ble/cover.py index a35abee..7134592 100644 --- a/custom_components/hunterdouglas_powerview_ble/cover.py +++ b/custom_components/hunterdouglas_powerview_ble/cover.py @@ -3,7 +3,6 @@ from typing import Any, Final from bleak.exc import BleakError - from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) @@ -127,7 +126,10 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti self.async_write_ha_state() except BleakError as err: LOGGER.error( - f"Failed to move cover '{self.name}' to {target_position}%: {err}" + "Failed to move cover '%s' to %f%%: %s", + self.name, + target_position, + err, ) def _reset_target_position(self) -> None: @@ -143,7 +145,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti await self._coord.api.open() self.async_write_ha_state() except BleakError as err: - LOGGER.error(f"Failed to open cover '{self.name}': {err}") + LOGGER.error("Failed to open cover '%s': %s", self.name, err) self._reset_target_position() async def async_close_cover(self, **kwargs: Any) -> None: @@ -156,7 +158,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti await self._coord.api.close() self.async_write_ha_state() except BleakError as err: - LOGGER.error(f"Failed to close cover '{self.name}': {err}") + LOGGER.error("Failed to close cover '%s': %s", self.name, err) self._reset_target_position() async def async_stop_cover(self, **kwargs: Any) -> None: @@ -167,4 +169,4 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti self._reset_target_position() self.async_write_ha_state() except BleakError as err: - LOGGER.error(f"Failed to stop cover '{self.name}': {err}") + LOGGER.error("Failed to stop cover '%s': %s", self.name, err) diff --git a/custom_components/hunterdouglas_powerview_ble/manifest.json b/custom_components/hunterdouglas_powerview_ble/manifest.json index 0038dc6..4bbb5b3 100644 --- a/custom_components/hunterdouglas_powerview_ble/manifest.json +++ b/custom_components/hunterdouglas_powerview_ble/manifest.json @@ -17,5 +17,5 @@ "issue_tracker": "https://github.com/patman15/hdpv_ble/issues", "loggers": ["hunterdouglas_powerview_ble"], "requirements": ["cryptography>=43.0.0"], - "version": 0.21 + "version": "0.22" } diff --git a/custom_components/hunterdouglas_powerview_ble/sensor.py b/custom_components/hunterdouglas_powerview_ble/sensor.py index bb9ea8c..ce275ba 100644 --- a/custom_components/hunterdouglas_powerview_ble/sensor.py +++ b/custom_components/hunterdouglas_powerview_ble/sensor.py @@ -4,9 +4,11 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, SensorEntity, SensorEntityDescription, +) +from homeassistant.components.sensor.const import ( + SensorDeviceClass, SensorStateClass, ) from homeassistant.const import ( diff --git a/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 3f7e733..a1f26d6 100644 --- a/emu/PV_BLE_cover/PV_BLE_cover.ino +++ b/emu/PV_BLE_cover/PV_BLE_cover.ino @@ -1,40 +1,58 @@ /** * Emulate a Hunter Douglas PowerView cover device using ESP32 + * used e.g. to gain the home_key from an existing installation via BLE * * TODO: - * - adding device to appartement does only work after long timeout, - * as some feedback to "reset scene automations" is expected + * - cleanup code + * - think about emulating a remote * * AUTHOR: patman15 * LICENSE: GPLv2 */ +#define NAME "myPVcover" +#define FW_VERSION "391" +#define SERIAL_NR "01234567890ABCDEF" + + #include #include #include #include +#define WOLFSSL_USER_SETTINGS +#include +#include "wolfssl/wolfcrypt/aes.h" + +Aes aes_coder; +void *hint = NULL; +int devId = INVALID_DEVID; //if not using async INVALID_DEVID is default + #include #include -#define NAME "myPVcover" - #define COVER_SERVICE_UUID "0000FDC1-0000-1000-8000-00805f9b34fb" #define COVER_CHAR_UUID "CAFE1001-C0FF-EE01-8000-A110CA7AB1E0" #define XXX_CHAR_UUID "CAFE1002-C0FF-EE01-8000-A110CA7AB1E0" -//#define FW_SERVICE_UUID "CAFE8000-C0FF-EE01-8000-A110CA7AB1E0" +#define FW_SERVICE_UUID "CAFE8000-C0FF-EE01-8000-A110CA7AB1E0" +#define FW_CHAR_UUID "CAFE8003-C0FF-EE01-8000-A110CA7AB1E0" + +#define DEV_SERVICE_UUID BLEUUID("180A") +#define SER_CHAR_UUID BLEUUID("2A25") +#define SWC_CHAR_UUID BLEUUID("2A28") #define BAT_SERVICE_UUID BLEUUID("180F") #define BAT_CHAR_UUID BLEUUID("2A19") - +#define DAT_LEN 255 #pragma pack(1) -struct header { +struct message { uint8_t serviceID; uint8_t cmdID; uint8_t sequence; uint8_t data_len; + uint8_t data[DAT_LEN]; }; struct position { @@ -45,151 +63,280 @@ struct position { uint8_t velocity; }; +struct notification { + uint8_t *data; + BLECharacteristic *characteristic; +}; + BLECharacteristic *pCharacteristic_cover, *pCharacteristic_fw, *pCharacteristic_unknown, *pCharacteristic_bat; +BLECharacteristic *pCharacteristic_dev, *pCharacteristic_ser; BLEServer *pServer = NULL; bool deviceConnected = false; bool oldDeviceConnected = false; +struct notification rx_data; +volatile bool data_available = false; +const byte zero_key[16] = { 0 }; +byte home_key[16] = { 0 }; -void Serialprintln(const char* input...) { - va_list args; - va_start(args, input); - for(const char* i=input; *i!=0; ++i) { - if(*i!='%') { Serial.print(*i); continue; } - switch(*(++i)) { - case '%': Serial.print('%'); break; - case 's': Serial.print(va_arg(args, char*)); break; - case 'd': Serial.print(va_arg(args, int), DEC); break; - case 'b': Serial.print(va_arg(args, int), BIN); break; - case 'x': Serial.print(va_arg(args, int), HEX); break; - case 'f': Serial.print(va_arg(args, double), 2); break; - } + +const char *BLEstate[] = { + "SUCCESS_INDICATE", + "SUCCESS_NOTIFY", + "ERROR_INDICATE_DISABLED", + "ERROR_NOTIFY_DISABLED", + "ERROR_GATT", + "ERROR_NO_CLIENT", + "ERROR_INDICATE_TIMEOUT", + "ERROR_INDICATE_FAILURE" +}; + +void print_hex(const uint8_t *value, uint8_t len, const char *prefix = "0x", const char *postfix = " ") { + for (int i = 0; i < len; i++) { + Serial.printf("%s%02X%s", prefix, value[i], postfix); } Serial.println(); - va_end(args); } -const char* decode_cmd(uint16_t cmd) { - switch(cmd) { - case 0x01: - return "set position"; - case 0xBA: - return "activate scene"; +uint8_t set_response(message *response, const message *request, const byte *data = NULL, const uint8_t data_len = 1) { + const uint8_t message_len = min(data_len, (uint8_t)DAT_LEN) + sizeof(struct message) - DAT_LEN; + response->serviceID = request->serviceID & 0xEF; + response->cmdID = request->cmdID; + response->sequence = request->sequence; + response->data_len = min(data_len, (uint8_t)DAT_LEN); + if (data) { + memcpy(response->data, data, std::min(data_len, (uint8_t)DAT_LEN)); + } else { + *response->data = 0x0; + } + Serial.printf("\tret value (%i): ", message_len); + print_hex((const uint8_t *)response, message_len); + if (memcmp(home_key, zero_key, sizeof(zero_key))) { + message unencrypted; + memcpy(&unencrypted, response, message_len); + // AES counter is reset every message, so we need to init it each time + if (wc_AesInit(&aes_coder, hint, devId) || wc_AesSetKey(&aes_coder, (const byte *)home_key, 16, zero_key, AES_ENCRYPTION)) { + Serial.println("FATAL: setting AES init failed!"); + return 0; + } + if (wc_AesCtrEncrypt(&aes_coder, (byte *)response, (const byte *)&unencrypted, message_len)) { + Serial.println(F("FATAL: encryption failed!")); + return 0; + } + Serial.printf("\tencrypted (%i): ", message_len); + print_hex((const uint8_t *)response, message_len); + } + return message_len; +} + +void decode(BLECharacteristic *pChar) { + message response; + byte data_dec[DAT_LEN]; + const uint16_t data_len = pChar->getLength(); + const byte *data_raw = pChar->getData(); + struct message msg; + uint8_t resp_size = 0; + + Serial.print("\t BLE data: "); + print_hex(data_raw, data_len); + + if (data_len < 4) return; + + if (memcmp(home_key, zero_key, sizeof(zero_key))) { + if (wc_AesInit(&aes_coder, hint, devId) || wc_AesSetKey(&aes_coder, (const byte *)home_key, 16, zero_key, AES_ENCRYPTION)) { + Serial.println("FATAL: setting AES init failed!"); + } + if (wc_AesCtrEncrypt(&aes_coder, data_dec, data_raw, data_len)) { + Serial.println(F("FATAL: decryption failed!")); + return; + } + Serial.print("\tdecrypted: "); + print_hex(data_dec, data_len); + } else { + memcpy(data_dec, data_raw, data_len); + } + + 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!) + 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, 0x87, 0x01, 0x00, 0x00, 0x5f, 0x9c, 0x02, 0x00, 0x5f, 0x9c, 0x02, 0x00, 0x2a, 0xe0, 0x08 }; // HW diagnostics + const byte ret_valFFDE[] = { 0x08, 0x00, 0x02, 0x26, 0x72, 0x01, 0x59, 0x01, 0x00 }; // power status + const byte ret_valFA5B[] = { 0x00, 0x0a, 0xa2, 0x88, 0x13, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // get scene + const byte ret_valFA5A[] = { 0x00, 0x02, 0xb0 }; // set scene + + Serial.print("\t\t"); + switch ((msg.serviceID << 8) | msg.cmdID) { + case 0xF1DD: + Serial.println("get product info."); + resp_size = set_response(&response, (const message *)data_dec, ret_valF1DD, sizeof(ret_valF1DD)); + break; + case 0xF701: + // set position + struct position pos; + memcpy((void *)&pos, &data_dec[4], msg.data_len); + Serial.printf("set position: pos1 %f%%, pos2 %d, pos3 %d, tilt %d, velocity %d\n", pos.pos1 / 100.0, pos.pos2, pos.pos3, pos.tilt, pos.velocity); + break; + case 0xF711: + // identify + Serial.printf("identify: %i times\n", data_dec[4]); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xF7B8: + // stop movement + Serial.println("stop."); + break; + case 0xF7BA: + // activate scene + Serial.printf("activate scene #%i\n", data_dec[4]); + break; + case 0xFA5A: + // set scene + Serial.printf("set scene #%i\n", data_dec[4]); + resp_size = set_response(&response, (const message *)data_dec, ret_valFA5A, sizeof(ret_valFA5A)); + break; + case 0xFA5B: + // get scene + Serial.printf("get scene #%i\n", data_dec[4]); + resp_size = set_response(&response, (const message *)data_dec, ret_valFA5B, sizeof(ret_valFA5B)); + break; + case 0xFAEA: + // Reset Scene Automations + Serial.println("reset scene automations:"); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xFB02: + // set shade key + Serial.print("set shade key: "); + print_hex(&data_raw[4], data_len - 4, "\\x", ""); + // set resonse 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); + } + break; + // case 0xFF67: + // // get shade time + // break; + case 0xFF77: + // set shade time + Serial.printf("set time: %i-%i-%i %i:%i:%i\n", data_dec[4] | data_dec[5] << 8, data_dec[6], data_dec[7], data_dec[8], data_dec[9], data_dec[10]); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xFF87: + Serial.printf("set sunrise %i:%i:%i, sunset %i:%i:%i\n", data_dec[4], data_dec[5], data_dec[6], data_dec[7], data_dec[8], data_dec[9]); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xFFD7: + Serial.printf("set shade configuration: 0x%02X, status LED: %s\n", data_dec[4], data_dec[5] ? "on" : "off"); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xFFDD: + // get HW diagnostics + Serial.println("get HW diagnostics."); + resp_size = set_response(&response, (const message *)data_dec, ret_valFFDD, sizeof(ret_valFFDD)); + break; + case 0xFFDE: + // get power status + Serial.println("get power status."); + resp_size = set_response(&response, (const message *)&data_dec, ret_valFFDE, sizeof(ret_valFFDE)); + break; + case 0xFFDF: + // set power type + Serial.printf("set power type: %i\n", data_dec[4]); + resp_size = set_response(&response, (const message *)data_dec); + break; + case 0xFFEE: + Serial.println("factory reset."); + resp_size = set_response(&response, (const message *)data_dec); + break; default: - return "ERR"; + Serial.println(F("*********************************** unknown message")); + } + if (resp_size) { + pChar->setValue((uint8_t *)&response, resp_size); + pChar->notify(); + } +} + +class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer *pServer) { + digitalWrite(LED_BUILTIN, HIGH); + Serial.printf("connect ID: %i\n", pServer->getConnId()); + deviceConnected = true; + BLEDevice::startAdvertising(); + }; + + void onDisconnect(BLEServer *pServer) { + digitalWrite(LED_BUILTIN, LOW); + Serial.printf("disconnect ID: %i\n\n", pServer->getConnId()); + deviceConnected = false; } -} - -void print_hex(uint8_t *value, uint8_t len) { - for (int i = 0; i < len; i++) { - Serial.print("0x"); - Serial.print(value[i], HEX); - Serial.print(" "); - } - Serial.println(""); -} - -class MyServerCallbacks: public BLEServerCallbacks { - void onConnect(BLEServer* pServer) { - Serial.print("connect ID: "); - Serial.println(pServer->getConnId()); - /*pServer->updatePeerMTU(pServer->getConnId(), 310); - Serial.print("MTU: "); - Serial.println(pServer->getPeerMTU(pServer->getConnId()));*/ - deviceConnected = true; - BLEDevice::startAdvertising(); - }; - - void onDisconnect(BLEServer* pServer) { - Serial.println("disconnect."); - deviceConnected = false; - } - - void onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) { - Serialprintln("MTU changed: %d", pServer->getPeerMTU(pServer->getConnId())); - } + void onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) { + Serial.printf("MTU changed: %d\n", pServer->getPeerMTU(pServer->getConnId())); + } }; -class coverCallbacks: public BLECharacteristicCallbacks { - void onWrite(BLECharacteristic *pCharacteristic) { +class coverCallbacks : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { + Serial.printf("Cover write %s\n", pCharacteristic->toString().c_str()); + decode(pCharacteristic); + } + + void onRead(BLECharacteristic *pCharacteristic) { + Serial.printf("Cover read: %s\n", pCharacteristic->toString().c_str()); + } + + void onNotify(BLECharacteristic *pCharacteristic) { + Serial.printf("Cover onNotify() %s\n", pCharacteristic->toString().c_str()); + } + + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code) { + Serial.printf("Cover onStatus() %s: %s\n", BLEstate[s], pCharacteristic->toString().c_str()); + } +}; + +class batteryCallbacks : public BLECharacteristicCallbacks { + + void onWrite(BLECharacteristic *pCharacteristic) { uint8_t *value = pCharacteristic->getData(); - Serialprintln("Cover write %s:", pCharacteristic->toString().c_str()); + Serial.printf("Battery write: %s:", pCharacteristic->toString().c_str()); print_hex(value, pCharacteristic->getLength()); - - struct header data; - memcpy((void *) &data, value, 4); - Serialprintln("SRV: %x, CMD %x, SEQ %x, LEN %x", data.serviceID, data.cmdID, data.sequence, data.data_len); - - switch ((data.serviceID << 8) | data.cmdID) { - case 0xF701: - // set position - struct position pos; - memcpy((void *) &pos, &value[4], data.data_len); - Serialprintln("\tset position\tpos1 %f%%, pos2 %d, pos3 %d, tilt %d, velocity %d", pos.pos1/100.0, pos.pos2, pos.pos3, pos.tilt, pos.velocity); - break; - case 0xF7B8: - // stop movement - Serial.println("\tstop"); - break; - case 0xF7BA: - // activate scene - Serialprintln("\tactivate scene\tscene #%d", (uint16_t) value[4]); - break; - case 0xFA5B: - // get scene - Serialprintln("\tget scene\tscene #%d", (uint16_t) value[4]); - break; - case 0xFAEA: - // Reset Scene Automations - // FIXME! wrong return value! - Serialprintln("\treset scene automations\t"); - uint8_t ret[]={0xFA, 0xEA, data.sequence, 0x1, 0x0}; - Serial.print("ret: "); - print_hex(ret, 5); - pCharacteristic->setValue(ret, 5); - pCharacteristic->indicate(); - break; - } Serial.println(); } void onRead(BLECharacteristic *pCharacteristic) { - Serialprintln("Cover read: %s", pCharacteristic->toString().c_str()); - Serial.println(); - } -}; + Serial.printf("Battery read: %s\n", pCharacteristic->toString().c_str()); + } -class batteryCallbacks: public BLECharacteristicCallbacks { - - void onWrite(BLECharacteristic *pCharacteristic) { - uint8_t *value = pCharacteristic->getData(); - - Serialprintln("Battery write: %s:", pCharacteristic->toString().c_str()); - print_hex(value, pCharacteristic->getLength()); - Serial.println(); - } - - void onRead(BLECharacteristic *pCharacteristic) { - Serialprintln("Battery read: %s", pCharacteristic->toString().c_str()); - Serial.println(); + void onNotify(BLECharacteristic *pCharacteristic) { + Serial.println("Battery onNotify()"); + } + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code) { + Serial.println("Battery onStatus()"); } }; -class genericCallbacks: public BLECharacteristicCallbacks { - - void onWrite(BLECharacteristic *pCharacteristic) { - uint8_t *value = pCharacteristic->getData(); +class genericCallbacks : public BLECharacteristicCallbacks { - Serialprintln("generic write %s:", pCharacteristic->toString().c_str()); - print_hex(value, pCharacteristic->getLength()); - Serial.println(); - } + void onWrite(BLECharacteristic *pCharacteristic) { + //uint8_t *value = pCharacteristic->getData(); + + Serial.printf("generic write %s:\n", pCharacteristic->toString().c_str()); + //print_hex(value, pCharacteristic->getLength()); + } void onRead(BLECharacteristic *pCharacteristic) { - Serialprintln("generic read %s.", pCharacteristic->toString().c_str()); - Serial.println(); + Serial.printf("generic read %s.\n", pCharacteristic->toString().c_str()); } + void onNotify(BLECharacteristic *pCharacteristic) { + Serial.printf("generic onNotify() %s\n", pCharacteristic->toString().c_str()); + } // not used + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code) { + Serial.printf("generic onStatus() %s - %s\n", BLEstate[s], pCharacteristic->toString().c_str()); + }; // not used }; void setup() { @@ -197,8 +344,6 @@ void setup() { Serial.println(NAME " initializing ..."); BLEDevice::init(NAME); - Serialprintln("MTU: %d", BLEDevice::getMTU()); - // Create the BLE Server pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); @@ -207,68 +352,71 @@ void setup() { BLEService *pCovService = pServer->createService(COVER_SERVICE_UUID); // Create a BLE Characteristic pCharacteristic_cover = pCovService->createCharacteristic( - COVER_CHAR_UUID, - BLECharacteristic::PROPERTY_NOTIFY | - BLECharacteristic::PROPERTY_WRITE | - BLECharacteristic::PROPERTY_WRITE_NR - ); + COVER_CHAR_UUID, + BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); pCharacteristic_cover->setCallbacks(new coverCallbacks()); // Create a BLE Descriptor - BLEDescriptor *pDesc1 = new BLEDescriptor("2901", 10); - pDesc1->setValue("cover"); + /* BLEDescriptor *pDesc1 = new BLEDescriptor("2901", 10); +pDesc1->setValue("cover");*/ + //pCharacteristic_cover->addDescriptor(pDesc1); pCharacteristic_cover->addDescriptor(new BLE2902()); - pCharacteristic_cover->addDescriptor(pDesc1); - - pCharacteristic_unknown = pCovService->createCharacteristic( - XXX_CHAR_UUID, - BLECharacteristic::PROPERTY_INDICATE | - BLECharacteristic::PROPERTY_WRITE | - BLECharacteristic::PROPERTY_WRITE_NR - ); - pCharacteristic_unknown->setCallbacks(new genericCallbacks()); + pCharacteristic_unknown = pCovService->createCharacteristic( + XXX_CHAR_UUID, + BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); + pCharacteristic_unknown->setCallbacks(new genericCallbacks()); + pCharacteristic_unknown->addDescriptor(new BLE2902()); + BLEService *pBatService = pServer->createService(BAT_SERVICE_UUID); - pCharacteristic_bat = pBatService->createCharacteristic( - BAT_CHAR_UUID, - BLECharacteristic::PROPERTY_READ - ); + BAT_CHAR_UUID, + BLECharacteristic::PROPERTY_READ); pCharacteristic_bat->setCallbacks(new batteryCallbacks()); - pBatService->addCharacteristic(pCharacteristic_bat); uint8_t battery_level = 42; pCharacteristic_bat->setValue(&battery_level, 1); pCharacteristic_bat->addDescriptor(new BLE2902()); - // BLEService *pFWService = pServer->createService(FW_SERVICE_UUID); - // pCharacteristic_fw = pCovService->createCharacteristic( - // CHAR_FW_UUID, - // BLECharacteristic::PROPERTY_READ | - // BLECharacteristic::PROPERTY_WRITE | - // BLECharacteristic::PROPERTY_WRITE_NR - // ); - // pCharacteristic_fw->setCallbacks(new genericCallbacks()); - // pCharacteristic_fw->addDescriptor(new BLE2902()); - // pCharacteristic_fw->addDescriptor(pDesc2); - //BLEDescriptor *pDesc2 = new BLEDescriptor("2901", 10); - //pDesc2->setValue("firmware"); + BLEService *pFWService = pServer->createService(FW_SERVICE_UUID); + pCharacteristic_fw = pFWService->createCharacteristic( + FW_CHAR_UUID, + BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR); + pCharacteristic_fw->setCallbacks(new genericCallbacks()); + pCharacteristic_fw->addDescriptor(new BLE2902()); + BLEDescriptor *pDesc2 = new BLEDescriptor("2901", 10); + pDesc2->setValue("firmware"); + pCharacteristic_fw->addDescriptor(pDesc2); - // Start the service + + BLEService *pDEVService = pServer->createService(DEV_SERVICE_UUID); + pCharacteristic_dev = pDEVService->createCharacteristic( + SWC_CHAR_UUID, + BLECharacteristic::PROPERTY_READ); + pCharacteristic_dev->setValue(FW_VERSION); + pCharacteristic_dev->setCallbacks(new genericCallbacks()); + pCharacteristic_ser = pDEVService->createCharacteristic( + SER_CHAR_UUID, + BLECharacteristic::PROPERTY_READ); + pCharacteristic_ser->setValue(SERIAL_NR); + pCharacteristic_ser->setCallbacks(new genericCallbacks()); + + // Start the services pCovService->start(); - //pFWService->start(); + pFWService->start(); + pBatService->start(); + pDEVService->start(); // Start advertising BLEAdvertisementData AdvertisementData; - const String manufacturerData = String("\x19\x08\x00\x00\x2A\x00\x00\x00\x00\x00\xA2",11); - // Hunter Douglas ^^--^^ ^^ ID-Type + const String manufacturerData = String("\x19\x08\x00\x00\x2A\x00\x00\x00\x00\x00\xA2", 11); + // Hunter Douglas ^^--^^ ^^key^^ ^^ ID-Type AdvertisementData.setManufacturerData(manufacturerData); AdvertisementData.setPartialServices(BLEUUID(COVER_SERVICE_UUID)); - AdvertisementData.setFlags((1 << 2) | (1 << 1)); // [BR/EDR Not Supported] | [LE General Discoverable Mode] + AdvertisementData.setFlags((1 << 2) | (1 << 1)); // [BR/EDR Not Supported] | [LE General Discoverable Mode] BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->setAdvertisementData(AdvertisementData); - BLEDevice::startAdvertising(); Serial.println("Device " NAME " ready."); @@ -287,5 +435,5 @@ void loop() { if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting oldDeviceConnected = deviceConnected; - } + } } diff --git a/emu/PV_BLE_cover/user_settings.h b/emu/PV_BLE_cover/user_settings.h new file mode 100644 index 0000000..b170315 --- /dev/null +++ b/emu/PV_BLE_cover/user_settings.h @@ -0,0 +1,540 @@ +/* user_settings_template.h + * + * Copyright (C) 2006-2024 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Example wolfSSL user settings with #if 0/1 gates to enable/disable algorithms and features. + * This file is included with wolfssl/wolfcrypt/settings.h when WOLFSSL_USER_SETTINGS is defined. + * Based on IDE/GCC-ARM/Headers/user_settings.h + */ + +#ifndef WOLFSSL_USER_SETTINGS_H +#define WOLFSSL_USER_SETTINGS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* If TARGET_EMBEDDED is defined then small target settings are used */ +#if !(defined(__MACH__) || defined(__FreeBSD__) || defined(__linux__) || defined(_WIN32)) + #define TARGET_EMBEDDED +#endif + +/* ------------------------------------------------------------------------- */ +/* Platform */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_GENERAL_ALIGNMENT 4 +#define SIZEOF_LONG_LONG 8 +#if 0 + #define NO_64BIT /* disable use of 64-bit variables */ +#endif + +#ifdef TARGET_EMBEDDED + /* disable mutex locking */ + #define SINGLE_THREADED + + /* reduce stack use. For variables over 100 bytes allocate from heap */ + #define WOLFSSL_SMALL_STACK + + /* Disable the built-in socket support and use the IO callbacks. + * Set IO callbacks with wolfSSL_CTX_SetIORecv/wolfSSL_CTX_SetIOSend + */ + #define WOLFSSL_USER_IO +#endif + +/* ------------------------------------------------------------------------- */ +/* Math Configuration */ +/* ------------------------------------------------------------------------- */ +/* Wolf Single Precision Math */ +#if 1 + #define WOLFSSL_HAVE_SP_RSA + #define WOLFSSL_HAVE_SP_DH + #define WOLFSSL_HAVE_SP_ECC + //#define WOLFSSL_SP_4096 /* Enable RSA/RH 4096-bit support */ + //#define WOLFSSL_SP_384 /* Enable ECC 384-bit SECP384R1 support */ + + //#define WOLFSSL_SP_MATH /* only SP math - disables integer.c/tfm.c */ + #define WOLFSSL_SP_MATH_ALL /* use SP math for all key sizes and curves */ + + //#define WOLFSSL_SP_NO_MALLOC + //#define WOLFSSL_SP_DIV_32 /* do not use 64-bit divides */ + + #ifdef TARGET_EMBEDDED + /* use smaller version of code */ + #define WOLFSSL_SP_SMALL + #else + /* SP Assembly Speedups - specific to chip type */ + #define WOLFSSL_SP_ASM + #endif + //#define WOLFSSL_SP_X86_64 + //#define WOLFSSL_SP_X86 + //#define WOLFSSL_SP_ARM32_ASM + //#define WOLFSSL_SP_ARM64_ASM + //#define WOLFSSL_SP_ARM_THUMB_ASM + //#define WOLFSSL_SP_ARM_CORTEX_M_ASM +#elif 1 + /* Fast Math (tfm.c) (stack based and timing resistant) */ + #define USE_FAST_MATH + #define TFM_TIMING_RESISTANT +#else + /* Normal (integer.c) (heap based, not timing resistant) - not recommended*/ + #define USE_INTEGER_HEAP_MATH +#endif + + +/* ------------------------------------------------------------------------- */ +/* Crypto */ +/* ------------------------------------------------------------------------- */ +/* RSA */ +#undef NO_RSA +#if 0 + #ifdef USE_FAST_MATH + /* Maximum math bits (Max RSA key bits * 2) */ + #define FP_MAX_BITS 4096 + #endif + + /* half as much memory but twice as slow */ + //#define RSA_LOW_MEM + + /* Enables blinding mode, to prevent timing attacks */ + #define WC_RSA_BLINDING + + /* RSA PSS Support */ + #define WC_RSA_PSS +#else + #define NO_RSA +#endif + +/* DH */ +#undef NO_DH +#if 0 + /* Use table for DH instead of -lm (math) lib dependency */ + #if 1 + #define WOLFSSL_DH_CONST + #define HAVE_FFDHE_2048 + //#define HAVE_FFDHE_4096 + //#define HAVE_FFDHE_6144 + //#define HAVE_FFDHE_8192 + #endif +#else + #define NO_DH +#endif + +/* ECC */ +#undef HAVE_ECC +#if 0 + #define HAVE_ECC + + /* Manually define enabled curves */ + #define ECC_USER_CURVES + + #ifdef ECC_USER_CURVES + /* Manual Curve Selection */ + //#define HAVE_ECC192 + //#define HAVE_ECC224 + #undef NO_ECC256 + //#define HAVE_ECC384 + //#define HAVE_ECC521 + #endif + + /* Fixed point cache (speeds repeated operations against same private key) */ + //#define FP_ECC + #ifdef FP_ECC + /* Bits / Entries */ + #define FP_ENTRIES 2 + #define FP_LUT 4 + #endif + + /* Optional ECC calculation method */ + /* Note: doubles heap usage, but slightly faster */ + #define ECC_SHAMIR + + /* Reduces heap usage, but slower */ + #define ECC_TIMING_RESISTANT + + /* Compressed ECC Key Support */ + //#define HAVE_COMP_KEY + + /* Use alternate ECC size for ECC math */ + #ifdef USE_FAST_MATH + /* MAX ECC BITS = ROUND8(MAX ECC) * 2 */ + #if defined(NO_RSA) && defined(NO_DH) + /* Custom fastmath size if not using RSA/DH */ + #define FP_MAX_BITS (256 * 2) + #else + /* use heap allocation for ECC points */ + #define ALT_ECC_SIZE + + /* wolfSSL will compute the FP_MAX_BITS_ECC, but it can be overridden */ + //#define FP_MAX_BITS_ECC (256 * 2) + #endif + + /* Speedups specific to curve */ + #ifndef NO_ECC256 + #define TFM_ECC256 + #endif + #endif +#endif + + +/* AES */ +#undef NO_AES +#if 1 + #define HAVE_AES_CBC + + /* GCM Method: GCM_TABLE_4BIT, GCM_SMALL, GCM_WORD32 or GCM_TABLE */ + #define HAVE_AESGCM + #ifdef TARGET_EMBEDDED + #define GCM_SMALL + #else + #define GCM_TABLE_4BIT + #endif + + #define WOLFSSL_AES_DIRECT + //#define HAVE_AES_ECB + #define WOLFSSL_AES_COUNTER + //#define HAVE_AESCCM +#else + #define NO_AES +#endif + + +/* DES3 */ +#undef NO_DES3 +#if 0 +#else + #define NO_DES3 +#endif + +/* ChaCha20 / Poly1305 */ +#undef HAVE_CHACHA +#undef HAVE_POLY1305 +#if 0 + #define HAVE_CHACHA + #define HAVE_POLY1305 + + /* Needed for Poly1305 */ + #define HAVE_ONE_TIME_AUTH +#endif + +/* Ed25519 / Curve25519 */ +#undef HAVE_CURVE25519 +#undef HAVE_ED25519 +#if 0 + #define HAVE_CURVE25519 + #define HAVE_ED25519 /* ED25519 Requires SHA512 */ + + /* Optionally use small math (less flash usage, but much slower) */ + #if 1 + #define CURVED25519_SMALL + #endif +#endif + + +/* ------------------------------------------------------------------------- */ +/* Hashing */ +/* ------------------------------------------------------------------------- */ +/* Sha */ +#undef NO_SHA +#if 1 + /* 1k smaller, but 25% slower */ + //#define USE_SLOW_SHA +#else + #define NO_SHA +#endif + +/* Sha256 */ +#undef NO_SHA256 +#if 1 + /* not unrolled - ~2k smaller and ~25% slower */ + //#define USE_SLOW_SHA256 + + /* Sha224 */ + #if 0 + #define WOLFSSL_SHA224 + #endif +#else + #define NO_SHA256 +#endif + +/* Sha512 */ +#undef WOLFSSL_SHA512 +#if 0 + #define WOLFSSL_SHA512 + + /* Sha384 */ + #undef WOLFSSL_SHA384 + #if 0 + #define WOLFSSL_SHA384 + #endif + + /* over twice as small, but 50% slower */ + //#define USE_SLOW_SHA512 +#endif + +/* Sha3 */ +#undef WOLFSSL_SHA3 +#if 0 + #define WOLFSSL_SHA3 +#endif + +/* MD5 */ +#undef NO_MD5 +#if 0 + +#else + #define NO_MD5 +#endif + +/* HKDF */ +#undef HAVE_HKDF +#if 0 + #define HAVE_HKDF +#endif + +/* CMAC */ +#undef WOLFSSL_CMAC +#if 0 + #define WOLFSSL_CMAC +#endif + + +/* ------------------------------------------------------------------------- */ +/* Benchmark / Test */ +/* ------------------------------------------------------------------------- */ +#ifdef TARGET_EMBEDDED + /* Use reduced benchmark / test sizes */ + #define BENCH_EMBEDDED +#endif + +/* Use test buffers from array (not filesystem) */ +#ifndef NO_FILESYSTEM +#define USE_CERT_BUFFERS_256 +#define USE_CERT_BUFFERS_2048 +#endif + +/* ------------------------------------------------------------------------- */ +/* Debugging */ +/* ------------------------------------------------------------------------- */ + +#undef DEBUG_WOLFSSL +#undef NO_ERROR_STRINGS +#if 0 + #define DEBUG_WOLFSSL +#else + #if 0 + #define NO_ERROR_STRINGS + #endif +#endif + + +/* ------------------------------------------------------------------------- */ +/* Memory */ +/* ------------------------------------------------------------------------- */ + +/* Override Memory API's */ +#if 0 + #define XMALLOC_OVERRIDE + + /* prototypes for user heap override functions */ + /* Note: Realloc only required for normal math */ + /* Note2: XFREE(NULL) must be properly handled */ + #include /* for size_t */ + extern void *myMalloc(size_t n, void* heap, int type); + extern void myFree(void *p, void* heap, int type); + extern void *myRealloc(void *p, size_t n, void* heap, int type); + + #define XMALLOC(n, h, t) myMalloc(n, h, t) + #define XFREE(p, h, t) myFree(p, h, t) + #define XREALLOC(p, n, h, t) myRealloc(p, n, h, t) +#endif + +#if 0 + /* Static memory requires fast math */ + #define WOLFSSL_STATIC_MEMORY + + /* Disable fallback malloc/free */ + #define WOLFSSL_NO_MALLOC + #if 1 + #define WOLFSSL_MALLOC_CHECK /* trap malloc failure */ + #endif +#endif + +/* Memory callbacks */ +#if 0 + #undef USE_WOLFSSL_MEMORY + #define USE_WOLFSSL_MEMORY + + /* Use this to measure / print heap usage */ + #if 0 + #define WOLFSSL_TRACK_MEMORY + #define WOLFSSL_DEBUG_MEMORY + #endif +#else + #ifndef WOLFSSL_STATIC_MEMORY + #define NO_WOLFSSL_MEMORY + /* Otherwise we will use stdlib malloc, free and realloc */ + #endif +#endif + + +/* ------------------------------------------------------------------------- */ +/* Port */ +/* ------------------------------------------------------------------------- */ + +/* Override Current Time */ +#if 0 + /* Allows custom "custom_time()" function to be used for benchmark */ + #define WOLFSSL_USER_CURRTIME + #define WOLFSSL_GMTIME + #define USER_TICKS + extern unsigned long my_time(unsigned long* timer); + #define XTIME my_time +#endif + + +/* ------------------------------------------------------------------------- */ +/* RNG */ +/* ------------------------------------------------------------------------- */ + +/* Choose RNG method */ +#if 0 + /* Custom Seed Source */ + #if 0 + /* Size of returned HW RNG value */ + #define CUSTOM_RAND_TYPE unsigned int + extern unsigned int my_rng_seed_gen(void); + #undef CUSTOM_RAND_GENERATE + #define CUSTOM_RAND_GENERATE my_rng_seed_gen + #endif + + /* Use built-in P-RNG (SHA256 based) with HW RNG */ + /* P-RNG + HW RNG (P-RNG is ~8K) */ + #undef HAVE_HASHDRBG + #define HAVE_HASHDRBG +#else + #undef WC_NO_HASHDRBG + #define WC_NO_HASHDRBG + + /* Bypass P-RNG and use only HW RNG */ + extern int my_rng_gen_block(unsigned char* output, unsigned int sz); + #undef CUSTOM_RAND_GENERATE_BLOCK + #define CUSTOM_RAND_GENERATE_BLOCK my_rng_gen_block +#endif + + +/* ------------------------------------------------------------------------- */ +/* Custom Standard Lib */ +/* ------------------------------------------------------------------------- */ +/* Allows override of all standard library functions */ +#undef STRING_USER +#if 0 + #define STRING_USER + + #include + + #define USE_WOLF_STRSEP + #define XSTRSEP(s1,d) wc_strsep((s1),(d)) + + #define USE_WOLF_STRTOK + #define XSTRTOK(s1,d,ptr) wc_strtok((s1),(d),(ptr)) + + #define XSTRNSTR(s1,s2,n) mystrnstr((s1),(s2),(n)) + + #define XMEMCPY(d,s,l) memcpy((d),(s),(l)) + #define XMEMSET(b,c,l) memset((b),(c),(l)) + #define XMEMCMP(s1,s2,n) memcmp((s1),(s2),(n)) + #define XMEMMOVE(d,s,l) memmove((d),(s),(l)) + + #define XSTRLEN(s1) strlen((s1)) + #define XSTRNCPY(s1,s2,n) strncpy((s1),(s2),(n)) + #define XSTRSTR(s1,s2) strstr((s1),(s2)) + + #define XSTRNCMP(s1,s2,n) strncmp((s1),(s2),(n)) + #define XSTRNCAT(s1,s2,n) strncat((s1),(s2),(n)) + #define XSTRNCASECMP(s1,s2,n) strncasecmp((s1),(s2),(n)) + + #define XSNPRINTF snprintf +#endif + + + +/* ------------------------------------------------------------------------- */ +/* Enable Features */ +/* ------------------------------------------------------------------------- */ + +#define WOLFSSL_TLS13 +#define WOLFSSL_OLD_PRIME_CHECK /* Use faster DH prime checking */ +#define HAVE_TLS_EXTENSIONS +#define HAVE_SUPPORTED_CURVES +#define WOLFSSL_BASE64_ENCODE + +//#define WOLFSSL_KEY_GEN /* For RSA Key gen only */ +//#define KEEP_PEER_CERT +//#define HAVE_COMP_KEY + +/* TLS Session Cache */ +#if 0 + #define SMALL_SESSION_CACHE +#else + #define NO_SESSION_CACHE +#endif + + +/* ------------------------------------------------------------------------- */ +/* Disable Features */ +/* ------------------------------------------------------------------------- */ +#define NO_WOLFSSL_SERVER +#define NO_WOLFSSL_CLIENT +//#define NO_CRYPT_TEST +//#define NO_CRYPT_BENCHMARK +#define WOLFCRYPT_ONLY + +/* do not warm when file is included to be built and not required to be */ +#define WOLFSSL_IGNORE_FILE_WARN + +/* In-lining of misc.c functions */ +/* If defined, must include wolfcrypt/src/misc.c in build */ +/* Slower, but about 1k smaller */ +//#define NO_INLINE + +#ifdef TARGET_EMBEDDED + #define NO_FILESYSTEM + #define NO_WRITEV + #define NO_MAIN_DRIVER + #define NO_DEV_RANDOM +#endif + +#define NO_OLD_TLS +#define NO_PSK + +#define NO_DSA +#define NO_RC4 +#define NO_MD4 +#define NO_PWDBASED +//#define NO_CODING +//#define NO_ASN_TIME +//#define NO_CERTS +//#define NO_SIG_WRAPPER + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFSSL_USER_SETTINGS_H */ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d10245b..59f0065 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,11 @@ # pyproject.toml -#[tool.setuptools.packages.find] -#where = ["custom_components/"] -#include = ["bms_ble"] +[project] +requires-python = ">=3.12.0" [tool.pytest.ini_options] minversion = "8.0" -addopts="--cov=custom_components.bms_ble --cov-report=term-missing --cov-fail-under=100" +addopts="--cov=custom_components.hunterdouglas_powerview_ble --cov-report=term-missing --cov-fail-under=100" pythonpath = [ "custom_components.hunterdouglas_powerview_ble", ] @@ -15,5 +14,152 @@ testpaths = [ ] asyncio_mode = "auto" +# ruff configuration taken from HA 2024.11.2 (less ignores) +[tool.ruff] +required-version = ">=0.6.8" + [tool.ruff.lint.per-file-ignores] "scripts/*" = ["T201"] + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC210", # Async functions should not call blocking HTTP methods + "ASYNC220", # Async functions should not create subprocesses with blocking methods + "ASYNC221", # Async functions should not run processes with blocking methods + "ASYNC222", # Async functions should not wait on processes with blocking methods + "ASYNC230", # Async functions should not open files with blocking methods like open + "ASYNC251", # Async functions should not call time.sleep + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "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} + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "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 + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TCH", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + +# "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}) +# "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 +# "RUF001", # String contains ambiguous unicode character. +# "RUF002", # Docstring contains ambiguous unicode character. +# "RUF003", # Comment contains ambiguous unicode character. +# "RUF015", # Prefer next(...) over single element slice +# "SIM102", # Use a single if statement instead of nested if statements +# "SIM103", # Return the condition {condition} directly +# "SIM108", # Use ternary operator {contents} instead of if-else-block +# "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 + + "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", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605" +] diff --git a/requirements.txt b/requirements.txt index 952be3c..bf8d48b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -homeassistant==2024.8.0 +homeassistant==2024.11.0 pip>=21.3.1 -ruff==0.4.2 +ruff==0.6.8 diff --git a/requirements_test.txt b/requirements_test.txt index ef103e2..3ca9df3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pip>=21.3.1 -homeassistant==2024.6.0 +-r requirements.txt + wheel home-assistant-bluetooth -habluetooth>=2.4.0 +habluetooth>=3.6.0 bluetooth-adapters -pytest>=8.0.2 -pytest-cov>=4.0.0 -pytest-socket>=0.5.0 -pytest-asyncio +pytest>=8.3.3 +pytest-cov>=5.0.0 +pytest-socket>=0.7.0 +pytest-asyncio>=0.24.0 sqlalchemy freezegun requests-mock @@ -16,10 +16,10 @@ aiohttp aiohttp_cors aiohttp-fast-url-dispatcher aiohttp-zlib-ng -bleak>=0.19.0 -bleak-retry-connector>=3.3.0 +bleak>=0.22.3 +bleak-retry-connector>=3.6.0 bluetooth-data-tools pyserial-asyncio pyudev -pytest-homeassistant-custom-component==0.13.132 +pytest-homeassistant-custom-component==0.13.181