|
|
|
|
@@ -5,18 +5,51 @@
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
|
<title>SillyHome Next</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #111317; color: #f3f5f7; scroll-behavior:smooth; --accent:#ff8a1c; --accent-strong:#ffab45; --panel:#181c22; --panel-soft:#20262e; --border:#303842; }
|
|
|
|
|
body { margin: 0; font-size:15px; }
|
|
|
|
|
header { padding: 18px 20px; background: linear-gradient(135deg,#1c1f26,#3a2411 70%,#5c2d08); }
|
|
|
|
|
h1,h2,h3 { margin: 0 0 12px; }
|
|
|
|
|
header p { margin: 5px 0; color: #c3d1dc; }
|
|
|
|
|
main { display: grid; grid-template-columns: minmax(280px,.82fr) minmax(0,1.38fr); gap: 12px; padding: 12px; max-width:1380px; margin:0 auto; }
|
|
|
|
|
section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; min-width:0; }
|
|
|
|
|
section:target { outline:2px solid var(--accent); outline-offset:2px; }
|
|
|
|
|
.wide { grid-column: 1 / -1; }
|
|
|
|
|
.quick-nav { position:sticky; top:0; z-index:10; display:flex; gap:8px; overflow-x:auto; padding:10px 14px; background:rgba(17,19,23,.94); border-bottom:1px solid var(--border); backdrop-filter:blur(8px); }
|
|
|
|
|
.quick-nav a { flex:0 0 auto; padding:10px 12px; border-radius:999px; background:#222831; border:1px solid var(--border); color:#f3f5f7; text-decoration:none; font-weight:700; font-size:.92rem; }
|
|
|
|
|
.quick-nav a.primary { background:#8f4208; border-color:var(--accent); }
|
|
|
|
|
:root {
|
|
|
|
|
color-scheme: dark;
|
|
|
|
|
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
|
|
|
background:#0e1218;
|
|
|
|
|
color:#f4f7fb;
|
|
|
|
|
scroll-behavior:smooth;
|
|
|
|
|
--accent:#ff8a1c;
|
|
|
|
|
--accent-strong:#ffb45b;
|
|
|
|
|
--complement:#1cc7ff;
|
|
|
|
|
--complement-soft:#123447;
|
|
|
|
|
--panel:#171d25;
|
|
|
|
|
--panel-soft:#202833;
|
|
|
|
|
--panel-quiet:#111720;
|
|
|
|
|
--border:#314050;
|
|
|
|
|
--text-soft:#aebdcc;
|
|
|
|
|
--ok:#53e0a5;
|
|
|
|
|
--warn:#f3c969;
|
|
|
|
|
--bad:#8fb8ff;
|
|
|
|
|
}
|
|
|
|
|
* { box-sizing:border-box; }
|
|
|
|
|
body { margin:0; font-size:15px; background:var(--panel-quiet); }
|
|
|
|
|
h1,h2,h3 { margin:0 0 10px; letter-spacing:0; }
|
|
|
|
|
p { margin:6px 0; }
|
|
|
|
|
header { padding:18px 18px 14px; border-bottom:1px solid var(--border); background:#121821; }
|
|
|
|
|
.topbar { max-width:1480px; margin:0 auto; display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
|
|
|
|
|
.brand { display:grid; gap:5px; min-width:0; }
|
|
|
|
|
.brand-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
|
|
|
.brand-mark { width:34px; height:34px; border-radius:8px; display:grid; place-items:center; background:var(--accent); color:#201204; font-weight:900; }
|
|
|
|
|
header p { color:var(--text-soft); max-width:820px; }
|
|
|
|
|
.status-pill { display:flex; align-items:center; gap:8px; padding:8px 10px; border:1px solid var(--border); border-radius:8px; background:#101722; color:#d9e6f0; white-space:nowrap; }
|
|
|
|
|
.dot { width:9px; height:9px; border-radius:50%; background:var(--complement); box-shadow:0 0 0 3px rgba(28,199,255,.15); }
|
|
|
|
|
.quick-nav { position:sticky; top:0; z-index:10; display:flex; gap:8px; overflow-x:auto; padding:10px 18px; background:rgba(14,18,24,.96); border-bottom:1px solid var(--border); backdrop-filter:blur(8px); }
|
|
|
|
|
.quick-nav a { flex:0 0 auto; min-height:36px; display:grid; place-items:center; padding:8px 12px; border-radius:8px; background:#151c25; border:1px solid var(--border); color:#f3f7fb; text-decoration:none; font-weight:750; font-size:.92rem; }
|
|
|
|
|
.quick-nav a.primary { background:var(--accent); border-color:var(--accent); color:#211204; }
|
|
|
|
|
main { display:grid; grid-template-columns:minmax(270px,.72fr) minmax(0,1.58fr); grid-template-areas:"control board" "control detail" "status status" "guide guide"; gap:12px; padding:12px; max-width:1480px; margin:0 auto; }
|
|
|
|
|
section { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:12px; min-width:0; }
|
|
|
|
|
section:target { outline:2px solid var(--complement); outline-offset:2px; }
|
|
|
|
|
.control-panel { grid-area:control; align-self:start; position:sticky; top:58px; }
|
|
|
|
|
.board-panel { grid-area:board; }
|
|
|
|
|
.detail-panel { grid-area:detail; }
|
|
|
|
|
.status-panel { grid-area:status; }
|
|
|
|
|
.guide-panel { grid-area:guide; }
|
|
|
|
|
.panel-title { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; }
|
|
|
|
|
.toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:10px 0; }
|
|
|
|
|
.toolbar button { margin-top:0; }
|
|
|
|
|
details.collapsible > summary,
|
|
|
|
|
.manual-context > summary,
|
|
|
|
|
.group-panel > summary { cursor:pointer; font-weight:800; color:#eaf1f8; }
|
|
|
|
|
@@ -31,30 +64,30 @@
|
|
|
|
|
.manual-context[open] > summary::after,
|
|
|
|
|
.group-panel[open] > summary::after { content:"zuklappen"; }
|
|
|
|
|
.steps { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:12px; }
|
|
|
|
|
.step { background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; }
|
|
|
|
|
.step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:50%; background:var(--accent); color:#211204; font-weight:800; margin-bottom:8px; }
|
|
|
|
|
.step { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; }
|
|
|
|
|
.step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:8px; background:var(--complement); color:#03151d; font-weight:900; margin-bottom:8px; }
|
|
|
|
|
.step p { margin:5px 0; }
|
|
|
|
|
.ok { color: #73e0a9; }
|
|
|
|
|
.warn { color: #f3c969; }
|
|
|
|
|
.bad { color: #ff8f8f; }
|
|
|
|
|
label { display: block; margin: 9px 0 4px; color: #b9c9d6; }
|
|
|
|
|
select,input,button { box-sizing: border-box; width: 100%; border-radius: 8px; border: 1px solid #3b4b5b; padding: 10px; background: #11161d; color: #fff; font:inherit; min-width:0; }
|
|
|
|
|
.bad { color: var(--bad); }
|
|
|
|
|
label { display:block; margin:9px 0 4px; color:#c3d2df; font-weight:700; }
|
|
|
|
|
select,input,button { width:100%; border-radius:8px; border:1px solid #3b4b5b; padding:10px; background:#111821; color:#fff; font:inherit; min-width:0; }
|
|
|
|
|
select[multiple] { min-height:150px; }
|
|
|
|
|
button { min-height:44px; margin-top: 10px; background: var(--accent); color:#211204; border: 0; font-weight: 800; cursor: pointer; }
|
|
|
|
|
button.secondary { background: #37495c; }
|
|
|
|
|
button.danger { background: #7b3434; }
|
|
|
|
|
button.compact { width:auto; min-width:120px; margin-right:8px; }
|
|
|
|
|
button { min-height:42px; margin-top:10px; background:var(--accent); color:#211204; border:0; font-weight:850; cursor:pointer; }
|
|
|
|
|
button.secondary { background:var(--complement-soft); color:#dff6ff; border:1px solid #22607c; }
|
|
|
|
|
button.danger { background:#2a3441; color:#f2f6fb; border:1px solid #536273; }
|
|
|
|
|
button.compact { width:auto; min-width:112px; margin-right:8px; padding:8px 10px; min-height:36px; }
|
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
|
|
|
|
|
td,th { padding: 8px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; }
|
|
|
|
|
ul { margin: 8px 0; padding-left: 18px; }
|
|
|
|
|
.notice { border-left: 4px solid var(--accent); padding-left: 10px; }
|
|
|
|
|
.notice { border-left:4px solid var(--complement); padding-left:10px; }
|
|
|
|
|
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
|
|
|
|
|
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
|
|
|
|
.chip { padding:4px 8px; border-radius:999px; background:#222831; border:1px solid var(--border); font-size:.85rem; }
|
|
|
|
|
.muted { color:#9fb0be; }
|
|
|
|
|
.card-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
|
|
|
|
|
.actuator-card { background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
|
|
|
|
|
.actuator-card.selected { border-color:var(--accent); box-shadow:0 0 0 1px rgba(255,138,28,.35); }
|
|
|
|
|
.chip { padding:4px 8px; border-radius:8px; background:#222b36; border:1px solid var(--border); font-size:.85rem; }
|
|
|
|
|
.muted { color:var(--text-soft); }
|
|
|
|
|
.card-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:10px; }
|
|
|
|
|
.actuator-card { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
|
|
|
|
|
.actuator-card.selected { border-color:var(--complement); box-shadow:0 0 0 1px rgba(28,199,255,.35); }
|
|
|
|
|
.card-title { display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:8px; }
|
|
|
|
|
.entity-id { overflow-wrap:anywhere; font-weight:800; }
|
|
|
|
|
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:6px; margin:8px 0; }
|
|
|
|
|
@@ -63,16 +96,20 @@
|
|
|
|
|
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
|
|
|
|
|
.actions button { flex:1 1 180px; margin-top:0; }
|
|
|
|
|
.detail-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap; }
|
|
|
|
|
.manual-context { margin-top:12px; background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; }
|
|
|
|
|
.manual-context { margin-top:12px; background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; }
|
|
|
|
|
.inline-controls { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:8px; margin:8px 0; }
|
|
|
|
|
.manual-entry { min-height:80px; resize:vertical; }
|
|
|
|
|
textarea { box-sizing:border-box; width:100%; border-radius:10px; border:1px solid #3b4b5b; padding:12px; background:#101820; color:#fff; font:inherit; }
|
|
|
|
|
textarea { width:100%; border-radius:8px; border:1px solid #3b4b5b; padding:12px; background:#101820; color:#fff; font:inherit; }
|
|
|
|
|
optgroup { color:#cfe0ec; background:#101820; }
|
|
|
|
|
code { color:#cfe0ec; overflow-wrap:anywhere; }
|
|
|
|
|
.empty-state { min-height:140px; display:grid; place-items:center; text-align:center; border:1px dashed var(--border); border-radius:8px; background:#111821; color:var(--text-soft); padding:16px; }
|
|
|
|
|
@media (max-width: 760px) {
|
|
|
|
|
header { padding:18px 14px; }
|
|
|
|
|
header { padding:16px 12px; }
|
|
|
|
|
header h1 { font-size:1.55rem; }
|
|
|
|
|
.topbar { display:grid; }
|
|
|
|
|
.status-pill { width:max-content; }
|
|
|
|
|
main { display:block; padding:8px; }
|
|
|
|
|
.control-panel { position:static; }
|
|
|
|
|
section { margin-bottom:10px; padding:10px; border-radius:8px; }
|
|
|
|
|
.steps { grid-template-columns:1fr; }
|
|
|
|
|
.grid-two { grid-template-columns:1fr; }
|
|
|
|
|
@@ -91,55 +128,31 @@
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<header>
|
|
|
|
|
<h1>SillyHome Next</h1>
|
|
|
|
|
<p>Hier wählst du nur Geräte aus, deren Bedienung SillyHome lernen soll. Sensoren, Zusammenhänge und Modelle werden automatisch verwaltet.</p>
|
|
|
|
|
<p class="notice">Sicherer Start: Zuerst wird nur beobachtet und vorhergesagt. Ohne deine spätere Freigabe wird nichts geschaltet.</p>
|
|
|
|
|
<div class="topbar">
|
|
|
|
|
<div class="brand">
|
|
|
|
|
<div class="brand-row">
|
|
|
|
|
<span class="brand-mark">SH</span>
|
|
|
|
|
<h1>SillyHome Next</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<p>Arbeitsdashboard für gelernte Home-Assistant-Bedienung: Geräte auswählen, Lernstand prüfen, Freigaben steuern.</p>
|
|
|
|
|
<p class="notice">Sicherer Start: Zuerst wird nur beobachtet und vorhergesagt. Ohne deine spätere Freigabe wird nichts geschaltet.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-pill"><span class="dot"></span><span id="load-budget">Startdaten laden ...</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
<nav class="quick-nav" aria-label="Schnellnavigation">
|
|
|
|
|
<a class="primary" href="#choose">Gerät wählen</a>
|
|
|
|
|
<a href="#observed">Beobachtet</a>
|
|
|
|
|
<a href="#detail">Details</a>
|
|
|
|
|
<a href="#status-section">Status</a>
|
|
|
|
|
<a class="primary" href="#choose">Steuerung</a>
|
|
|
|
|
<a href="#observed">Geräte</a>
|
|
|
|
|
<a href="#detail">Freigabe</a>
|
|
|
|
|
<a href="#status-section">System</a>
|
|
|
|
|
<a href="#guide">Ablauf</a>
|
|
|
|
|
</nav>
|
|
|
|
|
<main>
|
|
|
|
|
<section class="wide" id="guide">
|
|
|
|
|
<details class="collapsible">
|
|
|
|
|
<summary><span>So gehst du vor</span></summary>
|
|
|
|
|
<div class="steps">
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">1</span>
|
|
|
|
|
<h3>Aktor auswählen</h3>
|
|
|
|
|
<p><strong>Wo?</strong> Unten im Feld „Gerät auswählen“.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> SillyHome ordnet Raum, Sensoren, Zustände und vorhandene Historie automatisch zu.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">2</span>
|
|
|
|
|
<h3>Wie gewohnt bedienen</h3>
|
|
|
|
|
<p><strong>Wo?</strong> Weiterhin in Home Assistant, an Schaltern oder über deine bisherigen Bedienwege.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> SillyHome lernt deine Handlungen und zeigt Vorhersagen an, schaltet aber noch nicht selbst.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">3</span>
|
|
|
|
|
<h3>Später freigeben</h3>
|
|
|
|
|
<p><strong>Wo?</strong> In den Details des ausgewählten Geräts, sobald genug Verhalten gelernt wurde.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> Erst dann darf SillyHome passende Vorhersagen automatisch ausführen. Die Freigabe kann jederzeit gestoppt werden.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<section class="control-panel" id="choose">
|
|
|
|
|
<div class="panel-title">
|
|
|
|
|
<h2>Steuerung</h2>
|
|
|
|
|
<span class="chip">v1</span>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section id="status-section">
|
|
|
|
|
<h2>System & Cache</h2>
|
|
|
|
|
<p class="muted">Die Startansicht nutzt lokale Summaries und Cache-Daten. Home-Assistant-Discovery lädt im Hintergrund nach.</p>
|
|
|
|
|
<div id="status">Prüfung läuft ...</div>
|
|
|
|
|
<div class="chips" id="status-chips"></div>
|
|
|
|
|
<div id="dashboard-stats" class="metric-grid"></div>
|
|
|
|
|
<button class="secondary" onclick="loadOverview()">Dashboard aktualisieren</button>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section id="choose">
|
|
|
|
|
<h2>1. Gerät zum Lernen auswählen</h2>
|
|
|
|
|
<p class="muted">Wähle eine Lampe, einen Rollladen oder einen anderen unterstützten Aktor. Du wählst keine Sensoren und erstellst keine Regeln.</p>
|
|
|
|
|
<label for="actuator-input">Entitätsname oder Gerät aus Home Assistant</label>
|
|
|
|
|
<input id="actuator-input" list="actuator-options" placeholder="z. B. light.licht_abstellraum" autocomplete="off">
|
|
|
|
|
@@ -168,29 +181,79 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label for="actuator-search">Liste durchsuchen</label>
|
|
|
|
|
<input id="actuator-search" placeholder="Raum, Gerät oder Entity" oninput="renderActuatorSelect()" autocomplete="off">
|
|
|
|
|
<input id="actuator-search" placeholder="Raum, Gerät oder Entity" oninput="renderActuatorSelect()" onfocus="ensureActuatorDiscovery()" autocomplete="off">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<label for="actuator-select">Oder aus Liste wählen</label>
|
|
|
|
|
<select id="actuator-select" onchange="selectActuatorFromList()">
|
|
|
|
|
<option value="">Geräteliste wird geladen ...</option>
|
|
|
|
|
<select id="actuator-select" onchange="selectActuatorFromList()" onfocus="ensureActuatorDiscovery()">
|
|
|
|
|
<option value="">Geräteliste bei Bedarf laden</option>
|
|
|
|
|
</select>
|
|
|
|
|
<button onclick="configureActuator()">Gerät hinzufügen und Beobachtung starten</button>
|
|
|
|
|
<button class="secondary" onclick="ensureActuatorDiscovery()">Geräteliste laden</button>
|
|
|
|
|
<p id="actuator-config-result" class="muted">Noch kein Aktor ausgewählt.</p>
|
|
|
|
|
<details class="manual-context">
|
|
|
|
|
<summary>Vorschläge anzeigen</summary>
|
|
|
|
|
<p class="muted">Vorschläge können Home Assistant stark abfragen und werden deshalb nicht beim Start geladen.</p>
|
|
|
|
|
<button class="secondary" onclick="loadActuatorSuggestions()">Vorschläge laden</button>
|
|
|
|
|
</details>
|
|
|
|
|
<div id="actuator-suggestions" class="card-list"></div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="wide" id="observed">
|
|
|
|
|
<h2>2. Beobachtete Geräte</h2>
|
|
|
|
|
<p class="muted">Öffne „Details“, um Lernfortschritt, aktuelle Vorhersage und den automatisch gefundenen Kontext zu sehen.</p>
|
|
|
|
|
<section class="board-panel" id="observed">
|
|
|
|
|
<div class="panel-title">
|
|
|
|
|
<div>
|
|
|
|
|
<h2>Beobachtete Geräte</h2>
|
|
|
|
|
<p class="muted">Öffne „Details“, um Lernfortschritt, aktuelle Vorhersage und den automatisch gefundenen Kontext zu sehen.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="secondary compact" onclick="loadOverview()">Aktualisieren</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="configured-actuators">Noch nicht geladen.</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="wide" id="detail">
|
|
|
|
|
<h2>3. Lernfortschritt und Freigabe</h2>
|
|
|
|
|
<section class="detail-panel" id="detail">
|
|
|
|
|
<h2>Lernfortschritt und Freigabe</h2>
|
|
|
|
|
<p class="muted">Die Freigabe erscheint erst, wenn genug eindeutig zugeordnete Handlungen gelernt wurden. Vorher bleibt das Gerät sicher im Beobachtungsmodus.</p>
|
|
|
|
|
<div id="actuator-detail" class="muted">Öffne bei einem beobachteten Gerät die Details.</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="status-panel" id="status-section">
|
|
|
|
|
<div class="panel-title">
|
|
|
|
|
<div>
|
|
|
|
|
<h2>System & Cache</h2>
|
|
|
|
|
<p class="muted">Die Startansicht nutzt lokale Summaries und Cache-Daten. Home-Assistant-Discovery lädt erst bei Bedarf.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="secondary compact" onclick="loadStatus()">Status prüfen</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="status">Prüfung läuft ...</div>
|
|
|
|
|
<div class="chips" id="status-chips"></div>
|
|
|
|
|
<div id="dashboard-stats" class="metric-grid"></div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="guide-panel" id="guide">
|
|
|
|
|
<details class="collapsible">
|
|
|
|
|
<summary><span>So gehst du vor</span></summary>
|
|
|
|
|
<div class="steps">
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">1</span>
|
|
|
|
|
<h3>Aktor auswählen</h3>
|
|
|
|
|
<p><strong>Wo?</strong> Links im Feld „Gerät auswählen“.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> SillyHome ordnet Raum, Sensoren, Zustände und vorhandene Historie automatisch zu.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">2</span>
|
|
|
|
|
<h3>Wie gewohnt bedienen</h3>
|
|
|
|
|
<p><strong>Wo?</strong> Weiterhin in Home Assistant, an Schaltern oder über deine bisherigen Bedienwege.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> SillyHome lernt deine Handlungen und zeigt Vorhersagen an, schaltet aber noch nicht selbst.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="step">
|
|
|
|
|
<span class="step-number">3</span>
|
|
|
|
|
<h3>Später freigeben</h3>
|
|
|
|
|
<p><strong>Wo?</strong> In den Details des ausgewählten Geräts, sobald genug Verhalten gelernt wurde.</p>
|
|
|
|
|
<p><strong>Was passiert?</strong> Erst dann darf SillyHome passende Vorhersagen automatisch ausführen. Die Freigabe kann jederzeit gestoppt werden.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
<script>
|
|
|
|
|
const escapeHtml = value => String(value ?? "")
|
|
|
|
|
@@ -206,8 +269,10 @@ let manualContextState = {options: [], selected: new Set()};
|
|
|
|
|
let cachedActuators = null;
|
|
|
|
|
let cachedEntities = null;
|
|
|
|
|
let cachedDiscovery = null;
|
|
|
|
|
let discoveryLoadPromise = null;
|
|
|
|
|
const ACTUATOR_RESULT_LIMIT = 50;
|
|
|
|
|
const STATUS_TIMEOUT_MS = 2500;
|
|
|
|
|
const STATUS_TIMEOUT_MS = 2000;
|
|
|
|
|
const DASHBOARD_TIMEOUT_MS = 4500;
|
|
|
|
|
|
|
|
|
|
function uniqueValues(values) {
|
|
|
|
|
return [...new Set(values.filter(Boolean))];
|
|
|
|
|
@@ -352,21 +417,27 @@ function optionGroups(entities, selectedIds = new Set()) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadOverview() {
|
|
|
|
|
const startedAt = performance.now();
|
|
|
|
|
const budget = document.getElementById("load-budget");
|
|
|
|
|
if (budget) budget.textContent = "Startdaten laden ...";
|
|
|
|
|
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
|
|
|
|
try {
|
|
|
|
|
const dashboard = await api("v1/actuators/dashboard");
|
|
|
|
|
const dashboard = await apiWithTimeout("v1/actuators/dashboard", DASHBOARD_TIMEOUT_MS);
|
|
|
|
|
cachedActuators = dashboard.actuators || [];
|
|
|
|
|
cachedEntities = [];
|
|
|
|
|
renderDashboardStatus(dashboard);
|
|
|
|
|
renderConfiguredActuators();
|
|
|
|
|
if (budget) budget.textContent = `Bereit in ${Math.round(performance.now() - startedAt)} ms`;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
document.getElementById("configured-actuators").innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
|
|
|
|
await loadSummaryData();
|
|
|
|
|
renderConfiguredActuators();
|
|
|
|
|
void loadStatus();
|
|
|
|
|
if (budget) budget.textContent = "Startdaten verzögert";
|
|
|
|
|
try {
|
|
|
|
|
await loadSummaryData();
|
|
|
|
|
renderConfiguredActuators();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
document.getElementById("configured-actuators").innerHTML = "<div class='empty-state'>Startdaten sind gerade nicht verfügbar.</div>";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
void loadActuatorDiscovery();
|
|
|
|
|
void loadActuatorSuggestions();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadStatus() {
|
|
|
|
|
@@ -472,6 +543,21 @@ async function loadActuatorDiscovery() {
|
|
|
|
|
renderActuatorDiscovery();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureActuatorDiscovery() {
|
|
|
|
|
const select = document.getElementById("actuator-select");
|
|
|
|
|
if (cachedDiscovery) {
|
|
|
|
|
renderActuatorDiscovery();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!discoveryLoadPromise) {
|
|
|
|
|
if (select) select.innerHTML = `<option value="">Geräteliste wird geladen ...</option>`;
|
|
|
|
|
discoveryLoadPromise = loadActuatorDiscovery().finally(() => {
|
|
|
|
|
discoveryLoadPromise = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await discoveryLoadPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderActuatorDiscovery() {
|
|
|
|
|
const options = document.getElementById("actuator-options");
|
|
|
|
|
const select = document.getElementById("actuator-select");
|
|
|
|
|
@@ -607,7 +693,11 @@ async function configureActuator() {
|
|
|
|
|
|| document.getElementById("actuator-select").value.trim()
|
|
|
|
|
);
|
|
|
|
|
const result = document.getElementById("actuator-config-result");
|
|
|
|
|
if (!actuatorId) return;
|
|
|
|
|
if (!actuatorId) {
|
|
|
|
|
await ensureActuatorDiscovery();
|
|
|
|
|
result.textContent = "Wähle ein Gerät aus der geladenen Liste oder trage eine Entity-ID ein.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
result.textContent = "Kontext wird automatisch analysiert ...";
|
|
|
|
|
try {
|
|
|
|
|
const record = await api("v1/actuators", {
|
|
|
|
|
@@ -685,12 +775,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|
|
|
|
currentActuatorId = actuatorId;
|
|
|
|
|
const box = document.getElementById("actuator-detail");
|
|
|
|
|
try {
|
|
|
|
|
let record;
|
|
|
|
|
try {
|
|
|
|
|
record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}/related-automations/refresh`, {method: "POST"});
|
|
|
|
|
} catch (_) {
|
|
|
|
|
record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}`);
|
|
|
|
|
}
|
|
|
|
|
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}`);
|
|
|
|
|
await loadContextOptions(actuatorId);
|
|
|
|
|
const contexts = [
|
|
|
|
|
record.assignment.selected_numeric_entity_id,
|
|
|
|
|
@@ -820,6 +905,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|
|
|
|
</div>
|
|
|
|
|
<h3>Passende Home-Assistant-Automationen</h3>
|
|
|
|
|
<p class="muted">Bei einer Übernahme pausiert SillyHome diese Automationen. Beim Stoppen können sie gezielt fortgesetzt werden.</p>
|
|
|
|
|
<button class="secondary compact" onclick="refreshRelatedAutomations('${escapeHtml(record.actuator_entity_id)}')">Automationen neu suchen</button>
|
|
|
|
|
${automationControls}
|
|
|
|
|
<h3>Welche Zusammenhänge automatisch verwendet werden</h3>
|
|
|
|
|
${evidence ? `<ul>${evidence}</ul>` : "<p class='warn'>Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.</p>"}
|
|
|
|
|
@@ -972,6 +1058,17 @@ async function setRelatedAutomation(actuatorId, automationEntityId, enabled) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshRelatedAutomations(actuatorId) {
|
|
|
|
|
try {
|
|
|
|
|
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/related-automations/refresh`, {method: "POST"});
|
|
|
|
|
invalidateDashboardCache();
|
|
|
|
|
await loadConfiguredActuators();
|
|
|
|
|
await showActuator(actuatorId, "Passende Home-Assistant-Automationen neu geprüft.");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
alert(error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeActuator(actuatorId) {
|
|
|
|
|
if (!confirm(`${actuatorId} aus SillyHome entfernen?`)) return;
|
|
|
|
|
try {
|
|
|
|
|
|