From 054f35f838571fb9470aa772eabcc6af0dd325d5 Mon Sep 17 00:00:00 2001 From: Frans-Willem Hardijzer Date: Mon, 14 Oct 2024 14:45:14 +0200 Subject: [PATCH 1/8] Helper script to extract homekey(s) from PowerView Gateway Gen3 --- scripts/extract_gateway3_homekey.py | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 scripts/extract_gateway3_homekey.py diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py new file mode 100644 index 0000000..fbcb2ff --- /dev/null +++ b/scripts/extract_gateway3_homekey.py @@ -0,0 +1,71 @@ +import requests +import json +import base64 +import struct + +HUB = "http://192.168.0.184" + +def create_request(sid, cid, sequenceId, data): + data = struct.pack(" Date: Thu, 2 Jan 2025 10:21:36 +0100 Subject: [PATCH 2/8] Update extract_gateway3_homekey.py --- scripts/extract_gateway3_homekey.py | 124 +++++++++++++++++----------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py index fbcb2ff..ff5058b 100644 --- a/scripts/extract_gateway3_homekey.py +++ b/scripts/extract_gateway3_homekey.py @@ -1,71 +1,99 @@ -import requests -import json +"""Extract PowerView homekey from a G3 PowerView Gateway""" + import base64 +import json import struct +from typing import Any, Final -HUB = "http://192.168.0.184" +import requests -def create_request(sid, cid, sequenceId, data): - data = struct.pack(" bytes: + """Assemble a request frame for the PowerView protocol.""" + return struct.pack(" dict[str, Any]: if len(packet) < 4: - raise Exception('Packet size too small') - sid, cid, sequenceId, length = struct.unpack(" bytes: + """Create a GetShadeKey request frame.""" + return create_request(251, 18, sequence_id, b"") + + +def get_shade_key(hub: str, ble_name) -> bytes: + """Get the homekey for a shade.""" + try: + shades_exec_resp: requests.Response = requests.post( + hub + "/home/shades/exec?shades=" + ble_name, + json={"hex": create_get_shade_key_request(1).hex()}, + timeout=TIMEOUT, + ) + shades_exec_resp.raise_for_status() + except requests.exceptions.RequestException as ex: + print(f"Unable to send GetShadeKey {ex!s}") + raise + + result: dict = json.loads(shades_exec_resp.content) + if result.get("err") != 0 or len(result.get("responses", [])) != 1: + raise IOError("Error when attempting GetShadeKey") + response: Final[bytes] = bytes.fromhex(result["responses"][0]["hex"]) + dec_resp: Final[dict[str, Any]] = decode_response(response) + if dec_resp["errorCode"] != 0: + raise ValueError("BLE errorCode is not 0") + if len(dec_resp["data"]) != 16: + raise ValueError("Expected 16 byte homekey") + return dec_resp["data"] + + +def main(hub: str) -> None: + """Main function, extract homekeys from all shades.""" + try: + shades_resp: requests.Response = requests.get( + hub + "/home/shades", timeout=TIMEOUT + ) + shades_resp.raise_for_status() + except requests.exceptions.RequestException as ex: + print(f"Unable to get list of shades:\n\t{ex!s}") + return + shades = json.loads(shades_resp.content) print(f"Found {len(shades)} shades, interrogating") for shade in shades: - name = base64.b64decode(shade['name']).decode('utf-8') + name: str = base64.b64decode(shade["name"]).decode("utf-8") + key: bytes = get_shade_key(hub, shade["bleName"]) + print(f"Shade '{name}':") print(f"\tBLE name: '{shade['bleName']}'") - - key = get_shade_key(hub, shade['bleName']) print(f"\tHomeKey: {key.hex()}") -if __name__ == '__main__': +if __name__ == "__main__": import argparse import sys - parser = argparse.ArgumentParser(description="Extract PowerView homekey from a G3 PowerView Gateway") - parser.add_argument("hub", help="URL to HUB", default="http://powerview-g3.local") + + parser = argparse.ArgumentParser( + description="Extract PowerView homekey from a G3 PowerView Gateway" + ) + parser.add_argument("hub", nargs="?", help="URL to HUB", default=HUB) args = parser.parse_args() sys.exit(main(**vars(args))) From 52f7390fc0168ce45223cf63e6e031252137b1cd Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:30:23 +0100 Subject: [PATCH 3/8] fixed ruff --- pyproject.toml | 3 +++ scripts/extract_gateway3_homekey.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 404daa0..451ebb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,6 @@ testpaths = [ "tests", ] asyncio_mode = "auto" + +[tool.ruff.lint] +ignore = ["T201"] # ignore for script diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py index ff5058b..049586d 100644 --- a/scripts/extract_gateway3_homekey.py +++ b/scripts/extract_gateway3_homekey.py @@ -1,4 +1,4 @@ -"""Extract PowerView homekey from a G3 PowerView Gateway""" +"""Extract PowerView homekey from a G3 PowerView Gateway.""" import base64 import json @@ -55,7 +55,7 @@ def get_shade_key(hub: str, ble_name) -> bytes: result: dict = json.loads(shades_exec_resp.content) if result.get("err") != 0 or len(result.get("responses", [])) != 1: - raise IOError("Error when attempting GetShadeKey") + raise OSError("Error when attempting GetShadeKey") response: Final[bytes] = bytes.fromhex(result["responses"][0]["hex"]) dec_resp: Final[dict[str, Any]] = decode_response(response) if dec_resp["errorCode"] != 0: From d79096357d227dabeb02e358238c28dec0235cc7 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:32:30 +0100 Subject: [PATCH 4/8] modified ignore pattern --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 451ebb4..d10245b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,5 +15,5 @@ testpaths = [ ] asyncio_mode = "auto" -[tool.ruff.lint] -ignore = ["T201"] # ignore for script +[tool.ruff.lint.per-file-ignores] +"scripts/*" = ["T201"] From f2ad61a0166885c1065bf856d898f2a76fb1ecff Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:37:34 +0100 Subject: [PATCH 5/8] Update extract_gateway3_homekey.py --- scripts/extract_gateway3_homekey.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py index 049586d..b52b1e3 100644 --- a/scripts/extract_gateway3_homekey.py +++ b/scripts/extract_gateway3_homekey.py @@ -17,6 +17,7 @@ def create_request(sid: int, cid: int, sequence_id: int, data: bytes) -> bytes: def decode_response(packet: bytes) -> dict[str, Any]: + """Decode a response frame from the PowerView protocol.""" if len(packet) < 4: raise ValueError("Packet size too small") sid, cid, sequence_id, length = struct.unpack(" bytes: def main(hub: str) -> None: - """Main function, extract homekeys from all shades.""" + """Extract the homekeys from all shades.""" try: shades_resp: requests.Response = requests.get( hub + "/home/shades", timeout=TIMEOUT From a6aaf4d7279b68f84b3229ac2cfa15f40c6e1e93 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:41:29 +0100 Subject: [PATCH 6/8] Create __init.py__ --- scripts/__init.py__ | 1 + 1 file changed, 1 insertion(+) create mode 100644 scripts/__init.py__ diff --git a/scripts/__init.py__ b/scripts/__init.py__ new file mode 100644 index 0000000..605b7d6 --- /dev/null +++ b/scripts/__init.py__ @@ -0,0 +1 @@ +"""Script to extract PowerView homekey from a G3 PowerView Gateway.""" \ No newline at end of file From b2d5335e1d82dd50698f37528b0014424e128e4b Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:44:03 +0100 Subject: [PATCH 7/8] Rename __init.py__ to __init__.py --- scripts/{__init.py__ => __init__.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename scripts/{__init.py__ => __init__.py} (90%) diff --git a/scripts/__init.py__ b/scripts/__init__.py similarity index 90% rename from scripts/__init.py__ rename to scripts/__init__.py index 605b7d6..1443856 100644 --- a/scripts/__init.py__ +++ b/scripts/__init__.py @@ -1 +1 @@ -"""Script to extract PowerView homekey from a G3 PowerView Gateway.""" \ No newline at end of file +"""Script to extract PowerView homekey from a G3 PowerView Gateway.""" From 62bbfd73614a71b90d512f6c92d5b5ab611a12d6 Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:10:12 +0100 Subject: [PATCH 8/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6af5195..92a4adf 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Installation can be done using [HACS](https://hacs.xyz/) by [adding a custom rep 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. +2. Extracting from gateway: This [script](scripts/extract_gateway3_homekey.py) is able to extract the key from a working PowerView gateway. 3. Grabing from the app: Checkout this [post in the Home Assistant community forum](https://community.home-assistant.io/t/hunter-douglas-powerview-gen-3-integration/424836/228). 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).