Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3140f65527 | |||
| 5727053951 | |||
| 658516cd96 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.7 - 2026-06-15
|
||||
- WebSocket-State-Changes lesen jetzt das echte Home-Assistant-Eventformat
|
||||
(`event.data.entity_id`), damit Kontextwechsel wie Türsensoren sofort
|
||||
Vorhersagen und Schaltungen auslösen statt erst beim nächsten Statusabruf
|
||||
|
||||
## 0.7.6 - 2026-06-14
|
||||
- Kontextvorschläge blenden zusätzlich Batterie-, Status-, Node-, Last-Seen-
|
||||
und Basic-Entities aus, sofern sie nicht bewusst manuell ausgewählt wurden
|
||||
|
||||
## 0.7.5 - 2026-06-14
|
||||
- Kontextvorschläge weiter geschärft: Standardliste zeigt nur gleiche Räume,
|
||||
gemeinsame Geräte/Tokens oder echte globale Außenwerte
|
||||
- Diagnosewerte wie MQTT-, WiFi-, Restart- und Connect-Zähler werden nicht mehr
|
||||
als fachliche Kontextvorschläge angeboten
|
||||
|
||||
## 0.7.4 - 2026-06-14
|
||||
- Kontext-Auswahl liefert jetzt aktorbezogene Vorschläge statt einer pauschalen
|
||||
Roh-Liste aller Sensoren und Zustände
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: SillyHome Next
|
||||
version: "0.7.4"
|
||||
version: "0.7.7"
|
||||
slug: sillyhome_next
|
||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||
|
||||
@@ -79,6 +79,31 @@ _MANUAL_CONTEXT_DOMAINS = frozenset({
|
||||
"weather",
|
||||
})
|
||||
_CONTEXT_SUGGESTION_LIMIT = 120
|
||||
_OUTDOOR_TOKENS = frozenset({"aussen", "außen", "outdoor", "garten", "terrasse", "balkon"})
|
||||
_DIAGNOSTIC_TOKENS = frozenset({
|
||||
"basic",
|
||||
"battery",
|
||||
"connect",
|
||||
"count",
|
||||
"diagnostic",
|
||||
"firmware",
|
||||
"gesehen",
|
||||
"last",
|
||||
"linkquality",
|
||||
"knoten",
|
||||
"knotens",
|
||||
"mqtt",
|
||||
"node",
|
||||
"reason",
|
||||
"restart",
|
||||
"rssi",
|
||||
"signal",
|
||||
"ssid",
|
||||
"status",
|
||||
"uptime",
|
||||
"wifi",
|
||||
"zuletzt",
|
||||
})
|
||||
|
||||
|
||||
class ActuatorReconciliationService:
|
||||
@@ -131,6 +156,11 @@ class ActuatorReconciliationService:
|
||||
selected = entity.entity_id in selected_ids
|
||||
if selected:
|
||||
score = max(score, 1.0)
|
||||
if not selected and (
|
||||
_is_diagnostic_context(entity)
|
||||
or not _has_context_relationship(actuator, entity)
|
||||
):
|
||||
continue
|
||||
if not selected and score < 0.1:
|
||||
continue
|
||||
ranked.append((score, _context_sort_group(entity), entity))
|
||||
@@ -687,6 +717,32 @@ def _context_sort_group(entity: HaEntitySummary) -> str:
|
||||
return f"20_{entity.domain}_{device_class}"
|
||||
|
||||
|
||||
def _is_diagnostic_context(entity: HaEntitySummary) -> bool:
|
||||
tokens = _metadata_tokens(entity, include_stopwords=True)
|
||||
return bool(tokens.intersection(_DIAGNOSTIC_TOKENS))
|
||||
|
||||
|
||||
def _has_context_relationship(actuator: HaEntitySummary, entity: HaEntitySummary) -> bool:
|
||||
if (
|
||||
actuator.area_name
|
||||
and entity.area_name
|
||||
and actuator.area_name == entity.area_name
|
||||
and actuator.area_name.lower() not in _GENERIC_AREA_NAMES
|
||||
):
|
||||
return True
|
||||
if actuator.device_id and entity.device_id and actuator.device_id == entity.device_id:
|
||||
return True
|
||||
if actuator.device_name and entity.device_name and actuator.device_name == entity.device_name:
|
||||
return True
|
||||
if _metadata_tokens(actuator).intersection(_metadata_tokens(entity)):
|
||||
return True
|
||||
entity_tokens = _metadata_tokens(entity, include_stopwords=True)
|
||||
return bool(
|
||||
entity_tokens.intersection(_OUTDOOR_TOKENS)
|
||||
and entity.device_class in {"illuminance", "humidity", "temperature"}
|
||||
)
|
||||
|
||||
|
||||
def _score_candidate(
|
||||
actuator: HaEntitySummary,
|
||||
entity: HaEntitySummary,
|
||||
@@ -732,8 +788,7 @@ def _score_candidate(
|
||||
if context and role is EntityRole.BINARY_CONTEXT:
|
||||
score += 0.05
|
||||
evidence.append("Binärer Kontextsensor bevorzugt für Zusatzkontext.")
|
||||
outdoor_tokens = {"aussen", "außen", "outdoor", "garten", "terrasse", "balkon"}
|
||||
if entity_tokens.intersection(outdoor_tokens) and entity.device_class in {
|
||||
if entity_tokens.intersection(_OUTDOOR_TOKENS) and entity.device_class in {
|
||||
"illuminance",
|
||||
"humidity",
|
||||
"temperature",
|
||||
@@ -801,7 +856,7 @@ def _preferred_device_classes(domain: str, *, context: bool) -> frozenset[str]:
|
||||
return frozenset(mapping.get(domain, {"power", "energy", "temperature"}))
|
||||
|
||||
|
||||
def _metadata_tokens(entity: HaEntitySummary) -> set[str]:
|
||||
def _metadata_tokens(entity: HaEntitySummary, *, include_stopwords: bool = False) -> set[str]:
|
||||
raw_values = [
|
||||
entity.entity_id,
|
||||
entity.friendly_name,
|
||||
@@ -813,7 +868,7 @@ def _metadata_tokens(entity: HaEntitySummary) -> set[str]:
|
||||
if value is None:
|
||||
continue
|
||||
for token in _TOKEN_PATTERN.findall(value.lower().replace("_", " ")):
|
||||
if len(token) < 3 or token in _STOPWORDS:
|
||||
if len(token) < 3 or (not include_stopwords and token in _STOPWORDS):
|
||||
continue
|
||||
tokens.add(token)
|
||||
return tokens
|
||||
|
||||
14
app/main.py
14
app/main.py
@@ -100,7 +100,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
app = FastAPI(
|
||||
title="SillyHome Next API",
|
||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||
version="0.7.4",
|
||||
version="0.7.7",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
app.state.settings = load_settings()
|
||||
@@ -213,12 +213,20 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
||||
event = data.get("event", {})
|
||||
if event.get("event_type") != "state_changed":
|
||||
continue
|
||||
entity_id = event.get("entity_id")
|
||||
event_data = event.get("data", {})
|
||||
if not isinstance(event_data, dict):
|
||||
logger.warning("State-Changed-Event ohne gültige Daten empfangen")
|
||||
continue
|
||||
entity_id = event_data.get("entity_id")
|
||||
if not entity_id:
|
||||
continue
|
||||
# Prüfe, ob Entity ein Aktor oder relevanter Kontext ist
|
||||
# Sofortige Vorhersage für betroffene Aktoren auslösen
|
||||
await asyncio.to_thread(engine.handle_state_change, entity_id, event.get("new_state"))
|
||||
await asyncio.to_thread(
|
||||
engine.handle_state_change,
|
||||
entity_id,
|
||||
event_data.get("new_state"),
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Ungültige JSON-Nachricht von HA-WebSocket")
|
||||
except Exception as exc:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "sillyhome-next"
|
||||
version = "0.7.4"
|
||||
version = "0.7.7"
|
||||
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -78,7 +78,7 @@ def _service(
|
||||
model_store=str(tmp_path / "models"),
|
||||
automation_store=str(tmp_path / "automations"),
|
||||
actuator_store=str(tmp_path / "actuators"),
|
||||
history_days=14,
|
||||
history_days=31,
|
||||
min_training_points=5,
|
||||
retrain_stale_hours=24,
|
||||
reconcile_interval_seconds=900,
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None:
|
||||
'{"type":"auth_ok"}',
|
||||
(
|
||||
'{"type":"event","event":{"event_type":"state_changed",'
|
||||
'"entity_id":"light.test","new_state":{"state":"on"}}}'
|
||||
'"data":{"entity_id":"light.test","new_state":{"state":"on"}}}}'
|
||||
),
|
||||
asyncio.CancelledError(),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user