Merge branch 'main' into main

This commit is contained in:
Patrick
2025-01-02 10:34:01 +01:00
committed by GitHub
20 changed files with 1274 additions and 235 deletions

19
.github/workflows/_validate.bak vendored Normal file
View File

@@ -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"

16
.github/workflows/hassfest.yaml vendored Normal file
View File

@@ -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"

28
.github/workflows/lint.yml vendored Normal file
View File

@@ -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 .

22
.github/workflows/stale.yaml vendored Normal file
View File

@@ -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

2
.gitignore vendored
View File

@@ -150,3 +150,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
aes.py
*.bak

View File

@@ -5,11 +5,10 @@
A Home Assistant integration to support Hunter Douglas Powerview devices via Bluetooth A Home Assistant integration to support Hunter Douglas Powerview devices via Bluetooth
## :warning: Limitations > [!WARNING]
- This integration is under development! > - This integration is under development!
- Test coverage is low, malfunction might occur. > - 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)
- Currently only position change is supported (e.g., no tilt)
## Features ## Features
- Zero configuration - Zero configuration
@@ -39,10 +38,14 @@ The integration provides the following information about the battery
Platform | Description | Unit | Details Platform | Description | Unit | Details
-- | -- | -- | -- -- | -- | -- | --
`binary_sensor` | battery charging indicator | `bool` | true if battery is charging `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) `cover` | view/control position | `%` | percentage cover is open (100% is open)
`sensor` | SoC (state of charge) | `%` | range 100% (full), 50%, 20%, 0% (battery empty) `sensor` | SoC (state of charge) | `%` | range 100% (full), 50%, 20%, 0% (battery empty)
## Installation ## 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 ### Automatic
Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom repository](https://hacs.xyz/docs/faq/custom_repositories/). 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. Restart Home Assistant
1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Hunter Douglas PowerView (BLE)" 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 ## Outlook
- Add support for encryption - Add support for encryption

View File

@@ -5,7 +5,6 @@
""" """
from bleak.exc import BleakError from bleak.exc import BleakError
from homeassistant.components.bluetooth import async_ble_device_from_address from homeassistant.components.bluetooth import async_ble_device_from_address
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
@@ -15,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .coordinator import PVCoordinator 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] type ConfigEntryType = ConfigEntry[PVCoordinator]

View File

@@ -1,28 +1,28 @@
"""Hunter Douglas PowerView BLE API.""" """Hunter Douglas PowerView BLE API."""
import asyncio import asyncio
import time
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
import time
from typing import Final from typing import Final
from bleak import BleakClient from bleak import BleakClient
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from bleak.exc import BleakError from bleak.exc import BleakError
from bleak.uuids import normalize_uuid_str 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 import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.base import CipherContext
from homeassistant.components.cover import ATTR_CURRENT_POSITION from homeassistant.components.cover import ATTR_CURRENT_POSITION
from .const import LOGGER, TIMEOUT from .const import LOGGER, TIMEOUT
UUID_COV_SERVICE: Final = normalize_uuid_str("fdc1") UUID_COV_SERVICE: Final[str] = normalize_uuid_str("fdc1")
UUID_TX: Final = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0" UUID_TX: Final[str] = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0"
UUID_DEV_SERVICE: Final = normalize_uuid_str("180a") UUID_DEV_SERVICE: Final[str] = normalize_uuid_str("180a")
UUID_BAT_SERVICE: Final = normalize_uuid_str("180f") UUID_BAT_SERVICE: Final[str] = normalize_uuid_str("180f")
ATTR_ACTIVITY: Final = "activity" ATTR_ACTIVITY: Final[str] = "activity"
SHADE_TYPE: Final[dict[int, str]] = { SHADE_TYPE: Final[dict[int, str]] = {
@@ -59,6 +59,7 @@ class ShadeCmd(Enum):
SET_POSITION = 0x01F7 SET_POSITION = 0x01F7
STOP = 0xB8F7 STOP = 0xB8F7
ACTIVATE_SCENE = 0xBAF7 ACTIVATE_SCENE = 0xBAF7
IDENTIFY = 0x11F7
@dataclass @dataclass
@@ -82,12 +83,21 @@ class PowerViewBLE:
self._ble_device: Final[BLEDevice] = ble_device self._ble_device: Final[BLEDevice] = ble_device
self.name: Final[str] = self._ble_device.name or "unknown" self.name: Final[str] = self._ble_device.name or "unknown"
self._seqcnt: int = 1 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_event = asyncio.Event()
self._data: bytearray self._data: bytearray
self._info: PVDeviceInfo = PVDeviceInfo() self._info: PVDeviceInfo = PVDeviceInfo()
self._cmd_lock: Final = asyncio.Lock() self._cmd_lock: Final = asyncio.Lock()
self._cmd_next = None self._cmd_next = None
self._is_encrypted: bool = False
self._cipher: Final = ( self._cipher: Final = (
Cipher(algorithms.AES(home_key), modes.CTR(bytearray(16))) Cipher(algorithms.AES(home_key), modes.CTR(bytearray(16)))
if len(home_key) == 16 if len(home_key) == 16
@@ -98,6 +108,15 @@ class PowerViewBLE:
await self._data_event.wait() await self._data_event.wait()
self._data_event.clear() 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 @property
def info(self) -> PVDeviceInfo: def info(self) -> PVDeviceInfo:
"""Return device information, e.g. SW version.""" """Return device information, e.g. SW version."""
@@ -106,7 +125,7 @@ class PowerViewBLE:
@property @property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Return whether remote device is connected.""" """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 # general cmd: uint16_t cmd, uint8_t seqID, uint8_t data_len
async def _cmd( async def _cmd(
@@ -120,7 +139,6 @@ class PowerViewBLE:
async with self._cmd_lock: async with self._cmd_lock:
try: try:
await self._connect() await self._connect()
assert self._client is not None, "missing BT client"
cmd_run = self._cmd_next cmd_run = self._cmd_next
tx_data = ( tx_data = (
bytearray( bytearray(
@@ -129,11 +147,12 @@ class PowerViewBLE:
) )
+ cmd_run[1] + 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() enc = self._cipher.encryptor()
tx_data = enc.update(tx_data) + enc.finalize() tx_data = enc.update(tx_data) + enc.finalize()
LOGGER.debug(" encrypted: %s", tx_data.hex(" "))
self._data_event.clear() self._data_event.clear()
LOGGER.debug("sending cmd: %s", tx_data)
await self._client.write_gatt_char(UUID_TX, tx_data, False) await self._client.write_gatt_char(UUID_TX, tx_data, False)
self._seqcnt += 1 self._seqcnt += 1
LOGGER.debug("waiting for response") LOGGER.debug("waiting for response")
@@ -156,14 +175,14 @@ class PowerViewBLE:
LOGGER.debug("not a V2 record!") LOGGER.debug("not a V2 record!")
return [] return []
pos = int.from_bytes(data[3:5], byteorder="little") 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 [ return [
(ATTR_CURRENT_POSITION, ((pos >> 2) / 10)), (ATTR_CURRENT_POSITION, ((pos >> 2) / 10)),
("position2", pos2 >> 2), ("position2", pos2 >> 2),
("position3", int(data[6])), ("position3", int(data[6])),
("tilt", int(data[7])), ("tilt", int(data[7])),
("home_id", int.from_bytes(data[0:2], byteorder="little")), ("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_opening", bool(pos & 0x3 == 0x2)),
("is_closing", bool(pos & 0x3 == 0x1)), ("is_closing", bool(pos & 0x3 == 0x1)),
("battery_charging", bool(pos & 0x3 == 0x3)), # observed ("battery_charging", bool(pos & 0x3 == 0x3)), # observed
@@ -195,7 +214,7 @@ class PowerViewBLE:
async def stop(self) -> None: async def stop(self) -> None:
"""Stop device movement.""" """Stop device movement."""
LOGGER.debug("%s stop", self.name) LOGGER.debug("%s stop", self.name)
await self._cmd((ShadeCmd.STOP, bytearray(b""))) await self._cmd((ShadeCmd.STOP, bytearray()))
async def close(self) -> None: async def close(self) -> None:
"""Fully close cover.""" """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.""" """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: if len(data) < 4:
LOGGER.error("Reponse message too short") LOGGER.error("Reponse message too short")
return False return False
@@ -229,14 +249,14 @@ class PowerViewBLE:
return False return False
if int(data[2]) != seq_nr: if int(data[2]) != seq_nr:
LOGGER.warning( 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 return False
if int(data[3]) != 1: if int(data[3]) != 1:
LOGGER.error("Wrong response data length") LOGGER.error("Wrong response data length")
return False return False
if int(data[4] != 0): 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 False
return True return True
@@ -255,7 +275,6 @@ class PowerViewBLE:
async with self._cmd_lock: async with self._cmd_lock:
try: try:
await self._connect() await self._connect()
assert self._client is not None
for key, uuid in uuids.items(): for key, uuid in uuids.items():
LOGGER.debug("querying %s(%s)", key, uuid) LOGGER.debug("querying %s(%s)", key, uuid)
@@ -265,7 +284,7 @@ class PowerViewBLE:
.decode("UTF-8") .decode("UTF-8")
) )
finally: finally:
await self._disconnect() await self.disconnect()
LOGGER.debug("%s device data: %s", self.name, data) LOGGER.debug("%s device data: %s", self.name, data)
return data.copy() return data.copy()
@@ -275,8 +294,13 @@ class PowerViewBLE:
LOGGER.debug("Disconnected from %s", client.address) LOGGER.debug("Disconnected from %s", client.address)
def _notification_handler(self, _sender, data: bytearray) -> None: 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 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() self._data_event.set()
async def _connect(self) -> None: async def _connect(self) -> None:
@@ -288,9 +312,7 @@ class PowerViewBLE:
LOGGER.debug("%s already connected", self.name) LOGGER.debug("%s already connected", self.name)
return return
start = time.time() start: float = time.time()
await close_stale_connections(self._ble_device)
self._client = await establish_connection( self._client = await establish_connection(
BleakClient, BleakClient,
self._ble_device, self._ble_device,
@@ -308,10 +330,10 @@ class PowerViewBLE:
# await self._query_dev_info() # await self._query_dev_info()
async def _disconnect(self) -> None: async def disconnect(self) -> None:
"""Disconnect the device and stop notifications.""" """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) LOGGER.debug("Disconnecting device %s", self.name)
try: try:
self._data_event.clear() self._data_event.clear()

View File

@@ -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()

View File

@@ -4,7 +4,6 @@ from dataclasses import dataclass
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak, BluetoothServiceInfoBleak,

View File

@@ -13,12 +13,15 @@ from typing import Final
# ) # )
DOMAIN: Final = "hunterdouglas_powerview_ble" DOMAIN: Final[str] = "hunterdouglas_powerview_ble"
LOGGER: Final = logging.getLogger(__package__) LOGGER: Final = logging.getLogger(__package__)
MFCT_ID: Final = 2073 MFCT_ID: Final[int] = 2073
TIMEOUT: Final = 5 TIMEOUT: Final[int] = 5
HOME_KEY: Final = b""
# 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) # attributes (do not change)
ATTR_RSSI = "rssi" ATTR_RSSI: Final[str] = "rssi"

View File

@@ -3,9 +3,8 @@
from typing import Any from typing import Any
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from homeassistant.components import bluetooth 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 ( from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothDataUpdateCoordinator,
) )
@@ -79,6 +78,12 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator):
"""Check if a device is present.""" """Check if a device is present."""
return bluetooth.async_address_present(self.hass, self._mac, connectable=True) 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 @callback
def _async_handle_bluetooth_event( def _async_handle_bluetooth_event(
self, self,
@@ -98,6 +103,7 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator):
bytearray(service_info.manufacturer_data.get(2073, b"")) bytearray(service_info.manufacturer_data.get(2073, b""))
) )
) )
self.api.encrypted = bool(self.data.get("home_id"))
LOGGER.debug("data sample %s", self.data) LOGGER.debug("data sample %s", self.data)
super()._async_handle_bluetooth_event(service_info, change) super()._async_handle_bluetooth_event(service_info, change)

View File

@@ -3,7 +3,6 @@
from typing import Any, Final from typing import Any, Final
from bleak.exc import BleakError from bleak.exc import BleakError
from homeassistant.components.bluetooth.passive_update_coordinator import ( from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity, PassiveBluetoothCoordinatorEntity,
) )
@@ -127,7 +126,10 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
self.async_write_ha_state() self.async_write_ha_state()
except BleakError as err: except BleakError as err:
LOGGER.error( 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: def _reset_target_position(self) -> None:
@@ -143,7 +145,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
await self._coord.api.open() await self._coord.api.open()
self.async_write_ha_state() self.async_write_ha_state()
except BleakError as err: 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() self._reset_target_position()
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
@@ -156,7 +158,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
await self._coord.api.close() await self._coord.api.close()
self.async_write_ha_state() self.async_write_ha_state()
except BleakError as err: 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() self._reset_target_position()
async def async_stop_cover(self, **kwargs: Any) -> None: async def async_stop_cover(self, **kwargs: Any) -> None:
@@ -167,4 +169,4 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
self._reset_target_position() self._reset_target_position()
self.async_write_ha_state() self.async_write_ha_state()
except BleakError as err: 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)

View File

@@ -17,5 +17,5 @@
"issue_tracker": "https://github.com/patman15/hdpv_ble/issues", "issue_tracker": "https://github.com/patman15/hdpv_ble/issues",
"loggers": ["hunterdouglas_powerview_ble"], "loggers": ["hunterdouglas_powerview_ble"],
"requirements": ["cryptography>=43.0.0"], "requirements": ["cryptography>=43.0.0"],
"version": 0.21 "version": "0.22"
} }

View File

@@ -4,9 +4,11 @@ from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity, PassiveBluetoothCoordinatorEntity,
) )
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
)
from homeassistant.components.sensor.const import (
SensorDeviceClass,
SensorStateClass, SensorStateClass,
) )
from homeassistant.const import ( from homeassistant.const import (

View File

@@ -1,40 +1,58 @@
/** /**
* Emulate a Hunter Douglas PowerView cover device using ESP32 * Emulate a Hunter Douglas PowerView cover device using ESP32
* used e.g. to gain the home_key from an existing installation via BLE
* *
* TODO: * TODO:
* - adding device to appartement does only work after long timeout, * - cleanup code
* as some feedback to "reset scene automations" is expected * - think about emulating a remote
* *
* AUTHOR: patman15 * AUTHOR: patman15
* LICENSE: GPLv2 * LICENSE: GPLv2
*/ */
#define NAME "myPVcover"
#define FW_VERSION "391"
#define SERIAL_NR "01234567890ABCDEF"
#include <BLEDevice.h> #include <BLEDevice.h>
#include <BLEServer.h> #include <BLEServer.h>
#include <BLEUtils.h> #include <BLEUtils.h>
#include <BLE2902.h> #include <BLE2902.h>
#define WOLFSSL_USER_SETTINGS
#include <wolfssl.h>
#include "wolfssl/wolfcrypt/aes.h"
Aes aes_coder;
void *hint = NULL;
int devId = INVALID_DEVID; //if not using async INVALID_DEVID is default
#include <stdarg.h> #include <stdarg.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#define NAME "myPVcover"
#define COVER_SERVICE_UUID "0000FDC1-0000-1000-8000-00805f9b34fb" #define COVER_SERVICE_UUID "0000FDC1-0000-1000-8000-00805f9b34fb"
#define COVER_CHAR_UUID "CAFE1001-C0FF-EE01-8000-A110CA7AB1E0" #define COVER_CHAR_UUID "CAFE1001-C0FF-EE01-8000-A110CA7AB1E0"
#define XXX_CHAR_UUID "CAFE1002-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_SERVICE_UUID BLEUUID("180F")
#define BAT_CHAR_UUID BLEUUID("2A19") #define BAT_CHAR_UUID BLEUUID("2A19")
#define DAT_LEN 255
#pragma pack(1) #pragma pack(1)
struct header { struct message {
uint8_t serviceID; uint8_t serviceID;
uint8_t cmdID; uint8_t cmdID;
uint8_t sequence; uint8_t sequence;
uint8_t data_len; uint8_t data_len;
uint8_t data[DAT_LEN];
}; };
struct position { struct position {
@@ -45,151 +63,280 @@ struct position {
uint8_t velocity; uint8_t velocity;
}; };
struct notification {
uint8_t *data;
BLECharacteristic *characteristic;
};
BLECharacteristic *pCharacteristic_cover, *pCharacteristic_fw, *pCharacteristic_unknown, *pCharacteristic_bat; BLECharacteristic *pCharacteristic_cover, *pCharacteristic_fw, *pCharacteristic_unknown, *pCharacteristic_bat;
BLECharacteristic *pCharacteristic_dev, *pCharacteristic_ser;
BLEServer *pServer = NULL; BLEServer *pServer = NULL;
bool deviceConnected = false; bool deviceConnected = false;
bool oldDeviceConnected = 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; const char *BLEstate[] = {
va_start(args, input); "SUCCESS_INDICATE",
for(const char* i=input; *i!=0; ++i) { "SUCCESS_NOTIFY",
if(*i!='%') { Serial.print(*i); continue; } "ERROR_INDICATE_DISABLED",
switch(*(++i)) { "ERROR_NOTIFY_DISABLED",
case '%': Serial.print('%'); break; "ERROR_GATT",
case 's': Serial.print(va_arg(args, char*)); break; "ERROR_NO_CLIENT",
case 'd': Serial.print(va_arg(args, int), DEC); break; "ERROR_INDICATE_TIMEOUT",
case 'b': Serial.print(va_arg(args, int), BIN); break; "ERROR_INDICATE_FAILURE"
case 'x': Serial.print(va_arg(args, int), HEX); break; };
case 'f': Serial.print(va_arg(args, double), 2); break;
} 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(); Serial.println();
va_end(args);
} }
const char* decode_cmd(uint16_t cmd) { uint8_t set_response(message *response, const message *request, const byte *data = NULL, const uint8_t data_len = 1) {
switch(cmd) { const uint8_t message_len = min(data_len, (uint8_t)DAT_LEN) + sizeof(struct message) - DAT_LEN;
case 0x01: response->serviceID = request->serviceID & 0xEF;
return "set position"; response->cmdID = request->cmdID;
case 0xBA: response->sequence = request->sequence;
return "activate scene"; 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: 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 onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) {
Serial.printf("MTU changed: %d\n", pServer->getPeerMTU(pServer->getConnId()));
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()));
}
}; };
class coverCallbacks: public BLECharacteristicCallbacks { class coverCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) { void onWrite(BLECharacteristic *pCharacteristic) {
uint8_t *value = pCharacteristic->getData(); Serial.printf("Cover write %s\n", pCharacteristic->toString().c_str());
decode(pCharacteristic);
Serialprintln("Cover 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) { void onRead(BLECharacteristic *pCharacteristic) {
Serialprintln("Cover read: %s", pCharacteristic->toString().c_str()); Serial.printf("Cover read: %s\n", pCharacteristic->toString().c_str());
Serial.println(); }
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 { class batteryCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) { void onWrite(BLECharacteristic *pCharacteristic) {
uint8_t *value = pCharacteristic->getData(); uint8_t *value = pCharacteristic->getData();
Serialprintln("Battery write: %s:", pCharacteristic->toString().c_str()); Serial.printf("Battery write: %s:", pCharacteristic->toString().c_str());
print_hex(value, pCharacteristic->getLength()); print_hex(value, pCharacteristic->getLength());
Serial.println(); Serial.println();
} }
void onRead(BLECharacteristic *pCharacteristic) { void onRead(BLECharacteristic *pCharacteristic) {
Serialprintln("Battery read: %s", pCharacteristic->toString().c_str()); Serial.printf("Battery read: %s\n", 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 { class genericCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) { void onWrite(BLECharacteristic *pCharacteristic) {
uint8_t *value = pCharacteristic->getData(); //uint8_t *value = pCharacteristic->getData();
Serialprintln("generic write %s:", pCharacteristic->toString().c_str()); Serial.printf("generic write %s:\n", pCharacteristic->toString().c_str());
print_hex(value, pCharacteristic->getLength()); //print_hex(value, pCharacteristic->getLength());
Serial.println();
} }
void onRead(BLECharacteristic *pCharacteristic) { void onRead(BLECharacteristic *pCharacteristic) {
Serialprintln("generic read %s.", pCharacteristic->toString().c_str()); Serial.printf("generic read %s.\n", pCharacteristic->toString().c_str());
Serial.println();
} }
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() { void setup() {
@@ -197,8 +344,6 @@ void setup() {
Serial.println(NAME " initializing ..."); Serial.println(NAME " initializing ...");
BLEDevice::init(NAME); BLEDevice::init(NAME);
Serialprintln("MTU: %d", BLEDevice::getMTU());
// Create the BLE Server // Create the BLE Server
pServer = BLEDevice::createServer(); pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks()); pServer->setCallbacks(new MyServerCallbacks());
@@ -207,68 +352,71 @@ void setup() {
BLEService *pCovService = pServer->createService(COVER_SERVICE_UUID); BLEService *pCovService = pServer->createService(COVER_SERVICE_UUID);
// Create a BLE Characteristic // Create a BLE Characteristic
pCharacteristic_cover = pCovService->createCharacteristic( pCharacteristic_cover = pCovService->createCharacteristic(
COVER_CHAR_UUID, COVER_CHAR_UUID,
BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_NR
);
pCharacteristic_cover->setCallbacks(new coverCallbacks()); pCharacteristic_cover->setCallbacks(new coverCallbacks());
// Create a BLE Descriptor // Create a BLE Descriptor
BLEDescriptor *pDesc1 = new BLEDescriptor("2901", 10); /* BLEDescriptor *pDesc1 = new BLEDescriptor("2901", 10);
pDesc1->setValue("cover"); pDesc1->setValue("cover");*/
//pCharacteristic_cover->addDescriptor(pDesc1);
pCharacteristic_cover->addDescriptor(new BLE2902()); pCharacteristic_cover->addDescriptor(new BLE2902());
pCharacteristic_cover->addDescriptor(pDesc1);
pCharacteristic_unknown = pCovService->createCharacteristic( pCharacteristic_unknown = pCovService->createCharacteristic(
XXX_CHAR_UUID, XXX_CHAR_UUID,
BLECharacteristic::PROPERTY_INDICATE | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_NR
);
pCharacteristic_unknown->setCallbacks(new genericCallbacks()); pCharacteristic_unknown->setCallbacks(new genericCallbacks());
pCharacteristic_unknown->addDescriptor(new BLE2902());
BLEService *pBatService = pServer->createService(BAT_SERVICE_UUID); BLEService *pBatService = pServer->createService(BAT_SERVICE_UUID);
pCharacteristic_bat = pBatService->createCharacteristic( pCharacteristic_bat = pBatService->createCharacteristic(
BAT_CHAR_UUID, BAT_CHAR_UUID,
BLECharacteristic::PROPERTY_READ BLECharacteristic::PROPERTY_READ);
);
pCharacteristic_bat->setCallbacks(new batteryCallbacks()); pCharacteristic_bat->setCallbacks(new batteryCallbacks());
pBatService->addCharacteristic(pCharacteristic_bat);
uint8_t battery_level = 42; uint8_t battery_level = 42;
pCharacteristic_bat->setValue(&battery_level, 1); pCharacteristic_bat->setValue(&battery_level, 1);
pCharacteristic_bat->addDescriptor(new BLE2902()); pCharacteristic_bat->addDescriptor(new BLE2902());
// BLEService *pFWService = pServer->createService(FW_SERVICE_UUID); BLEService *pFWService = pServer->createService(FW_SERVICE_UUID);
// pCharacteristic_fw = pCovService->createCharacteristic( pCharacteristic_fw = pFWService->createCharacteristic(
// CHAR_FW_UUID, FW_CHAR_UUID,
// BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
// BLECharacteristic::PROPERTY_WRITE | pCharacteristic_fw->setCallbacks(new genericCallbacks());
// BLECharacteristic::PROPERTY_WRITE_NR pCharacteristic_fw->addDescriptor(new BLE2902());
// ); BLEDescriptor *pDesc2 = new BLEDescriptor("2901", 10);
// pCharacteristic_fw->setCallbacks(new genericCallbacks()); pDesc2->setValue("firmware");
// pCharacteristic_fw->addDescriptor(new BLE2902()); pCharacteristic_fw->addDescriptor(pDesc2);
// pCharacteristic_fw->addDescriptor(pDesc2);
//BLEDescriptor *pDesc2 = new BLEDescriptor("2901", 10);
//pDesc2->setValue("firmware");
// 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(); pCovService->start();
//pFWService->start(); pFWService->start();
pBatService->start();
pDEVService->start();
// Start advertising // Start advertising
BLEAdvertisementData AdvertisementData; BLEAdvertisementData AdvertisementData;
const String manufacturerData = String("\x19\x08\x00\x00\x2A\x00\x00\x00\x00\x00\xA2",11); const String manufacturerData = String("\x19\x08\x00\x00\x2A\x00\x00\x00\x00\x00\xA2", 11);
// Hunter Douglas ^^--^^ ^^ ID-Type // Hunter Douglas ^^--^^ ^^key^^ ^^ ID-Type
AdvertisementData.setManufacturerData(manufacturerData); AdvertisementData.setManufacturerData(manufacturerData);
AdvertisementData.setPartialServices(BLEUUID(COVER_SERVICE_UUID)); 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(); BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->setAdvertisementData(AdvertisementData); pAdvertising->setAdvertisementData(AdvertisementData);
BLEDevice::startAdvertising(); BLEDevice::startAdvertising();
Serial.println("Device " NAME " ready."); Serial.println("Device " NAME " ready.");

View File

@@ -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 <stddef.h> /* 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 <string.h>
#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 */

View File

@@ -1,12 +1,11 @@
# pyproject.toml # pyproject.toml
#[tool.setuptools.packages.find] [project]
#where = ["custom_components/"] requires-python = ">=3.12.0"
#include = ["bms_ble"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "8.0" 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 = [ pythonpath = [
"custom_components.hunterdouglas_powerview_ble", "custom_components.hunterdouglas_powerview_ble",
] ]
@@ -15,5 +14,152 @@ testpaths = [
] ]
asyncio_mode = "auto" 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] [tool.ruff.lint.per-file-ignores]
"scripts/*" = ["T201"] "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"
]

View File

@@ -1,4 +1,4 @@
homeassistant==2024.8.0 homeassistant==2024.11.0
pip>=21.3.1 pip>=21.3.1
ruff==0.4.2 ruff==0.6.8

View File

@@ -1,13 +1,13 @@
pip>=21.3.1 -r requirements.txt
homeassistant==2024.6.0
wheel wheel
home-assistant-bluetooth home-assistant-bluetooth
habluetooth>=2.4.0 habluetooth>=3.6.0
bluetooth-adapters bluetooth-adapters
pytest>=8.0.2 pytest>=8.3.3
pytest-cov>=4.0.0 pytest-cov>=5.0.0
pytest-socket>=0.5.0 pytest-socket>=0.7.0
pytest-asyncio pytest-asyncio>=0.24.0
sqlalchemy sqlalchemy
freezegun freezegun
requests-mock requests-mock
@@ -16,10 +16,10 @@ aiohttp
aiohttp_cors aiohttp_cors
aiohttp-fast-url-dispatcher aiohttp-fast-url-dispatcher
aiohttp-zlib-ng aiohttp-zlib-ng
bleak>=0.19.0 bleak>=0.22.3
bleak-retry-connector>=3.3.0 bleak-retry-connector>=3.6.0
bluetooth-data-tools bluetooth-data-tools
pyserial-asyncio pyserial-asyncio
pyudev pyudev
pytest-homeassistant-custom-component==0.13.132 pytest-homeassistant-custom-component==0.13.181