Files
homeAutomation/energy-management/frontend/src/Devices.tsx
2026-08-19 18:35:47 +02:00

248 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, type FormEvent } from "react";
import { api, type ControlKind, type DeviceConfig } from "./api";
const emptyForm = {
entityId: "",
name: "",
kind: "switch" as ControlKind,
priority: 1,
minValue: 0,
maxValue: 1,
dwellSeconds: 300,
manualOverride: false,
onlyWhenSun: false,
fromTime: null as string | null,
toTime: null as string | null,
enableUrl: null as string | null,
disableUrl: null as string | null,
};
export function Devices() {
const [devices, setDevices] = useState<DeviceConfig[]>([]);
const [form, setForm] = useState(emptyForm);
const [error, setError] = useState<string | null>(null);
async function refresh() {
setDevices(await api.getDevices());
}
useEffect(() => {
refresh();
}, []);
async function handleCreate(e: FormEvent) {
e.preventDefault();
try {
await api.createDevice(form);
setForm(emptyForm);
await refresh();
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create device");
}
}
async function toggleOverride(d: DeviceConfig) {
await api.updateDevice(d.id, { manualOverride: !d.manualOverride });
await refresh();
}
async function remove(d: DeviceConfig) {
if (!confirm(`Remove ${d.name}?`)) return;
await api.deleteDevice(d.id);
await refresh();
}
return (
<>
<div className="card">
<h2>Add device</h2>
<form onSubmit={handleCreate}>
<div className="form-grid">
<label>
Name
<input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
/>
</label>
<label>
Home Assistant entity ID
<input
value={form.entityId}
onChange={(e) => setForm({ ...form, entityId: e.target.value })}
placeholder="switch.ev_charger"
required
/>
</label>
<label>
Control type
<select
value={form.kind}
onChange={(e) =>
setForm({ ...form, kind: e.target.value as ControlKind })
}
>
<option value="switch">Switch (on/off)</option>
<option value="number">Number (set-point)</option>
</select>
</label>
<label>
Priority (lower = shed first)
<input
type="number"
value={form.priority}
onChange={(e) =>
setForm({ ...form, priority: Number(e.target.value) })
}
/>
</label>
{form.kind === "number" && (
<>
<label>
Min value (shed target)
<input
type="number"
value={form.minValue}
onChange={(e) =>
setForm({ ...form, minValue: Number(e.target.value) })
}
/>
</label>
<label>
Max value (normal)
<input
type="number"
value={form.maxValue}
onChange={(e) =>
setForm({ ...form, maxValue: Number(e.target.value) })
}
/>
</label>
</>
)}
<label>
Min dwell time (seconds)
<input
type="number"
value={form.dwellSeconds}
onChange={(e) =>
setForm({ ...form, dwellSeconds: Number(e.target.value) })
}
/>
</label>
<label>
Only enable if sun
<input
type="checkbox"
checked={form.onlyWhenSun}
onChange={(e) =>
setForm({ ...form, onlyWhenSun: e.target.checked })
}
/>
</label>
<label>
From (allowed window start)
<input
type="time"
value={form.fromTime ?? ""}
onChange={(e) =>
setForm({ ...form, fromTime: e.target.value || null })
}
/>
</label>
<label>
To (allowed window end)
<input
type="time"
value={form.toTime ?? ""}
onChange={(e) =>
setForm({ ...form, toTime: e.target.value || null })
}
/>
</label>
<label>
Custom enable endpoint (optional)
<input
type="url"
value={form.enableUrl ?? ""}
onChange={(e) =>
setForm({ ...form, enableUrl: e.target.value || null })
}
placeholder="https://example.local/device/on"
/>
</label>
<label>
Custom disable endpoint (optional)
<input
type="url"
value={form.disableUrl ?? ""}
onChange={(e) =>
setForm({ ...form, disableUrl: e.target.value || null })
}
placeholder="https://example.local/device/off"
/>
</label>
</div>
<p style={{ color: "var(--text-muted)", fontSize: 13 }}>
Set both custom endpoints to control this device over plain HTTP
(POST) instead of Home Assistant. The entity ID above is still used
as the device's unique key.
</p>
{error && (
<p style={{ color: "var(--status-critical)", fontSize: 13 }}>
{error}
</p>
)}
<button className="primary" type="submit">
Add device
</button>
</form>
</div>
<div className="card">
<h2>Configured devices</h2>
{devices.length === 0 ? (
<p style={{ color: "var(--text-muted)", fontSize: 13 }}>
No devices configured yet.
</p>
) : (
devices
.slice()
.sort((a, b) => a.priority - b.priority)
.map((d) => (
<div className="device-row" key={d.id}>
<div>
<div className="device-name">{d.name}</div>
<div className="device-meta">
{d.entityId} · {d.kind} · priority {d.priority} · dwell{" "}
{d.dwellSeconds}s{d.onlyWhenSun && " · sun only"}
{d.fromTime && d.toTime && ` · ${d.fromTime}${d.toTime}`}
{d.enableUrl && d.disableUrl && " · custom endpoints"}
<div>{d.enableUrl}</div>
<div>{d.disableUrl}</div>
</div>
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{d.manualOverride && (
<span className="badge override">Manual</span>
)}
<button
className="secondary"
onClick={() => toggleOverride(d)}
>
{d.manualOverride ? "Resume auto" : "Manual override"}
</button>
<button className="secondary" onClick={() => remove(d)}>
Remove
</button>
</div>
</div>
))
)}
</div>
</>
);
}