#!/usr/bin/env python3 """ List the Home Assistant entities this project cares about. Used to pin down entity IDs before wiring them into the firmware, and to re-check them after changing something in HA -- entity IDs are not stable across integration renames, and a wrong one fails silently as "unavailable". export HA_TOKEN='...' # Profile -> Security -> Long-lived access token python3 tools/ha_discover.py The token is read from the environment and never printed or written anywhere. --host HOST default 192.168.1.134 --all every entity, not just the interesting domains --json raw JSON for the matched entities, including attributes """ import argparse import json import os import sys import urllib.error import urllib.request # The domains this display draws from. "sensor" is where a weather station's # individual measurements land; "weather" is the forecast entity; "sun" gives # sunrise/sunset; "light" is what we control. DEFAULT_DOMAINS = ("weather", "sun", "light", "sensor", "binary_sensor", "switch") def fetch(host: str, token: str, path: str): req = urllib.request.Request( f"http://{host}:8123{path}", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(req, timeout=10) as resp: return json.load(resp) def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--host", default="192.168.1.134") ap.add_argument("--all", action="store_true", help="include every domain") ap.add_argument("--json", action="store_true", help="dump raw JSON with attributes") args = ap.parse_args() token = os.environ.get("HA_TOKEN") if not token: print("HA_TOKEN is not set.\n" "In Home Assistant: click your user (bottom left) -> Security tab ->\n" "Long-lived access tokens -> Create token. Then:\n" " export HA_TOKEN='paste-it-here'", file=sys.stderr) return 2 try: states = fetch(args.host, token, "/api/states") except urllib.error.HTTPError as e: detail = "token rejected -- create a new long-lived token" if e.code == 401 else e.reason print(f"HTTP {e.code} from {args.host}: {detail}", file=sys.stderr) return 1 except urllib.error.URLError as e: print(f"Cannot reach Home Assistant at {args.host}:8123 -- {e.reason}", file=sys.stderr) return 1 matched = [ s for s in states if args.all or s["entity_id"].split(".", 1)[0] in DEFAULT_DOMAINS ] matched.sort(key=lambda s: s["entity_id"]) if args.json: print(json.dumps(matched, indent=2)) return 0 print(f"{len(matched)} entities on {args.host} " f"(of {len(states)} total)\n") by_domain: dict[str, list] = {} for s in matched: by_domain.setdefault(s["entity_id"].split(".", 1)[0], []).append(s) for domain in sorted(by_domain): entities = by_domain[domain] print(f"=== {domain} ({len(entities)}) " + "=" * (46 - len(domain))) for s in entities: attrs = s.get("attributes", {}) name = attrs.get("friendly_name", "") unit = attrs.get("unit_of_measurement", "") state = s["state"] if len(state) > 28: state = state[:25] + "..." print(f" {s['entity_id']:<48} {state:>30} {unit:<6} {name}") print() # The forecast is not in /api/states -- it moved to a service call that # returns a response. Flag which entities support it so we know what to ask. weather = by_domain.get("weather", []) if weather: print("Forecast note: weather.* forecasts come from the") print(" weather.get_forecasts service (POST /api/services/weather/get_forecasts") print(" with ?return_response), not from /api/states. Candidates:") for s in weather: print(f" {s['entity_id']}") return 0 if __name__ == "__main__": sys.exit(main())