feat: add manual context assignment and fix actuator discovery
This commit is contained in:
@@ -7,7 +7,6 @@ from app.actuators.lifecycle import ActuatorReconciliationService
|
||||
from app.actuators.models import (
|
||||
AssignmentSource,
|
||||
LifecycleStatus,
|
||||
ManualOverride,
|
||||
model_id_for_actuator,
|
||||
)
|
||||
from app.actuators.store import ActuatorStore
|
||||
@@ -240,7 +239,45 @@ def test_reconciliation_does_not_cross_assign_other_room_light_energy(
|
||||
assert record.lifecycle.status is LifecycleStatus.ARCHIVED
|
||||
|
||||
|
||||
def test_legacy_manual_override_is_cleared_and_automatic_mapping_wins(tmp_path: Path) -> None:
|
||||
def test_reconciliation_ignores_generic_monitoring_area_for_automatic_context(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
start = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||
entities = [
|
||||
HaEntitySummary(
|
||||
entity_id="light.abstellkammer",
|
||||
domain="light",
|
||||
friendly_name="Licht Abstellkammer",
|
||||
area_name="Monitoring",
|
||||
),
|
||||
HaEntitySummary(
|
||||
entity_id="binary_sensor.disk_overheating",
|
||||
domain="binary_sensor",
|
||||
device_class="problem",
|
||||
friendly_name="Max. fehlerhafte Sektoren ueberschritten",
|
||||
area_name="Monitoring",
|
||||
),
|
||||
HaEntitySummary(
|
||||
entity_id="sensor.router_power",
|
||||
domain="sensor",
|
||||
device_class="power",
|
||||
state_class="measurement",
|
||||
unit_of_measurement="W",
|
||||
friendly_name="Router Leistung",
|
||||
area_name="Monitoring",
|
||||
),
|
||||
]
|
||||
service = _service(tmp_path, entities, {"sensor.router_power": _points(8, start, 1.0)})
|
||||
|
||||
record = service.configure_actuator("light.abstellkammer")
|
||||
|
||||
assert record.assignment.selected_numeric_entity_id is None
|
||||
assert record.assignment.selected_context_entity_ids == []
|
||||
assert record.assignment.review_required is True
|
||||
assert record.lifecycle.status is LifecycleStatus.ARCHIVED
|
||||
|
||||
|
||||
def test_manual_assignment_persists_and_wins_over_automatic_mapping(tmp_path: Path) -> None:
|
||||
start = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||
entities = [
|
||||
HaEntitySummary(
|
||||
@@ -273,21 +310,18 @@ def test_legacy_manual_override_is_cleared_and_automatic_mapping_wins(tmp_path:
|
||||
"sensor.abstellkammer_power": _points(8, start, 30.0),
|
||||
}
|
||||
service = _service(tmp_path, entities, history)
|
||||
configured = service.configure_actuator("light.abstellkammer")
|
||||
legacy = configured.model_copy(
|
||||
update={
|
||||
"manual_override": ManualOverride(
|
||||
numeric_entity_id="sensor.abstellkammer_power",
|
||||
context_entity_ids=[],
|
||||
note="Alte manuelle Zuordnung",
|
||||
)
|
||||
}
|
||||
service.configure_actuator("light.abstellkammer")
|
||||
service.set_manual_assignment(
|
||||
"light.abstellkammer",
|
||||
numeric_entity_id="sensor.abstellkammer_power",
|
||||
context_entity_ids=["sensor.abstellkammer_illuminance"],
|
||||
note="Manuell wichtiger Sensor",
|
||||
)
|
||||
service._store.upsert(legacy)
|
||||
|
||||
restarted = _service(tmp_path, entities, history)
|
||||
record = restarted.reconcile_actuator("light.abstellkammer")
|
||||
|
||||
assert record.assignment.selected_numeric_entity_id == "sensor.abstellkammer_illuminance"
|
||||
assert record.assignment.source.value == "automatic"
|
||||
assert record.manual_override is None
|
||||
assert record.assignment.selected_numeric_entity_id == "sensor.abstellkammer_power"
|
||||
assert record.assignment.selected_context_entity_ids == ["sensor.abstellkammer_illuminance"]
|
||||
assert record.assignment.source is AssignmentSource.MANUAL
|
||||
assert record.manual_override is not None
|
||||
|
||||
@@ -179,14 +179,39 @@ def test_actuator_api_configures_reconciles_and_removes(tmp_path: Path) -> None:
|
||||
assert client.get("/v1/actuators").json() == []
|
||||
|
||||
|
||||
def test_manual_override_endpoint_is_not_exposed(tmp_path: Path) -> None:
|
||||
def test_manual_assignment_endpoint_updates_context(tmp_path: Path) -> None:
|
||||
with TestClient(app) as client:
|
||||
_install_service(tmp_path)
|
||||
client.post("/v1/actuators", json={"actuator_entity_id": "light.abstellkammer"})
|
||||
|
||||
response = client.post(
|
||||
"/v1/actuators/light.abstellkammer/override",
|
||||
json={"numeric_entity_id": "sensor.abstellkammer_illuminance"},
|
||||
"/v1/actuators/light.abstellkammer/assignment",
|
||||
json={
|
||||
"numeric_entity_id": "sensor.abstellkammer_illuminance",
|
||||
"context_entity_ids": ["binary_sensor.abstellkammer_motion"],
|
||||
"note": "Manuell gesetzt",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["assignment"]["source"] == "manual"
|
||||
assert payload["assignment"]["selected_numeric_entity_id"] == (
|
||||
"sensor.abstellkammer_illuminance"
|
||||
)
|
||||
assert payload["assignment"]["selected_context_entity_ids"] == [
|
||||
"binary_sensor.abstellkammer_motion"
|
||||
]
|
||||
|
||||
|
||||
def test_context_options_returns_learnable_entities(tmp_path: Path) -> None:
|
||||
with TestClient(app) as client:
|
||||
_install_service(tmp_path)
|
||||
|
||||
response = client.get("/v1/actuators/context-options")
|
||||
|
||||
assert response.status_code == 200
|
||||
entity_ids = {item["entity_id"] for item in response.json()}
|
||||
assert "sensor.abstellkammer_illuminance" in entity_ids
|
||||
assert "binary_sensor.abstellkammer_motion" in entity_ids
|
||||
assert "light.abstellkammer" in entity_ids
|
||||
|
||||
@@ -12,6 +12,7 @@ def test_dashboard_is_served_at_root() -> None:
|
||||
assert "So gehst du vor" in response.text
|
||||
assert "Gerät zum Lernen auswählen" in response.text
|
||||
assert "Entitätsname oder Gerät aus Home Assistant" in response.text
|
||||
assert "Oder aus Liste wä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
|
||||
@@ -21,6 +22,9 @@ def test_dashboard_is_served_at_root() -> None:
|
||||
assert "Pausieren" in response.text
|
||||
assert "Davon erkannte HA-Automationen" in response.text
|
||||
assert "Aktuelle Situation auswerten" in response.text
|
||||
assert "Kontext selbst festlegen" in response.text
|
||||
assert "Diese Kontext-Auswahl speichern" in response.text
|
||||
assert "manual-context-select" 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
|
||||
|
||||
Reference in New Issue
Block a user