fix: added energy-management

This commit is contained in:
Willem Serruys
2026-08-19 18:35:47 +02:00
parent fece1603e7
commit 53fa112aa8
38 changed files with 6060 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import type { Env } from "../env.js";
export interface HaState {
entity_id: string;
state: string;
attributes: Record<string, unknown>;
}
export class HomeAssistantClient {
constructor(private readonly env: Env) {}
private async request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${this.env.HA_URL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${this.env.HA_TOKEN}`,
"Content-Type": "application/json",
...init?.headers,
},
});
if (!res.ok) {
throw new Error(`Home Assistant request failed: ${init?.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`);
}
return (await res.json()) as T;
}
async getState(entityId: string): Promise<HaState> {
return this.request<HaState>(`/api/states/${entityId}`);
}
async getStates(): Promise<HaState[]> {
return this.request<HaState[]>(`/api/states`);
}
async callService(domain: string, service: string, data: Record<string, unknown>): Promise<void> {
await this.request(`/api/services/${domain}/${service}`, {
method: "POST",
body: JSON.stringify(data),
});
}
async setSwitch(entityId: string, on: boolean): Promise<void> {
const domain = entityId.split(".")[0];
await this.callService(domain, on ? "turn_on" : "turn_off", { entity_id: entityId });
}
async setNumber(entityId: string, value: number): Promise<void> {
await this.callService("number", "set_value", { entity_id: entityId, value });
}
/** Whether HA's sun.sun entity currently reports the sun above the horizon. */
async isSunUp(): Promise<boolean> {
const state = await this.getState("sun.sun");
return state.state === "above_horizon";
}
}

View File

@@ -0,0 +1,87 @@
import type { DeviceConfig, DeviceStatus } from "../types.js";
import type { HomeAssistantClient } from "./client.js";
function normalizeHaValue(kind: DeviceConfig["kind"], state: string): number | null {
if (kind === "switch") {
if (state === "on") return 1;
if (state === "off") return 0;
return null;
}
const n = Number(state);
return Number.isFinite(n) ? n : null;
}
/** True when both directions are driven by custom endpoints rather than Home Assistant. */
function isExternallyControlled(device: DeviceConfig): boolean {
return device.enableUrl !== null && device.disableUrl !== null;
}
async function callEndpoint(url: string): Promise<void> {
const res = await fetch(url, { method: "POST" });
if (!res.ok) {
throw new Error(`custom endpoint request failed: POST ${url} -> ${res.status} ${await res.text()}`);
}
}
export async function getDeviceStatus(
ha: HomeAssistantClient,
device: DeviceConfig,
shed: boolean,
lastChangedAt: string | null,
): Promise<DeviceStatus> {
// Externally controlled devices have no Home Assistant entity to poll; state is
// whatever we last commanded via the custom endpoints.
if (isExternallyControlled(device)) {
return {
...device,
currentValue: shed ? (device.kind === "switch" ? 0 : device.minValue) : device.kind === "switch" ? 1 : device.maxValue,
shed,
lastChangedAt,
available: true,
};
}
try {
const haState = await ha.getState(device.entityId);
return {
...device,
currentValue: normalizeHaValue(device.kind, haState.state),
shed,
lastChangedAt,
available: haState.state !== "unavailable" && haState.state !== "unknown",
};
} catch {
return {
...device,
currentValue: null,
shed,
lastChangedAt,
available: false,
};
}
}
/** Sheds (reduces load of) a device: turns a switch off, or lowers a number to its floor. */
export async function shedDevice(ha: HomeAssistantClient, device: DeviceConfig): Promise<void> {
if (device.disableUrl) {
await callEndpoint(device.disableUrl);
return;
}
if (device.kind === "switch") {
await ha.setSwitch(device.entityId, false);
} else {
await ha.setNumber(device.entityId, device.minValue);
}
}
/** Restores a device to its normal operating value. */
export async function restoreDevice(ha: HomeAssistantClient, device: DeviceConfig): Promise<void> {
if (device.enableUrl) {
await callEndpoint(device.enableUrl);
return;
}
if (device.kind === "switch") {
await ha.setSwitch(device.entityId, true);
} else {
await ha.setNumber(device.entityId, device.maxValue);
}
}