Context for developing and debugging Hubitat Elevation apps, drivers, and hub environment — sandbox constraints, lifecycle idioms, capability contracts, plus grounded deploy/log-tail/lint mechanisms.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
#!/usr/bin/env python3
"""Owner script for the hubs.json stateful artifact — the only writer of its shape.
hubs.json records how to reach each hub for CODE operations (per-hub by IP; devices can
mesh but code cannot — see the multi-hub-topology rule). It holds IPs only, never secrets:
Maker API credentials come from the environment, not this file.
Schema (schema_version 1):
{"schema_version": 1,
"default": "main" | null,
"hubs": {"<name>": {"ip": "192.0.2.10", "port": 8080}, ...}}
Actions (deterministic JSON mutation): init, add, set-default, remove, list, validate.
On read, an older/absent schema_version is migrated up and rewritten (this script owns
migration; readers treat an unrecognized version as no-usable-state).
Usage:
hubs_config.py init [--path hubs.json] [--force]
hubs_config.py add --name main --ip 192.0.2.10 [--port 8080] [--default] [--path ...]
hubs_config.py set-default --name main [--path ...]
hubs_config.py remove --name main [--path ...]
hubs_config.py list [--path ...]
hubs_config.py validate [--path ...]
Output: the resulting config as JSON on stdout. Exit non-zero with a stderr message on error.
"""
import argparse
import json
import sys
from pathlib import Path
SCHEMA_VERSION = 1
def migrate(cfg: dict) -> dict:
"""Upgrade an older/absent-version config to the current schema. Owner-only."""
if "schema_version" not in cfg:
cfg["schema_version"] = SCHEMA_VERSION
cfg.setdefault("hubs", {})
cfg.setdefault("default", None)
if cfg["schema_version"] > SCHEMA_VERSION:
raise ValueError(
f"hubs.json schema_version {cfg['schema_version']} is newer than this script "
f"supports ({SCHEMA_VERSION}); upgrade the plugin.")
cfg["schema_version"] = SCHEMA_VERSION
return cfg
def load(path: Path) -> dict:
if not path.exists():
raise FileNotFoundError(f"{path} does not exist — run `hubs_config.py init` first")
raw = json.loads(path.read_text())
original_version = raw.get("schema_version") # capture before migrate() mutates raw in place
cfg = migrate(raw)
# Owner-migrates-on-read (stateful-artifacts): if the on-disk version was old, persist the
# upgrade so the migration is durable — even for read actions (list/validate), not just writes.
if original_version != SCHEMA_VERSION:
save(path, cfg)
return cfg
def save(path: Path, cfg: dict) -> None:
path.write_text(json.dumps(cfg, indent=2) + "\n")
def empty_config() -> dict:
return {"schema_version": SCHEMA_VERSION, "default": None, "hubs": {}}
def add_hub(cfg: dict, name: str, ip: str, port: int = 8080, make_default: bool = False) -> dict:
cfg["hubs"][name] = {"ip": ip, "port": port}
if make_default or cfg.get("default") is None:
cfg["default"] = name
return cfg
def set_default(cfg: dict, name: str) -> dict:
if name not in cfg["hubs"]:
raise ValueError(f"no hub named {name!r} (have: {', '.join(sorted(cfg['hubs'])) or 'none'})")
cfg["default"] = name
return cfg
def remove_hub(cfg: dict, name: str) -> dict:
if name not in cfg["hubs"]:
raise ValueError(f"no hub named {name!r}")
del cfg["hubs"][name]
if cfg.get("default") == name:
cfg["default"] = next(iter(cfg["hubs"]), None)
return cfg
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="Manage the hubs.json config.")
p.add_argument("action", choices=["init", "add", "set-default", "remove", "list", "validate"])
p.add_argument("--path", default="hubs.json")
p.add_argument("--name")
p.add_argument("--ip")
p.add_argument("--port", type=int, default=8080)
p.add_argument("--default", action="store_true")
p.add_argument("--force", action="store_true")
args = p.parse_args(argv)
path = Path(args.path)
try:
if args.action == "init":
if path.exists() and not args.force:
raise ValueError(f"{path} already exists — pass --force to overwrite")
cfg = empty_config()
elif args.action == "add":
if not (args.name and args.ip):
raise ValueError("add requires --name and --ip")
cfg = migrate(json.loads(path.read_text())) if path.exists() else empty_config()
cfg = add_hub(cfg, args.name, args.ip, args.port, args.default)
elif args.action == "set-default":
if not args.name:
raise ValueError("set-default requires --name")
cfg = set_default(load(path), args.name)
elif args.action == "remove":
if not args.name:
raise ValueError("remove requires --name")
cfg = remove_hub(load(path), args.name)
else: # list / validate
cfg = load(path)
except (ValueError, OSError, json.JSONDecodeError) as e:
print(str(e), file=sys.stderr)
return 1
if args.action not in ("list", "validate"):
try:
save(path, cfg)
except OSError as e:
print(f"cannot write {path}: {e}", file=sys.stderr)
return 1
json.dump(cfg, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
sys.exit(main())