diff --git a/pyproject.toml b/pyproject.toml index fd44020..59f0065 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,9 @@ asyncio_mode = "auto" [tool.ruff] required-version = ">=0.6.8" +[tool.ruff.lint.per-file-ignores] +"scripts/*" = ["T201"] + [tool.ruff.lint] select = [ "A001", # Variable {name} is shadowing a Python builtin @@ -160,4 +163,3 @@ ignore = [ # Disabled because ruff does not understand type of __all__ generated by a function "PLE0605" ] - diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..1443856 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Script to extract PowerView homekey from a G3 PowerView Gateway.""" diff --git a/scripts/extract_gateway3_homekey.py b/scripts/extract_gateway3_homekey.py new file mode 100644 index 0000000..b52b1e3 --- /dev/null +++ b/scripts/extract_gateway3_homekey.py @@ -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(" 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: + """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)))