116 lines
4.1 KiB
Python
Executable File
116 lines
4.1 KiB
Python
Executable File
#!/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())
|