This commit is contained in:
patman15
2025-06-02 17:57:32 +02:00
4 changed files with 105 additions and 2 deletions

View File

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

View File

@@ -18,6 +18,9 @@ asyncio_mode = "auto"
[tool.ruff] [tool.ruff]
required-version = ">=0.6.8" required-version = ">=0.6.8"
[tool.ruff.lint.per-file-ignores]
"scripts/*" = ["T201"]
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"A001", # Variable {name} is shadowing a Python builtin "A001", # Variable {name} is shadowing a Python builtin
@@ -160,4 +163,3 @@ ignore = [
# Disabled because ruff does not understand type of __all__ generated by a function # Disabled because ruff does not understand type of __all__ generated by a function
"PLE0605" "PLE0605"
] ]

1
scripts/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Script to extract PowerView homekey from a G3 PowerView Gateway."""

View File

@@ -0,0 +1,100 @@
"""Extract PowerView homekey from a G3 PowerView Gateway."""
import base64
import json
import struct
from typing import Any, Final
import requests
HUB: Final[str] = "http://powerview-g3.local"
TIMEOUT: Final[int] = 10
def create_request(sid: int, cid: int, sequence_id: int, data: bytes) -> bytes:
"""Assemble a request frame for the PowerView protocol."""
return struct.pack("<BBBB", sid, cid, sequence_id, len(data)) + data
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("<BBBB", packet[0:4])
if len(packet) != 4 + length:
raise ValueError("Not all data present")
if length < 1:
raise ValueError("No errorCode present")
(error_code,) = struct.unpack("<B", packet[4:5])
data: Final[bytes] = packet[5:]
return {
"cid": cid,
"sid": sid,
"sequenceId": sequence_id,
"errorCode": error_code,
"data": data,
}
def create_get_shade_key_request(sequence_id) -> 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 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:
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:
"""Extract the 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: 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']}'")
print(f"\tHomeKey: {key.hex()}")
if __name__ == "__main__":
import argparse
import sys
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)))