Import smart-home into Gitea

This commit is contained in:
Claude
2026-07-30 18:39:43 -05:00
commit c79e0a2d87
7 changed files with 462 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bun
// Home Assistant REST API client for the CoyotePack / SW Labs smart-home project.
// Runtime: bun (this machine has no node). See README.md.
//
// Config: baseUrl comes from config.json (falls back to config.example.json).
// Token comes from the env var named by config.tokenEnv (default HA_TOKEN) — never hard-coded.
//
// Usage:
// HA_TOKEN=... bun ha.js ping # verify connection + show HA version
// HA_TOKEN=... bun ha.js states # dump all entity states -> data/states.json
// HA_TOKEN=... bun ha.js states light # only entities in the "light" domain
// HA_TOKEN=... bun ha.js get light.kitchen # one entity
// HA_TOKEN=... bun ha.js call light turn_on '{"entity_id":"light.kitchen"}'
// HA_TOKEN=... bun ha.js services # list available services -> data/services.json
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = dirname(fileURLToPath(import.meta.url));
function loadConfig() {
const path = existsSync(join(ROOT, "config.json"))
? join(ROOT, "config.json")
: join(ROOT, "config.example.json");
return JSON.parse(readFileSync(path, "utf8"));
}
const cfg = loadConfig();
const BASE = (cfg.baseUrl || "").replace(/\/+$/, "");
const TOKEN = process.env[cfg.tokenEnv || "HA_TOKEN"];
if (!BASE) fail("No baseUrl in config.");
if (!TOKEN) fail(`No token. Set the ${cfg.tokenEnv || "HA_TOKEN"} environment variable.`);
function fail(msg) {
console.error(`ERROR: ${msg}`);
process.exit(1);
}
async function api(path, { method = "GET", body } = {}) {
const res = await fetch(`${BASE}/api${path}`, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const text = await res.text();
fail(`${method} ${path} -> HTTP ${res.status}\n${text}`);
}
const ct = res.headers.get("content-type") || "";
return ct.includes("application/json") ? res.json() : res.text();
}
function save(name, obj) {
const p = join(ROOT, "data", name);
writeFileSync(p, JSON.stringify(obj, null, 2));
console.log(`saved ${p}`);
}
const [cmd, arg1, arg2] = process.argv.slice(2);
switch (cmd) {
case "ping": {
const info = await api("/");
const cfgInfo = await api("/config");
console.log(`Connected to ${BASE}`);
console.log(` message: ${info.message}`);
console.log(` version: ${cfgInfo.version}`);
console.log(` location: ${cfgInfo.location_name}`);
break;
}
case "states": {
const all = await api("/states");
const filtered = arg1
? all.filter((e) => e.entity_id.startsWith(`${arg1}.`))
: all;
// Summary by domain
const byDomain = {};
for (const e of all) {
const d = e.entity_id.split(".")[0];
byDomain[d] = (byDomain[d] || 0) + 1;
}
console.log(`${all.length} entities across ${Object.keys(byDomain).length} domains:`);
for (const [d, n] of Object.entries(byDomain).sort((a, b) => b[1] - a[1])) {
console.log(` ${String(n).padStart(4)} ${d}`);
}
save(arg1 ? `states.${arg1}.json` : "states.json", filtered);
break;
}
case "get": {
if (!arg1) fail("usage: get <entity_id>");
console.log(JSON.stringify(await api(`/states/${arg1}`), null, 2));
break;
}
case "services": {
save("services.json", await api("/services"));
break;
}
case "call": {
if (!arg1 || !arg2) fail('usage: call <domain> <service> \'{"entity_id":"..."}\'');
const [domain, service] = [arg1, arg2];
const data = process.argv[5] ? JSON.parse(process.argv[5]) : {};
console.log(JSON.stringify(await api(`/services/${domain}/${service}`, { method: "POST", body: data }), null, 2));
break;
}
default:
console.log("commands: ping | states [domain] | get <entity_id> | services | call <domain> <service> <json>");
}