Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d070fc9ca | |||
| ba15cc4d83 | |||
| da51ac2063 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.2 - 2026-06-14
|
||||||
|
- Add-on-Build invalidiert den Docker-Cache bei jeder Versionsänderung, damit
|
||||||
|
Versionsmetadaten und tatsächlich ausgelieferter Anwendungscode übereinstimmen
|
||||||
|
- Korrigierte Ingress-Oberfläche aus 0.5.1 dadurch erstmals zuverlässig ausgeliefert
|
||||||
|
|
||||||
|
## 0.5.1 - 2026-06-14
|
||||||
|
- Technische Modell-, Intervall- und Sicherheitsparameter aus der normalen
|
||||||
|
Home-Assistant-Add-on-Konfiguration entfernt; sichere Standardwerte bleiben aktiv
|
||||||
|
- Ingress um einen klaren Ablauf mit Aktorauswahl, Beobachtungsphase und späterer
|
||||||
|
Ausführungsfreigabe ergänzt
|
||||||
|
- Bedienelemente und Diagnosen in verständlicher Alltagssprache erklärt
|
||||||
|
|
||||||
## 0.5.0 - 2026-06-14
|
## 0.5.0 - 2026-06-14
|
||||||
- Ingress auf reine Aktorauswahl, automatischen Lernstatus und Vorhersagen reduziert
|
- Ingress auf reine Aktorauswahl, automatischen Lernstatus und Vorhersagen reduziert
|
||||||
- Automatische Kontextzuordnung ohne Sensor-Overrides oder Review-Blockade
|
- Automatische Kontextzuordnung ohne Sensor-Overrides oder Review-Blockade
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_NO_CACHE_DIR=1
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
# The add-on version changes for every release. Copying its config before the
|
||||||
|
# clone makes Docker invalidate the application layer instead of reusing old code.
|
||||||
|
COPY config.yaml /tmp/addon-config.yaml
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends git \
|
&& apt-get install -y --no-install-recommends git \
|
||||||
&& git clone --depth 1 --branch main \
|
&& git clone --depth 1 --branch main \
|
||||||
http://192.168.6.31:3000/pino/sillyhome-next.git /app \
|
http://192.168.6.31:3000/pino/sillyhome-next.git /app \
|
||||||
&& python -m pip install --upgrade pip \
|
&& python -m pip install --upgrade pip \
|
||||||
&& python -m pip install /app \
|
&& python -m pip install /app \
|
||||||
&& rm -rf /var/lib/apt/lists/* /app/.git
|
&& rm -rf /var/lib/apt/lists/* /app/.git /tmp/addon-config.yaml
|
||||||
|
|
||||||
COPY run.sh /run.sh
|
COPY run.sh /run.sh
|
||||||
RUN chmod 0755 /run.sh
|
RUN chmod 0755 /run.sh
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Next
|
name: SillyHome Next
|
||||||
version: "0.5.0"
|
version: "0.5.2"
|
||||||
slug: sillyhome_next
|
slug: sillyhome_next
|
||||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||||
@@ -16,28 +16,6 @@ panel_admin: true
|
|||||||
homeassistant_api: true
|
homeassistant_api: true
|
||||||
hassio_api: false
|
hassio_api: false
|
||||||
auth_api: false
|
auth_api: false
|
||||||
options:
|
|
||||||
history_days: 14
|
|
||||||
min_training_points: 24
|
|
||||||
retrain_stale_hours: 24
|
|
||||||
reconcile_interval_seconds: 900
|
|
||||||
min_behavior_actions: 3
|
|
||||||
prediction_confidence: 0.82
|
|
||||||
prediction_window_minutes: 30
|
|
||||||
prediction_interval_seconds: 60
|
|
||||||
execution_cooldown_seconds: 900
|
|
||||||
timezone: Europe/Berlin
|
|
||||||
schema:
|
|
||||||
history_days: "int(1,31)"
|
|
||||||
min_training_points: "int(2,10000)"
|
|
||||||
retrain_stale_hours: "int(1,720)"
|
|
||||||
reconcile_interval_seconds: "int(60,86400)"
|
|
||||||
min_behavior_actions: "int(2,100)"
|
|
||||||
prediction_confidence: "float(0.5,0.99)"
|
|
||||||
prediction_window_minutes: "int(5,120)"
|
|
||||||
prediction_interval_seconds: "int(30,3600)"
|
|
||||||
execution_cooldown_seconds: "int(60,86400)"
|
|
||||||
timezone: "str"
|
|
||||||
map:
|
map:
|
||||||
- type: addon_config
|
- type: addon_config
|
||||||
read_only: false
|
read_only: false
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SillyHome Next API",
|
title="SillyHome Next API",
|
||||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||||
version="0.5.0",
|
version="0.5.2",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
app.state.settings = load_settings()
|
app.state.settings = load_settings()
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
main { display: grid; grid-template-columns: repeat(auto-fit,minmax(320px,1fr)); gap: 14px; padding: 14px; }
|
main { display: grid; grid-template-columns: repeat(auto-fit,minmax(320px,1fr)); gap: 14px; padding: 14px; }
|
||||||
section { background: #18212b; border: 1px solid #2d3a47; border-radius: 12px; padding: 16px; }
|
section { background: #18212b; border: 1px solid #2d3a47; border-radius: 12px; padding: 16px; }
|
||||||
.wide { grid-column: 1 / -1; }
|
.wide { grid-column: 1 / -1; }
|
||||||
|
.steps { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; }
|
||||||
|
.step { background:#111a23; border:1px solid #31404d; border-radius:10px; padding:14px; }
|
||||||
|
.step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:50%; background:#23715b; font-weight:700; margin-bottom:8px; }
|
||||||
|
.step p { margin:5px 0; }
|
||||||
.ok { color: #66dfa9; }
|
.ok { color: #66dfa9; }
|
||||||
.warn { color: #f3c969; }
|
.warn { color: #f3c969; }
|
||||||
.bad { color: #ff8f8f; }
|
.bad { color: #ff8f8f; }
|
||||||
@@ -34,34 +38,61 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>SillyHome Next</h1>
|
<h1>SillyHome Next</h1>
|
||||||
<p>Du wählst nur die Aktoren. SillyHome findet Kontext, lernt Gewohnheiten und trifft Vorhersagen im Shadow-Modus.</p>
|
<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">Geschaltet wird erst nach deiner ausdrücklichen Freigabe pro Aktor.</p>
|
<p class="notice">Sicherer Start: Zuerst wird nur beobachtet und vorhergesagt. Ohne deine spätere Freigabe wird nichts geschaltet.</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
<section class="wide">
|
||||||
|
<h2>So gehst du vor</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Systemstatus</h2>
|
<h2>Systemstatus</h2>
|
||||||
|
<p class="muted">Zeigt, ob Verbindung, Lernsystem und automatische Prüfungen funktionieren. Hier musst du normalerweise nichts einstellen.</p>
|
||||||
<div id="status">Prüfung läuft ...</div>
|
<div id="status">Prüfung läuft ...</div>
|
||||||
<div class="chips" id="status-chips"></div>
|
<div class="chips" id="status-chips"></div>
|
||||||
<button class="secondary" onclick="loadOverview()">Status aktualisieren</button>
|
<button class="secondary" onclick="loadOverview()">Status aktualisieren</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Aktor freigeben</h2>
|
<h2>1. Gerät zum Lernen auswählen</h2>
|
||||||
<p class="muted">Nach der Auswahl analysiert SillyHome automatisch passende Sensoren, Zustände und Historie.</p>
|
<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-select">Home-Assistant-Aktor</label>
|
<label for="actuator-select">Gerät aus Home Assistant</label>
|
||||||
<select id="actuator-select"></select>
|
<select id="actuator-select"></select>
|
||||||
<button onclick="configureActuator()">Auswählen und Lernen starten</button>
|
<button onclick="configureActuator()">Gerät hinzufügen und Beobachtung starten</button>
|
||||||
<p id="actuator-config-result" class="muted">Noch kein Aktor ausgewählt.</p>
|
<p id="actuator-config-result" class="muted">Noch kein Aktor ausgewählt.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="wide">
|
<section class="wide">
|
||||||
<h2>Ausgewählte Aktoren</h2>
|
<h2>2. Beobachtete Geräte</h2>
|
||||||
|
<p class="muted">Öffne „Details“, um Lernfortschritt, aktuelle Vorhersage und den automatisch gefundenen Kontext zu sehen.</p>
|
||||||
<div id="configured-actuators">Noch nicht geladen.</div>
|
<div id="configured-actuators">Noch nicht geladen.</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="wide">
|
<section class="wide">
|
||||||
<h2>Automatisch erkannter Lernkontext</h2>
|
<h2>3. Lernfortschritt und Freigabe</h2>
|
||||||
<div id="actuator-detail" class="muted">Wähle einen Aktor aus der Liste.</div>
|
<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>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
@@ -120,7 +151,7 @@ async function loadOverview() {
|
|||||||
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
||||||
`<span class="chip">Lernsystem: ${escapeHtml(ml.status)}</span>`,
|
`<span class="chip">Lernsystem: ${escapeHtml(ml.status)}</span>`,
|
||||||
`<span class="chip">Aktoren: ${actuators.length}</span>`,
|
`<span class="chip">Aktoren: ${actuators.length}</span>`,
|
||||||
`<span class="chip">Aktive Modelle: ${reconciliation.trained_models}</span>`,
|
`<span class="chip">Lernbereite Geräte: ${reconciliation.trained_models}</span>`,
|
||||||
].join("");
|
].join("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||||
@@ -171,7 +202,7 @@ async function loadConfiguredActuators() {
|
|||||||
const rows = await api("v1/actuators");
|
const rows = await api("v1/actuators");
|
||||||
box.innerHTML = rows.length ? `
|
box.innerHTML = rows.length ? `
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Aktor</th><th>Verhaltensmodell</th><th>Handlungen</th><th>Vorhersage</th><th></th></tr>
|
<tr><th>Gerät</th><th>Lernstatus</th><th>Gelernte Handlungen</th><th>Letzte Vorhersage</th><th>Aktionen</th></tr>
|
||||||
${rows.map(record => `
|
${rows.map(record => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(record.actuator_entity_id)}</td>
|
<td>${escapeHtml(record.actuator_entity_id)}</td>
|
||||||
@@ -216,26 +247,27 @@ async function showActuator(actuatorId) {
|
|||||||
<div>
|
<div>
|
||||||
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
||||||
<p><strong>Status:</strong> <span class="${statusClass(record)}">${escapeHtml(lifecycleLabel(record))}</span></p>
|
<p><strong>Status:</strong> <span class="${statusClass(record)}">${escapeHtml(lifecycleLabel(record))}</span></p>
|
||||||
<p><strong>Zuordnung:</strong> automatisch</p>
|
<p><strong>Kontextzuordnung:</strong> automatisch erledigt</p>
|
||||||
<p><strong>Sicherheit:</strong> ${Math.round(record.assignment.confidence * 100)} %</p>
|
<p><strong>Zuordnungssicherheit:</strong> ${Math.round(record.assignment.confidence * 100)} %</p>
|
||||||
<p><strong>Bewertung:</strong> ${escapeHtml(record.assignment.reason)}</p>
|
<p class="muted">Dieser Wert beschreibt, wie sicher Raum, Sensoren und Zustände zu diesem Gerät passen.</p>
|
||||||
|
<p><strong>Ergebnis:</strong> ${escapeHtml(record.assignment.reason)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Verhaltensmodell</h3>
|
<h3>Lernfortschritt</h3>
|
||||||
<p><strong>Modus:</strong> ${escapeHtml(behaviorLabel(record))}</p>
|
<p><strong>Betriebsart:</strong> ${escapeHtml(behaviorLabel(record))}</p>
|
||||||
<p><strong>Gelernte Handlungen:</strong> ${record.behavior.sample_count}</p>
|
<p><strong>Gelernte Handlungen:</strong> ${record.behavior.sample_count}</p>
|
||||||
<p><strong>Davon eindeutig Benutzer:</strong> ${record.behavior.high_confidence_sample_count}</p>
|
<p><strong>Davon eindeutig Benutzer:</strong> ${record.behavior.high_confidence_sample_count}</p>
|
||||||
<p><strong>Letztes Training:</strong> ${escapeHtml(record.behavior.last_trained_at || "noch nicht")}</p>
|
<p><strong>Letztes Training:</strong> ${escapeHtml(record.behavior.last_trained_at || "noch nicht")}</p>
|
||||||
<p><strong>Status:</strong> ${escapeHtml(record.behavior.reason)}</p>
|
<p><strong>Was noch passiert:</strong> ${escapeHtml(record.behavior.reason)}</p>
|
||||||
${activationButton}
|
${activationButton}
|
||||||
<button class="secondary" onclick="evaluateActuator('${escapeHtml(record.actuator_entity_id)}')">Vorhersage jetzt prüfen</button>
|
<button class="secondary" onclick="evaluateActuator('${escapeHtml(record.actuator_entity_id)}')">Vorhersage jetzt prüfen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>Aktuelle Vorhersage</h3>
|
<h3>Was SillyHome aktuell vorhersagt</h3>
|
||||||
${prediction
|
${prediction
|
||||||
? `<p><strong>${escapeHtml(prediction.target_state)}</strong> mit ${Math.round(prediction.confidence * 100)} % Sicherheit. ${escapeHtml(prediction.reason)} ${prediction.executed ? "<span class='ok'>Ausgeführt.</span>" : "<span class='muted'>Nicht ausgeführt.</span>"}</p>`
|
? `<p><strong>${escapeHtml(prediction.target_state)}</strong> mit ${Math.round(prediction.confidence * 100)} % Sicherheit. ${escapeHtml(prediction.reason)} ${prediction.executed ? "<span class='ok'>Ausgeführt.</span>" : "<span class='muted'>Nicht ausgeführt.</span>"}</p>`
|
||||||
: "<p class='muted'>Aktuell ist kein gelerntes Handlungsmuster fällig.</p>"}
|
: "<p class='muted'>Aktuell ist kein gelerntes Handlungsmuster fällig.</p>"}
|
||||||
<h3>Automatisch verwendeter Kontext</h3>
|
<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>"}
|
${evidence ? `<ul>${evidence}</ul>` : "<p class='warn'>Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.</p>"}
|
||||||
`;
|
`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -276,7 +308,7 @@ async function removeActuator(actuatorId) {
|
|||||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}`, {method: "DELETE"});
|
await api(`v1/actuators/${encodeURIComponent(actuatorId)}`, {method: "DELETE"});
|
||||||
if (currentActuatorId === actuatorId) {
|
if (currentActuatorId === actuatorId) {
|
||||||
currentActuatorId = null;
|
currentActuatorId = null;
|
||||||
document.getElementById("actuator-detail").textContent = "Wähle einen Aktor aus der Liste.";
|
document.getElementById("actuator-detail").textContent = "Öffne bei einem beobachteten Gerät die Details.";
|
||||||
}
|
}
|
||||||
await loadOverview();
|
await loadOverview();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-next"
|
name = "sillyhome-next"
|
||||||
version = "0.5.0"
|
version = "0.5.2"
|
||||||
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
18
tests/test_addon_config.py
Normal file
18
tests/test_addon_config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_addon_does_not_expose_internal_learning_parameters() -> None:
|
||||||
|
config = Path("addon/config.yaml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "\noptions:" not in config
|
||||||
|
assert "\nschema:" not in config
|
||||||
|
assert "prediction_confidence" not in config
|
||||||
|
assert "execution_cooldown_seconds" not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_addon_version_invalidates_application_build_layer() -> None:
|
||||||
|
dockerfile = Path("addon/Dockerfile").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
config_copy = dockerfile.index("COPY config.yaml /tmp/addon-config.yaml")
|
||||||
|
repository_clone = dockerfile.index("git clone --depth 1 --branch main")
|
||||||
|
assert config_copy < repository_clone
|
||||||
@@ -9,7 +9,10 @@ def test_dashboard_is_served_at_root() -> None:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "SillyHome Next" in response.text
|
assert "SillyHome Next" in response.text
|
||||||
assert "Aktor freigeben" in response.text
|
assert "So gehst du vor" in response.text
|
||||||
assert "ausdrücklichen Freigabe pro Aktor" in response.text
|
assert "Gerät zum Lernen auswählen" in response.text
|
||||||
|
assert "Wie gewohnt bedienen" in response.text
|
||||||
|
assert "Ohne deine spätere Freigabe wird nichts geschaltet" in response.text
|
||||||
|
assert "Du wählst keine Sensoren und erstellst keine Regeln" in response.text
|
||||||
assert "Automation-Entwurf" not in response.text
|
assert "Automation-Entwurf" not in response.text
|
||||||
assert "Manuelle Overrides" not in response.text
|
assert "Manuelle Overrides" not in response.text
|
||||||
|
|||||||
Reference in New Issue
Block a user