Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77f328c4a8 | |||
| 7ad97320a2 | |||
| 58d3126a35 | |||
| 1c5eab14b6 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.6.2 - 2026-06-14
|
||||||
|
- Eindeutig im Home-Assistant-Logbuch erkannte Automationen und Scripts zählen für
|
||||||
|
Lernen und Freigabe gleichwertig wie manuelle Bedienungen
|
||||||
|
- Automationsmuster erhalten dieselbe Modellgewichtung wie manuelle Handlungen
|
||||||
|
- Oberfläche zeigt die gemeinsame Zahl als `eindeutig geregelt`; eine
|
||||||
|
ausdrückliche Aktivierung pro Aktor bleibt weiterhin erforderlich
|
||||||
|
|
||||||
|
## 0.6.1 - 2026-06-14
|
||||||
|
- Manuelle Prüfung als `Aktuelle Situation auswerten` eindeutig von Simulation
|
||||||
|
oder Aktorschaltung abgegrenzt
|
||||||
|
- Sichtbare Rückmeldung mit Prüfzeitpunkt, vorhergesagtem Zustand und Sicherheit
|
||||||
|
oder klarem Hinweis auf einen fehlenden frischen Sensorwechsel
|
||||||
|
|
||||||
## 0.6.0 - 2026-06-14
|
## 0.6.0 - 2026-06-14
|
||||||
- Kausales Shadow-Lernen erkennt frische Kontextwechsel unmittelbar vor einer
|
- Kausales Shadow-Lernen erkennt frische Kontextwechsel unmittelbar vor einer
|
||||||
Aktorhandlung, etwa `Tür geschlossen → offen` vor `Licht aus → an`
|
Aktorhandlung, etwa `Tür geschlossen → offen` vor `Licht aus → an`
|
||||||
|
|||||||
@@ -109,8 +109,9 @@ Lernentscheidungen erfolgen automatisch.
|
|||||||
System das lokale Modell automatisch.
|
System das lokale Modell automatisch.
|
||||||
5. Vorhersagen laufen zunächst ausschließlich im Shadow-Modus.
|
5. Vorhersagen laufen zunächst ausschließlich im Shadow-Modus.
|
||||||
6. Erst nach ausdrücklicher Freigabe pro Aktor werden hochkonfidente,
|
6. Erst nach ausdrücklicher Freigabe pro Aktor werden hochkonfidente,
|
||||||
erlaubte Zustände geschaltet. Eigene Schaltungen und erkannte
|
erlaubte Zustände geschaltet. Eindeutig im HA-Logbuch erkannte Automationen
|
||||||
HA-Automationen werden nicht als Nutzerhandlungen zurückgelernt.
|
und Scripts zählen dabei gleichwertig wie manuelle Bedienungen. Eigene
|
||||||
|
Schaltungen von SillyHome werden nicht zurückgelernt.
|
||||||
|
|
||||||
Vor einem Update sollte in Home Assistant unter **Einstellungen → System → Backups**
|
Vor einem Update sollte in Home Assistant unter **Einstellungen → System → Backups**
|
||||||
eine Teil-Sicherung des Add-ons erstellt werden. Zur Wiederherstellung das gewünschte
|
eine Teil-Sicherung des Add-ons erstellt werden. Zur Wiederherstellung das gewünschte
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Next
|
name: SillyHome Next
|
||||||
version: "0.6.0"
|
version: "0.6.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
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ class BehaviorEngine:
|
|||||||
logbook=logbook,
|
logbook=logbook,
|
||||||
own_executions=record.behavior.execution_events,
|
own_executions=record.behavior.execution_events,
|
||||||
)
|
)
|
||||||
high_confidence = sum(1 for pattern in patterns if pattern.source == "user")
|
trusted_actions = sum(
|
||||||
|
1 for pattern in patterns if pattern.source in {"user", "automation"}
|
||||||
|
)
|
||||||
status = (
|
status = (
|
||||||
BehaviorStatus.TRAINED
|
BehaviorStatus.TRAINED
|
||||||
if len(patterns) >= self._settings.min_behavior_actions
|
if len(patterns) >= self._settings.min_behavior_actions
|
||||||
@@ -142,7 +144,7 @@ class BehaviorEngine:
|
|||||||
update={
|
update={
|
||||||
"status": status,
|
"status": status,
|
||||||
"sample_count": len(patterns),
|
"sample_count": len(patterns),
|
||||||
"high_confidence_sample_count": high_confidence,
|
"high_confidence_sample_count": trusted_actions,
|
||||||
"patterns": patterns[-_MAX_PATTERNS:],
|
"patterns": patterns[-_MAX_PATTERNS:],
|
||||||
"last_trained_at": now,
|
"last_trained_at": now,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
@@ -296,8 +298,8 @@ class BehaviorEngine:
|
|||||||
< self._settings.min_behavior_actions
|
< self._settings.min_behavior_actions
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Für die Freigabe fehlen noch eindeutig dir zugeordnete Handlungen. "
|
"Für die Freigabe fehlen noch eindeutig zugeordnete manuelle "
|
||||||
"Bediene den Aktor einige Male über Home Assistant."
|
"oder automatisierte Handlungen."
|
||||||
)
|
)
|
||||||
mode = BehaviorMode.ACTIVE
|
mode = BehaviorMode.ACTIVE
|
||||||
approved_at = now
|
approved_at = now
|
||||||
@@ -338,8 +340,6 @@ class BehaviorEngine:
|
|||||||
context_ids,
|
context_ids,
|
||||||
point.timestamp,
|
point.timestamp,
|
||||||
)
|
)
|
||||||
if source == "automation" and trigger is None:
|
|
||||||
continue
|
|
||||||
contexts = {
|
contexts = {
|
||||||
entity_id: state
|
entity_id: state
|
||||||
for entity_id in context_ids
|
for entity_id in context_ids
|
||||||
@@ -520,7 +520,7 @@ def _action_source(
|
|||||||
if nearest.context_user_id:
|
if nearest.context_user_id:
|
||||||
return "user", 1.0
|
return "user", 1.0
|
||||||
if nearest.context_domain in _AUTOMATION_CONTEXT_DOMAINS:
|
if nearest.context_domain in _AUTOMATION_CONTEXT_DOMAINS:
|
||||||
return "automation", 0.1
|
return "automation", 1.0
|
||||||
return "physical_or_unknown", 0.7
|
return "physical_or_unknown", 0.7
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ async function loadConfiguredActuators() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showActuator(actuatorId) {
|
async function showActuator(actuatorId, evaluationMessage = "") {
|
||||||
currentActuatorId = actuatorId;
|
currentActuatorId = actuatorId;
|
||||||
const box = document.getElementById("actuator-detail");
|
const box = document.getElementById("actuator-detail");
|
||||||
try {
|
try {
|
||||||
@@ -239,20 +239,20 @@ async function showActuator(actuatorId) {
|
|||||||
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
|
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
|
||||||
.join("");
|
.join("");
|
||||||
const prediction = record.behavior.prediction;
|
const prediction = record.behavior.prediction;
|
||||||
const requiredUserActions = 3;
|
const requiredTrustedActions = 3;
|
||||||
const learnedAutomationActions = record.behavior.patterns.filter(
|
const learnedAutomationActions = record.behavior.patterns.filter(
|
||||||
pattern => pattern.source === "automation",
|
pattern => pattern.source === "automation",
|
||||||
).length;
|
).length;
|
||||||
const missingUserActions = Math.max(
|
const missingTrustedActions = Math.max(
|
||||||
0,
|
0,
|
||||||
requiredUserActions - record.behavior.high_confidence_sample_count,
|
requiredTrustedActions - record.behavior.high_confidence_sample_count,
|
||||||
);
|
);
|
||||||
const activationButton = record.behavior.mode === "active"
|
const activationButton = record.behavior.mode === "active"
|
||||||
? `<button class="danger" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false)">Autonomes Schalten stoppen</button>`
|
? `<button class="danger" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false)">Autonomes Schalten stoppen</button>`
|
||||||
: record.behavior.status === "trained" && missingUserActions === 0
|
: record.behavior.status === "trained" && missingTrustedActions === 0
|
||||||
? `<button onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', true)">Lernen und Schalten freigeben</button>`
|
? `<button onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', true)">Lernen und Schalten freigeben</button>`
|
||||||
: record.behavior.status === "trained"
|
: record.behavior.status === "trained"
|
||||||
? `<p class='muted'>Freigabe noch gesperrt: ${missingUserActions} eindeutig manuelle Bedienung${missingUserActions === 1 ? "" : "en"} fehlen. Bediene das Licht dafür direkt über Home Assistant.</p>`
|
? `<p class='muted'>Freigabe noch gesperrt: ${missingTrustedActions} eindeutig zugeordnete manuelle oder automatisierte Handlung${missingTrustedActions === 1 ? "" : "en"} fehlen.</p>`
|
||||||
: "<p class='muted'>Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.</p>";
|
: "<p class='muted'>Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.</p>";
|
||||||
box.innerHTML = `
|
box.innerHTML = `
|
||||||
<div class="grid-two">
|
<div class="grid-two">
|
||||||
@@ -268,12 +268,14 @@ async function showActuator(actuatorId) {
|
|||||||
<h3>Lernfortschritt</h3>
|
<h3>Lernfortschritt</h3>
|
||||||
<p><strong>Betriebsart:</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 geregelt:</strong> ${record.behavior.high_confidence_sample_count}</p>
|
||||||
<p><strong>Davon erkannte HA-Automationen:</strong> ${learnedAutomationActions}</p>
|
<p><strong>Davon erkannte HA-Automationen:</strong> ${learnedAutomationActions}</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>Was noch passiert:</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)}')">Aktuelle Situation auswerten</button>
|
||||||
|
<p class="muted">Die Prüfung simuliert keinen Sensorwechsel und schaltet keinen Aktor.</p>
|
||||||
|
${evaluationMessage ? `<p class="ok">${escapeHtml(evaluationMessage)}</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>Was SillyHome aktuell vorhersagt</h3>
|
<h3>Was SillyHome aktuell vorhersagt</h3>
|
||||||
@@ -290,9 +292,18 @@ async function showActuator(actuatorId) {
|
|||||||
|
|
||||||
async function evaluateActuator(actuatorId) {
|
async function evaluateActuator(actuatorId) {
|
||||||
try {
|
try {
|
||||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/evaluate`, {method: "POST"});
|
const record = await api(
|
||||||
|
`v1/actuators/${encodeURIComponent(actuatorId)}/evaluate`,
|
||||||
|
{method: "POST"},
|
||||||
|
);
|
||||||
|
const checkedAt = new Date(
|
||||||
|
record.behavior.last_evaluated_at || Date.now(),
|
||||||
|
).toLocaleString("de-DE");
|
||||||
|
const message = record.behavior.prediction
|
||||||
|
? `Prüfung ${checkedAt}: ${record.behavior.prediction.target_state} mit ${Math.round(record.behavior.prediction.confidence * 100)} % vorhergesagt.`
|
||||||
|
: `Prüfung ${checkedAt}: Kein frischer passender Sensorwechsel erkannt; aktuell ist keine Aktion fällig.`;
|
||||||
await loadConfiguredActuators();
|
await loadConfiguredActuators();
|
||||||
await showActuator(actuatorId);
|
await showActuator(actuatorId, message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ Für jeden Aktor lädt SillyHome Next:
|
|||||||
- automatisch zugeordnete Mess- und Kontext-Entities
|
- automatisch zugeordnete Mess- und Kontext-Entities
|
||||||
- deren Zustand zum Zeitpunkt der Handlung
|
- deren Zustand zum Zeitpunkt der Handlung
|
||||||
|
|
||||||
Eindeutig einem Home-Assistant-Benutzer zugeordnete Handlungen erhalten das
|
Eindeutig einem Home-Assistant-Benutzer zugeordnete Handlungen und im Logbuch
|
||||||
höchste Gewicht. Erkannte Automations- und Script-Aktionen werden verworfen.
|
erkannte Automations- oder Script-Aktionen erhalten das höchste Gewicht.
|
||||||
Physische oder nicht eindeutig zuordenbare Bedienungen dürfen das
|
Physische oder nicht eindeutig zuordenbare Bedienungen dürfen das Shadow-Modell
|
||||||
Shadow-Modell ergänzen, reichen allein aber nicht zur Aktivierung.
|
ergänzen, reichen allein aber nicht zur Aktivierung.
|
||||||
|
|
||||||
## Modell
|
## Modell
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ Kontext. Mehrere passende historische Handlungen erhöhen die Confidence.
|
|||||||
2. `shadow`: Modell ist trainiert; Vorhersagen werden angezeigt, aber nicht ausgeführt.
|
2. `shadow`: Modell ist trainiert; Vorhersagen werden angezeigt, aber nicht ausgeführt.
|
||||||
3. `active`: Nutzer hat den Aktor ausdrücklich freigegeben.
|
3. `active`: Nutzer hat den Aktor ausdrücklich freigegeben.
|
||||||
|
|
||||||
Die Aktivierung verlangt genügend eindeutig einem Benutzer zugeordnete
|
Die Aktivierung verlangt genügend eindeutig zugeordnete manuelle oder
|
||||||
Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler Domains:
|
automatisierte Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler Domains:
|
||||||
`light`, `switch`, `fan`, `humidifier` und `cover`.
|
`light`, `switch`, `fan`, `humidifier` und `cover`.
|
||||||
|
|
||||||
## Schutzmechanismen
|
## Schutzmechanismen
|
||||||
@@ -48,4 +48,4 @@ Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler Domains:
|
|||||||
- keine Ausführung bei bereits erreichtem Zielzustand
|
- keine Ausführung bei bereits erreichtem Zielzustand
|
||||||
- keine Ausführung unbekannter Zustände oder riskanter Domains
|
- keine Ausführung unbekannter Zustände oder riskanter Domains
|
||||||
- eigene Schaltungen werden beim nächsten Training herausgefiltert
|
- eigene Schaltungen werden beim nächsten Training herausgefiltert
|
||||||
- bekannte Automation-/Script-Aktionen werden nicht als Nutzerverhalten gelernt
|
- Automation-/Script-Aktionen zählen nur bei eindeutiger Herkunft im HA-Logbuch
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ def test_engine_trains_predicts_in_shadow_and_executes_only_after_approval(
|
|||||||
shadow = engine.evaluate("light.office")
|
shadow = engine.evaluate("light.office")
|
||||||
|
|
||||||
assert trained.behavior.status is BehaviorStatus.TRAINED
|
assert trained.behavior.status is BehaviorStatus.TRAINED
|
||||||
assert trained.behavior.sample_count == 3
|
assert trained.behavior.sample_count == 6
|
||||||
assert trained.behavior.high_confidence_sample_count == 3
|
assert trained.behavior.high_confidence_sample_count == 6
|
||||||
assert shadow.behavior.mode is BehaviorMode.SHADOW
|
assert shadow.behavior.mode is BehaviorMode.SHADOW
|
||||||
assert shadow.behavior.prediction is not None
|
assert shadow.behavior.prediction is not None
|
||||||
assert shadow.behavior.prediction.target_state == "on"
|
assert shadow.behavior.prediction.target_state == "on"
|
||||||
@@ -179,17 +179,27 @@ def test_engine_trains_predicts_in_shadow_and_executes_only_after_approval(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_engine_excludes_known_automation_actions(tmp_path: Path) -> None:
|
def test_engine_counts_known_automation_actions_like_manual_actions(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
||||||
engine, _ = _engine(tmp_path, now)
|
engine, _ = _engine(tmp_path, now)
|
||||||
|
|
||||||
trained = engine.train("light.office")
|
trained = engine.train("light.office")
|
||||||
|
|
||||||
assert {pattern.target_state for pattern in trained.behavior.patterns} == {"on"}
|
assert {pattern.target_state for pattern in trained.behavior.patterns} == {
|
||||||
assert {pattern.source for pattern in trained.behavior.patterns} == {"user"}
|
"on",
|
||||||
|
"off",
|
||||||
|
}
|
||||||
|
assert {pattern.source for pattern in trained.behavior.patterns} == {
|
||||||
|
"user",
|
||||||
|
"automation",
|
||||||
|
}
|
||||||
|
assert trained.behavior.high_confidence_sample_count == 6
|
||||||
|
assert {pattern.weight for pattern in trained.behavior.patterns} == {1.0}
|
||||||
|
|
||||||
|
|
||||||
def test_engine_learns_causal_automation_for_shadow_without_user_credit(
|
def test_engine_learns_causal_automation_with_activation_credit(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
||||||
@@ -270,7 +280,8 @@ def test_engine_learns_causal_automation_for_shadow_without_user_credit(
|
|||||||
]
|
]
|
||||||
|
|
||||||
assert len(automation_patterns) == 3
|
assert len(automation_patterns) == 3
|
||||||
assert trained.behavior.high_confidence_sample_count == 0
|
assert trained.behavior.high_confidence_sample_count == 3
|
||||||
|
assert {pattern.weight for pattern in automation_patterns} == {1.0}
|
||||||
assert {
|
assert {
|
||||||
(
|
(
|
||||||
pattern.trigger_entity_id,
|
pattern.trigger_entity_id,
|
||||||
@@ -280,6 +291,10 @@ def test_engine_learns_causal_automation_for_shadow_without_user_credit(
|
|||||||
for pattern in automation_patterns
|
for pattern in automation_patterns
|
||||||
} == {("binary_sensor.storage_door", "off", "on")}
|
} == {("binary_sensor.storage_door", "off", "on")}
|
||||||
|
|
||||||
|
active = engine.set_active("light.storage", active=True)
|
||||||
|
|
||||||
|
assert active.behavior.mode is BehaviorMode.ACTIVE
|
||||||
|
|
||||||
|
|
||||||
def test_active_mode_rejects_unsafe_domains(tmp_path: Path) -> None:
|
def test_active_mode_rejects_unsafe_domains(tmp_path: Path) -> None:
|
||||||
settings = _settings(tmp_path)
|
settings = _settings(tmp_path)
|
||||||
@@ -301,7 +316,7 @@ def test_active_mode_rejects_unsafe_domains(tmp_path: Path) -> None:
|
|||||||
engine.set_active("lock.front_door", active=True)
|
engine.set_active("lock.front_door", active=True)
|
||||||
|
|
||||||
|
|
||||||
def test_active_mode_requires_user_attributed_actions(tmp_path: Path) -> None:
|
def test_active_mode_requires_trusted_manual_or_automation_actions(tmp_path: Path) -> None:
|
||||||
settings = _settings(tmp_path)
|
settings = _settings(tmp_path)
|
||||||
store = ActuatorStore(settings.actuator_store)
|
store = ActuatorStore(settings.actuator_store)
|
||||||
record = store.configure("light.office")
|
record = store.configure("light.office")
|
||||||
@@ -321,7 +336,7 @@ def test_active_mode_requires_user_attributed_actions(tmp_path: Path) -> None:
|
|||||||
reader = FakeBehaviorReader(entities=[], history=[], logbook=[])
|
reader = FakeBehaviorReader(entities=[], history=[], logbook=[])
|
||||||
engine = BehaviorEngine(ha_reader=reader, store=store, settings=settings)
|
engine = BehaviorEngine(ha_reader=reader, store=store, settings=settings)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="eindeutig dir zugeordnete"):
|
with pytest.raises(ValueError, match="manuelle oder automatisierte"):
|
||||||
engine.set_active("light.office", active=True)
|
engine.set_active("light.office", active=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,15 @@ def test_dashboard_is_served_at_root() -> None:
|
|||||||
assert "Ohne deine spätere Freigabe wird nichts geschaltet" 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 "Du wählst keine Sensoren und erstellst keine Regeln" in response.text
|
||||||
assert "Freigabe noch gesperrt" in response.text
|
assert "Freigabe noch gesperrt" in response.text
|
||||||
assert "Bediene das Licht dafür direkt über Home Assistant" in response.text
|
assert "manuelle oder automatisierte Handlung" in response.text
|
||||||
assert "Davon erkannte HA-Automationen" in response.text
|
assert "Davon erkannte HA-Automationen" in response.text
|
||||||
assert 'record.behavior.status === "trained" && missingUserActions === 0' in response.text
|
assert "Aktuelle Situation auswerten" in response.text
|
||||||
|
assert "Die Prüfung simuliert keinen Sensorwechsel" in response.text
|
||||||
|
assert "Kein frischer passender Sensorwechsel erkannt" in response.text
|
||||||
|
assert "Vorhersage jetzt prüfen" not in response.text
|
||||||
|
assert (
|
||||||
|
'record.behavior.status === "trained" && missingTrustedActions === 0'
|
||||||
|
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