From 9ee1b6c6d2e13d2ed7478bb0427e7fa93a68cf4b Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:15:44 +0100 Subject: [PATCH 01/19] update emulator --- .gitignore | 2 + emu/PV_BLE_cover/PV_BLE_cover.ino | 157 +++++++++++++++++------------- 2 files changed, 90 insertions(+), 69 deletions(-) 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/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 3f7e733..1fab724 100644 --- a/emu/PV_BLE_cover/PV_BLE_cover.ino +++ b/emu/PV_BLE_cover/PV_BLE_cover.ino @@ -45,30 +45,20 @@ struct position { uint8_t velocity; }; +struct notification { + uint8_t *data; + BLECharacteristic *characteristic; +}; + BLECharacteristic *pCharacteristic_cover, *pCharacteristic_fw, *pCharacteristic_unknown, *pCharacteristic_bat; BLEServer *pServer = NULL; bool deviceConnected = false; bool oldDeviceConnected = false; +struct notification rx_data; +volatile bool data_available = false; +uint8_t buffer[20]; -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; - } - } - Serial.println(); - va_end(args); -} - -const char* decode_cmd(uint16_t cmd) { +const char* dec_cmd(uint16_t cmd) { switch(cmd) { case 0x01: return "set position"; @@ -106,27 +96,26 @@ class MyServerCallbacks: public BLEServerCallbacks { } void onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) { - Serialprintln("MTU changed: %d", pServer->getPeerMTU(pServer->getConnId())); + Serial.printf("MTU changed: %d\n", pServer->getPeerMTU(pServer->getConnId())); } }; -class coverCallbacks: public BLECharacteristicCallbacks { - void onWrite(BLECharacteristic *pCharacteristic) { - uint8_t *value = pCharacteristic->getData(); +void decode() { + Serial.printf("Cover write %s:\n\t", rx_data.characteristic->toString().c_str()); + print_hex(rx_data.characteristic->getData(), rx_data.characteristic->getLength()); - Serialprintln("Cover write %s:", pCharacteristic->toString().c_str()); - print_hex(value, pCharacteristic->getLength()); + if (rx_data.characteristic->getLength() < 4) return; 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); + memcpy((void *) &data, rx_data.characteristic->getData(), 4); + Serial.printf("SRV: %x, CMD %x, SEQ %x, LEN %x\n", 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); + memcpy((void *) &pos, &rx_data.data[4], data.data_len); + Serial.printf("\tset position\tpos1 %f%%, pos2 %d, pos3 %d, tilt %d, velocity %d\n", pos.pos1/100.0, pos.pos2, pos.pos3, pos.tilt, pos.velocity); break; case 0xF7B8: // stop movement @@ -134,30 +123,46 @@ class coverCallbacks: public BLECharacteristicCallbacks { break; case 0xF7BA: // activate scene - Serialprintln("\tactivate scene\tscene #%d", (uint16_t) value[4]); + Serial.printf("\tactivate scene\tscene #%d\n", (uint16_t) rx_data.data[4]); break; case 0xFA5B: // get scene - Serialprintln("\tget scene\tscene #%d", (uint16_t) value[4]); + Serial.printf("\tget scene\tscene #%d\n", (uint16_t) rx_data.data[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(); + const uint8_t ret_val[] = {0xEA, 0xEA, data.sequence, 0x1, 0x0}; + Serial.println("\treset scene automations:"); + memcpy(&buffer, ret_val, 5); + delay(100); + Serial.print("\t\tret value: "); + print_hex(buffer, 5); + rx_data.characteristic->setValue((uint8_t *) &buffer, 5); + rx_data.characteristic->notify(); break; } Serial.println(); + +} + +class coverCallbacks: public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { + rx_data.characteristic = pCharacteristic; + data_available = true; } void onRead(BLECharacteristic *pCharacteristic) { - Serialprintln("Cover read: %s", pCharacteristic->toString().c_str()); - Serial.println(); - } + Serial.printf("Cover read: %s\n", pCharacteristic->toString().c_str()); + } + + void onNotify(BLECharacteristic *pCharacteristic){ + Serial.println("onNotify()"); + } // not used + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ + Serial.println("onStatus()"); + }; // not used + }; class batteryCallbacks: public BLECharacteristicCallbacks { @@ -165,31 +170,42 @@ class batteryCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { 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()); Serial.println(); } void onRead(BLECharacteristic *pCharacteristic) { - Serialprintln("Battery read: %s", pCharacteristic->toString().c_str()); - Serial.println(); + Serial.printf("Battery read: %s\n", pCharacteristic->toString().c_str()); } + + void onNotify(BLECharacteristic *pCharacteristic){ + Serial.println("Battery onNotify()"); + } // not used + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ + Serial.println("Battery onStatus()"); + }; // not used }; class genericCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - uint8_t *value = pCharacteristic->getData(); + //uint8_t *value = pCharacteristic->getData(); - Serialprintln("generic write %s:", pCharacteristic->toString().c_str()); - print_hex(value, pCharacteristic->getLength()); - Serial.println(); + Serial.printf("generic write %s:", pCharacteristic->toString().c_str()); + //print_hex(value, pCharacteristic->getLength()); + Serial.println(); } 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.println("generic onNotify()"); + } // not used + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ + Serial.println("generic onStatus()"); + }; // not used }; void setup() { @@ -197,8 +213,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()); @@ -214,31 +228,31 @@ void setup() { ); 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_NOTIFY | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR ); pCharacteristic_unknown->setCallbacks(new genericCallbacks()); - - - BLEService *pBatService = pServer->createService(BAT_SERVICE_UUID); + pCharacteristic_unknown->addDescriptor(new BLE2902()); - pCharacteristic_bat = pBatService->createCharacteristic( - 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 *pBatService = pServer->createService(BAT_SERVICE_UUID); + + // pCharacteristic_bat = pBatService->createCharacteristic( + // BAT_CHAR_UUID, + // BLECharacteristic::PROPERTY_READ + // ); + // pCharacteristic_bat->setCallbacks(new batteryCallbacks()); + // 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( @@ -270,7 +284,7 @@ void setup() { pAdvertising->setAdvertisementData(AdvertisementData); BLEDevice::startAdvertising(); - + Serial.println("Device " NAME " ready."); } @@ -287,5 +301,10 @@ void loop() { if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting oldDeviceConnected = deviceConnected; - } + } + if (deviceConnected && data_available) { + data_available = false; + decode(); + delay(100); + } } From 31352a69b83c948eebfa2110ee168c3da8609ecf Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:38:26 +0100 Subject: [PATCH 02/19] stricter ruff formatting --- .../hunterdouglas_powerview_ble/__init__.py | 1 - .../hunterdouglas_powerview_ble/api.py | 49 +++--- .../config_flow.py | 1 - .../coordinator.py | 9 +- .../hunterdouglas_powerview_ble/cover.py | 12 +- .../hunterdouglas_powerview_ble/sensor.py | 4 +- pyproject.toml | 155 +++++++++++++++++- requirements.txt | 2 +- 8 files changed, 195 insertions(+), 38 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/__init__.py b/custom_components/hunterdouglas_powerview_ble/__init__.py index abbdce4..307c1b1 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 diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index e45fca2..3567b3c 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -1,28 +1,27 @@ """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 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]] = { @@ -82,7 +81,15 @@ 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() @@ -106,7 +113,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 +127,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( @@ -156,7 +162,7 @@ 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), @@ -215,12 +221,12 @@ class PowerViewBLE: ), ) - def _verify_response(self, input: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: + def _verify_response(self, din: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: """Verify shade response data.""" - data = input + data: bytearray = din if self._cipher is not None: dec = self._cipher.decryptor() - data = dec.update(input) + dec.finalize() + data = bytearray(dec.update(din) + dec.finalize()) if len(data) < 4: LOGGER.error("Reponse message too short") return False @@ -229,14 +235,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 %d returned error #%d", cmd.value, int(data[4])) return False return True @@ -255,7 +261,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 +270,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() @@ -289,8 +294,6 @@ class PowerViewBLE: return start = time.time() - await close_stale_connections(self._ble_device) - self._client = await establish_connection( BleakClient, self._ble_device, @@ -308,10 +311,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/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/coordinator.py b/custom_components/hunterdouglas_powerview_ble/coordinator.py index 59198ce..35a526d 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, 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/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/pyproject.toml b/pyproject.toml index 404daa0..fd44020 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", ] @@ -14,3 +13,151 @@ testpaths = [ "tests", ] asyncio_mode = "auto" + +# ruff configuration taken from HA 2024.11.2 (less ignores) +[tool.ruff] +required-version = ">=0.6.8" + +[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..fd1987d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ homeassistant==2024.8.0 pip>=21.3.1 -ruff==0.4.2 +ruff==0.6.8 From 9ba81852e9875cff9d4456bb0df5c20e114e854b Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:27:16 +0100 Subject: [PATCH 03/19] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fc55a17..de00a1b 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ 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. +> - 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) ## Features - Zero configuration From 28e7b0a212d255b45fef035abe98cbf3ad133b9c Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:26:00 +0100 Subject: [PATCH 04/19] add workflows --- .github/workflows/hassfest.yaml | 16 ++++++++++++++++ .github/workflows/stale.yaml | 22 ++++++++++++++++++++++ .github/workflows/validate.yaml | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 .github/workflows/hassfest.yaml create mode 100644 .github/workflows/stale.yaml create mode 100644 .github/workflows/validate.yaml 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/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/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..df59ded --- /dev/null +++ b/.github/workflows/validate.yaml @@ -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" + From 88f467d7475a9594b61e2da91038c73a852ab1c3 Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:49:35 +0100 Subject: [PATCH 05/19] disable HACS validation HACS requires brand repository to be populated --- .github/workflows/{validate.yaml => _validate.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{validate.yaml => _validate.bak} (100%) diff --git a/.github/workflows/validate.yaml b/.github/workflows/_validate.bak similarity index 100% rename from .github/workflows/validate.yaml rename to .github/workflows/_validate.bak From 57bb36b3172dfd0d89ca05ec5ca06e36402a41c7 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:54:42 +0100 Subject: [PATCH 06/19] Update manifest.json --- custom_components/hunterdouglas_powerview_ble/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hunterdouglas_powerview_ble/manifest.json b/custom_components/hunterdouglas_powerview_ble/manifest.json index 0038dc6..4011766 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.21" } From 6301253c9b09d46b469f5f86604360ae88e6264b Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:15:04 +0100 Subject: [PATCH 07/19] update requirements to HA 2024.11 --- requirements.txt | 2 +- requirements_test.txt | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index fd1987d..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.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 From 438201bdbf82621581596ae1983380f0f3c48ec4 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:15:09 +0100 Subject: [PATCH 08/19] Create lint.yml --- .github/workflows/lint.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/lint.yml 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 . From e8693f126f08388f5543b9431eb7348c201feafe Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sat, 21 Dec 2024 09:10:40 +0100 Subject: [PATCH 09/19] stronger typing --- .../hunterdouglas_powerview_ble/const.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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" From 74b5550dc169717fdbe592f771674826ae4c2064 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:25:01 +0100 Subject: [PATCH 10/19] enable mix of (non-)encrypted devices --- .../hunterdouglas_powerview_ble/api.py | 14 ++++++++++++-- .../hunterdouglas_powerview_ble/coordinator.py | 1 + .../hunterdouglas_powerview_ble/manifest.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index 3567b3c..5f7db06 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -95,6 +95,7 @@ class PowerViewBLE: 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 @@ -105,6 +106,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.""" @@ -135,7 +145,7 @@ class PowerViewBLE: ) + cmd_run[1] ) - if self._cipher is not None: + if self._cipher is not None and self._is_encrypted: enc = self._cipher.encryptor() tx_data = enc.update(tx_data) + enc.finalize() self._data_event.clear() @@ -224,7 +234,7 @@ class PowerViewBLE: def _verify_response(self, din: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: """Verify shade response data.""" data: bytearray = din - if self._cipher is not None: + if self._cipher is not None and self._is_encrypted: dec = self._cipher.decryptor() data = bytearray(dec.update(din) + dec.finalize()) if len(data) < 4: diff --git a/custom_components/hunterdouglas_powerview_ble/coordinator.py b/custom_components/hunterdouglas_powerview_ble/coordinator.py index 35a526d..f283fe7 100644 --- a/custom_components/hunterdouglas_powerview_ble/coordinator.py +++ b/custom_components/hunterdouglas_powerview_ble/coordinator.py @@ -103,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/manifest.json b/custom_components/hunterdouglas_powerview_ble/manifest.json index 4011766..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" } From d75999191564d050a667da44f11f7337bca1a34f Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 29 Dec 2024 12:49:30 +0100 Subject: [PATCH 11/19] shade emulator can be integrated --- emu/PV_BLE_cover/PV_BLE_cover.ino | 479 ++++++++++++++++---------- emu/PV_BLE_cover/user_settings.h | 540 ++++++++++++++++++++++++++++++ 2 files changed, 844 insertions(+), 175 deletions(-) create mode 100644 emu/PV_BLE_cover/user_settings.h diff --git a/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 1fab724..45d0da7 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 { @@ -46,166 +64,279 @@ struct position { }; struct notification { - uint8_t *data; - BLECharacteristic *characteristic; + 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; -uint8_t buffer[20]; +const byte zero_key[16] = { 0 }; +byte home_key[16] = { 0 }; -const char* dec_cmd(uint16_t cmd) { - switch(cmd) { - case 0x01: - return "set position"; - case 0xBA: - return "activate scene"; - default: - return "ERR"; - } -} - -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) { - Serial.printf("MTU changed: %d\n", pServer->getPeerMTU(pServer->getConnId())); - } +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 decode() { - Serial.printf("Cover write %s:\n\t", rx_data.characteristic->toString().c_str()); - print_hex(rx_data.characteristic->getData(), rx_data.characteristic->getLength()); - - if (rx_data.characteristic->getLength() < 4) return; - - struct header data; - memcpy((void *) &data, rx_data.characteristic->getData(), 4); - Serial.printf("SRV: %x, CMD %x, SEQ %x, LEN %x\n", 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, &rx_data.data[4], data.data_len); - Serial.printf("\tset position\tpos1 %f%%, pos2 %d, pos3 %d, tilt %d, velocity %d\n", 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 - Serial.printf("\tactivate scene\tscene #%d\n", (uint16_t) rx_data.data[4]); - break; - case 0xFA5B: - // get scene - Serial.printf("\tget scene\tscene #%d\n", (uint16_t) rx_data.data[4]); - break; - case 0xFAEA: - // Reset Scene Automations - // FIXME! wrong return value! - const uint8_t ret_val[] = {0xEA, 0xEA, data.sequence, 0x1, 0x0}; - Serial.println("\treset scene automations:"); - memcpy(&buffer, ret_val, 5); - delay(100); - Serial.print("\t\tret value: "); - print_hex(buffer, 5); - rx_data.characteristic->setValue((uint8_t *) &buffer, 5); - rx_data.characteristic->notify(); - break; - } - Serial.println(); - +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(); } -class coverCallbacks: public BLECharacteristicCallbacks { +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\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: + 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())); + } +}; + +class coverCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { - rx_data.characteristic = pCharacteristic; - data_available = true; + 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()); + 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 onNotify(BLECharacteristic *pCharacteristic){ - Serial.println("onNotify()"); - } // not used - void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ - Serial.println("onStatus()"); - }; // not used - + 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) { uint8_t *value = pCharacteristic->getData(); - Serial.printf("Battery write: %s:", pCharacteristic->toString().c_str()); + Serial.printf("Battery write: %s:", pCharacteristic->toString().c_str()); print_hex(value, pCharacteristic->getLength()); Serial.println(); - } - - void onRead(BLECharacteristic *pCharacteristic) { - Serial.printf("Battery read: %s\n", pCharacteristic->toString().c_str()); } - void onNotify(BLECharacteristic *pCharacteristic){ + void onRead(BLECharacteristic *pCharacteristic) { + Serial.printf("Battery read: %s\n", pCharacteristic->toString().c_str()); + } + + void onNotify(BLECharacteristic *pCharacteristic) { Serial.println("Battery onNotify()"); - } // not used - void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ + } + void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code) { Serial.println("Battery onStatus()"); - }; // not used + } }; -class genericCallbacks: public BLECharacteristicCallbacks { - +class genericCallbacks : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { //uint8_t *value = pCharacteristic->getData(); - Serial.printf("generic write %s:", pCharacteristic->toString().c_str()); + Serial.printf("generic write %s:\n", pCharacteristic->toString().c_str()); //print_hex(value, pCharacteristic->getLength()); - Serial.println(); - } + } void onRead(BLECharacteristic *pCharacteristic) { - Serial.printf("generic read %s.\n", pCharacteristic->toString().c_str()); + Serial.printf("generic read %s.\n", pCharacteristic->toString().c_str()); } - void onNotify(BLECharacteristic *pCharacteristic){ - Serial.println("generic onNotify()"); - } // not used - void onStatus(BLECharacteristic *pCharacteristic, Status s, uint32_t code){ - Serial.println("generic onStatus()"); - }; // not used + 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() { @@ -221,70 +352,73 @@ 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");*/ +pDesc1->setValue("cover");*/ //pCharacteristic_cover->addDescriptor(pDesc1); pCharacteristic_cover->addDescriptor(new BLE2902()); - - + + pCharacteristic_unknown = pCovService->createCharacteristic( - XXX_CHAR_UUID, - BLECharacteristic::PROPERTY_NOTIFY | - BLECharacteristic::PROPERTY_WRITE | - BLECharacteristic::PROPERTY_WRITE_NR - ); + 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 - // ); - // pCharacteristic_bat->setCallbacks(new batteryCallbacks()); - // 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 *pBatService = pServer->createService(BAT_SERVICE_UUID); + pCharacteristic_bat = pBatService->createCharacteristic( + BAT_CHAR_UUID, + BLECharacteristic::PROPERTY_READ); + pCharacteristic_bat->setCallbacks(new batteryCallbacks()); + uint8_t battery_level = 42; + pCharacteristic_bat->setValue(&battery_level, 1); + pCharacteristic_bat->addDescriptor(new BLE2902()); - // Start the service + 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); + + + 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."); } @@ -301,10 +435,5 @@ void loop() { if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting oldDeviceConnected = deviceConnected; - } - if (deviceConnected && data_available) { - data_available = false; - decode(); - delay(100); } } 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 From 8b0bccee6ba6eb95e52a55d50102e8adb378984a Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:45:10 +0100 Subject: [PATCH 12/19] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index de00a1b..2cac695 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Platform | Description | Unit | Details `sensor` | SoC (state of charge) | `%` | range 100% (full), 50%, 20%, 0% (battery empty) ## Installation +> [!IMPORTANT] +> You need to 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/). From ba487d907d89479a141db72e61c3e00a006a6f98 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:39:33 +0100 Subject: [PATCH 13/19] debug enhancement --- custom_components/hunterdouglas_powerview_ble/api.py | 5 +++-- emu/PV_BLE_cover/PV_BLE_cover.ino | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index 5f7db06..420f76d 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -145,11 +145,12 @@ class PowerViewBLE: ) + cmd_run[1] ) + LOGGER.debug("sending cmd: %s", tx_data.hex(" ")) if self._cipher is not None and self._is_encrypted: enc = self._cipher.encryptor() 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") @@ -179,7 +180,7 @@ class PowerViewBLE: ("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 diff --git a/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 45d0da7..5e3259c 100644 --- a/emu/PV_BLE_cover/PV_BLE_cover.ino +++ b/emu/PV_BLE_cover/PV_BLE_cover.ino @@ -208,7 +208,7 @@ void decode(BLECharacteristic *pChar) { case 0xFB02: // set shade key Serial.print("set shade key: "); - print_hex(&data_raw[4], data_len - 4, "\\x"); + 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) { From 591815652d24783cd84e600de9c8fb1e2a145380 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:20:11 +0100 Subject: [PATCH 14/19] support identification --- .../hunterdouglas_powerview_ble/__init__.py | 2 +- .../hunterdouglas_powerview_ble/api.py | 28 +++++--- .../hunterdouglas_powerview_ble/button.py | 72 +++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 custom_components/hunterdouglas_powerview_ble/button.py diff --git a/custom_components/hunterdouglas_powerview_ble/__init__.py b/custom_components/hunterdouglas_powerview_ble/__init__.py index 307c1b1..51665fb 100644 --- a/custom_components/hunterdouglas_powerview_ble/__init__.py +++ b/custom_components/hunterdouglas_powerview_ble/__init__.py @@ -14,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 420f76d..70bb7f2 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -12,6 +12,7 @@ from bleak.exc import BleakError from bleak.uuids import normalize_uuid_str from bleak_retry_connector import establish_connection from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.ciphers.base import CipherContext from homeassistant.components.cover import ATTR_CURRENT_POSITION from .const import LOGGER, TIMEOUT @@ -58,6 +59,7 @@ class ShadeCmd(Enum): SET_POSITION = 0x01F7 STOP = 0xB8F7 ACTIVATE_SCENE = 0xBAF7 + IDENTIFY = 0x11F7 @dataclass @@ -112,7 +114,7 @@ class PowerViewBLE: return self._is_encrypted @encrypted.setter - def encrypted(self, value:bool) -> None: + def encrypted(self, value: bool) -> None: self._is_encrypted = value @property @@ -212,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.""" @@ -232,12 +234,13 @@ class PowerViewBLE: ), ) - def _verify_response(self, din: 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: bytearray = din - if self._cipher is not None and self._is_encrypted: - dec = self._cipher.decryptor() - data = bytearray(dec.update(din) + dec.finalize()) if len(data) < 4: LOGGER.error("Reponse message too short") return False @@ -253,7 +256,7 @@ class PowerViewBLE: LOGGER.error("Wrong response data length") return False if int(data[4] != 0): - LOGGER.error("Command %d returned error #%d", cmd.value, int(data[4])) + LOGGER.error("Command %X returned error #%d", cmd.value, int(data[4])) return False return True @@ -291,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: @@ -304,7 +312,7 @@ class PowerViewBLE: LOGGER.debug("%s already connected", self.name) return - start = time.time() + start: float = time.time() self._client = await establish_connection( BleakClient, self._ble_device, diff --git a/custom_components/hunterdouglas_powerview_ble/button.py b/custom_components/hunterdouglas_powerview_ble/button.py new file mode 100644 index 0000000..9c1796c --- /dev/null +++ b/custom_components/hunterdouglas_powerview_ble/button.py @@ -0,0 +1,72 @@ +"""Hunter Douglas Powerview cover.""" + +from typing import Any, Final + +from bleak.exc import BleakError +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 .api import CLOSED_POSITION, OPEN_POSITION +from .const import DOMAIN, HOME_KEY, 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.""" + await self._coord.api.identify() From 4b51ea514f2dd62389c78734654d953e873be308 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:24:14 +0100 Subject: [PATCH 15/19] fix ruff --- custom_components/hunterdouglas_powerview_ble/button.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/button.py b/custom_components/hunterdouglas_powerview_ble/button.py index 9c1796c..bf7649b 100644 --- a/custom_components/hunterdouglas_powerview_ble/button.py +++ b/custom_components/hunterdouglas_powerview_ble/button.py @@ -1,8 +1,7 @@ """Hunter Douglas Powerview cover.""" -from typing import Any, Final +from typing import Final -from bleak.exc import BleakError from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) @@ -17,8 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .api import CLOSED_POSITION, OPEN_POSITION -from .const import DOMAIN, HOME_KEY, LOGGER +from .const import DOMAIN, LOGGER from .coordinator import PVCoordinator BUTTONS_SHADE: Final = [ @@ -69,4 +67,5 @@ class PowerViewButton(PassiveBluetoothCoordinatorEntity[PVCoordinator], ButtonEn async def async_press(self) -> None: """Handle the button press.""" + LOGGER.debug("identify cover") await self._coord.api.identify() From e97eef94f888842eb30d7e0bdf920134cd294f5a Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:24:22 +0100 Subject: [PATCH 16/19] update emulator --- emu/PV_BLE_cover/PV_BLE_cover.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emu/PV_BLE_cover/PV_BLE_cover.ino b/emu/PV_BLE_cover/PV_BLE_cover.ino index 5e3259c..a1f26d6 100644 --- a/emu/PV_BLE_cover/PV_BLE_cover.ino +++ b/emu/PV_BLE_cover/PV_BLE_cover.ino @@ -179,7 +179,7 @@ void decode(BLECharacteristic *pChar) { break; case 0xF711: // identify - Serial.printf("identify: %i\n", data_dec[4]); + Serial.printf("identify: %i times\n", data_dec[4]); resp_size = set_response(&response, (const message *)data_dec); break; case 0xF7B8: From 2c03881b60b90b52515106f534287f9f354f3fe3 Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:26:33 +0100 Subject: [PATCH 17/19] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2cac695..f19b54a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ 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) From 1559776aa8fb530effcf066a6dab89ecbd5ac255 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:10:51 +0100 Subject: [PATCH 18/19] document key extraction --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f19b54a..e4e9a97 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ > [!WARNING] > - 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) ## Features @@ -18,7 +17,7 @@ ### Supported Devices Type* | Description --- | -- +-- | -- 1 | Designer Roller 4 | Roman 5 | Bottom Up @@ -45,7 +44,8 @@ Platform | Description | Unit | Details ## Installation > [!IMPORTANT] -> You need to 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! +> 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/). @@ -60,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. 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. +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 From 1590647c9ea39084dc183a6c70587502e4443235 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:22:30 +0100 Subject: [PATCH 19/19] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4e9a97..6af5195 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom rep ## 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. 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. +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).