Compare commits
1 Commits
v2.0.0-alp
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f750e3e41 |
@@ -2,7 +2,7 @@ FROM python:3.13-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.12
|
ARG SILLYHOME_FUTURE_REF=v2.0.0-alpha.13
|
||||||
RUN python -m pip install --no-cache-dir \
|
RUN python -m pip install --no-cache-dir \
|
||||||
"http://192.168.6.31:3000/Otto/sillyhome-future/archive/${SILLYHOME_FUTURE_REF}.tar.gz"
|
"http://192.168.6.31:3000/Otto/sillyhome-future/archive/${SILLYHOME_FUTURE_REF}.tar.gz"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Future
|
name: SillyHome Future
|
||||||
version: "2.0.0-alpha.12"
|
version: "2.0.0-alpha.13"
|
||||||
slug: sillyhome_future
|
slug: sillyhome_future
|
||||||
description: Event-first SillyHome v2 test controller
|
description: Event-first SillyHome v2 test controller
|
||||||
url: http://192.168.6.31:3000/Otto/sillyhome-future
|
url: http://192.168.6.31:3000/Otto/sillyhome-future
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class JobStatus(StrEnum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateStatus(StrEnum):
|
||||||
|
PROPOSED = "proposed"
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
DISMISSED = "dismissed"
|
||||||
|
|
||||||
|
|
||||||
class EntityState(BaseModel):
|
class EntityState(BaseModel):
|
||||||
entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$")
|
entity_id: str = Field(pattern=r"^[a-z0-9_]+\.[a-z0-9_]+$")
|
||||||
domain: str
|
domain: str
|
||||||
@@ -152,6 +158,28 @@ class SensorWeightOverride(BaseModel):
|
|||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
|
class AutopilotSettings(BaseModel):
|
||||||
|
enabled: bool = True
|
||||||
|
interval_seconds: int = Field(default=3600, ge=300)
|
||||||
|
min_candidate_confidence: float = Field(default=0.65, ge=0.0, le=1.0)
|
||||||
|
auto_train: bool = True
|
||||||
|
auto_evaluate: bool = True
|
||||||
|
auto_activate: bool = False
|
||||||
|
last_run_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateRecommendation(BaseModel):
|
||||||
|
candidate_id: str
|
||||||
|
actuator_entity_id: str
|
||||||
|
trigger_entity_id: str
|
||||||
|
trigger_state: str | None = None
|
||||||
|
target_state: str
|
||||||
|
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
||||||
|
reason: str
|
||||||
|
status: CandidateStatus = CandidateStatus.PROPOSED
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
class HistoryAnalysis(BaseModel):
|
class HistoryAnalysis(BaseModel):
|
||||||
entity_id: str
|
entity_id: str
|
||||||
samples: int
|
samples: int
|
||||||
@@ -199,11 +227,13 @@ class LearningState(BaseModel):
|
|||||||
model_evaluations: dict[str, ModelEvaluation] = Field(default_factory=dict)
|
model_evaluations: dict[str, ModelEvaluation] = Field(default_factory=dict)
|
||||||
jobs: dict[str, JobQueueItem] = Field(default_factory=dict)
|
jobs: dict[str, JobQueueItem] = Field(default_factory=dict)
|
||||||
weight_overrides: dict[str, SensorWeightOverride] = Field(default_factory=dict)
|
weight_overrides: dict[str, SensorWeightOverride] = Field(default_factory=dict)
|
||||||
|
candidates: dict[str, CandidateRecommendation] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class ControlState(BaseModel):
|
class ControlState(BaseModel):
|
||||||
profiles: dict[str, ControlProfile] = Field(default_factory=dict)
|
profiles: dict[str, ControlProfile] = Field(default_factory=dict)
|
||||||
global_enabled: bool = True
|
global_enabled: bool = True
|
||||||
|
autopilot: AutopilotSettings = Field(default_factory=AutopilotSettings)
|
||||||
|
|
||||||
|
|
||||||
class BackupBundle(BaseModel):
|
class BackupBundle(BaseModel):
|
||||||
|
|||||||
195
app/main.py
195
app/main.py
@@ -16,9 +16,12 @@ from app.core.ha_client import FutureHaClient, HaClientConfig, service_for_state
|
|||||||
from app.core.handoff import HandoffMatrix
|
from app.core.handoff import HandoffMatrix
|
||||||
from app.core.models import (
|
from app.core.models import (
|
||||||
AuditEvent,
|
AuditEvent,
|
||||||
|
AutopilotSettings,
|
||||||
AutomationProposal,
|
AutomationProposal,
|
||||||
BackupBundle,
|
BackupBundle,
|
||||||
BehaviorPatternV2,
|
BehaviorPatternV2,
|
||||||
|
CandidateRecommendation,
|
||||||
|
CandidateStatus,
|
||||||
ControlProfile,
|
ControlProfile,
|
||||||
ControlState,
|
ControlState,
|
||||||
EntityState,
|
EntityState,
|
||||||
@@ -123,27 +126,36 @@ class WeightOverrideRequest(BaseModel):
|
|||||||
note: str | None = Field(default=None, max_length=500)
|
note: str | None = Field(default=None, max_length=500)
|
||||||
|
|
||||||
|
|
||||||
|
class AutopilotRunResult(BaseModel):
|
||||||
|
candidates: list[CandidateRecommendation]
|
||||||
|
trained_models: list[ModelRecord]
|
||||||
|
evaluations: list[ModelEvaluation]
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||||
global ha_client
|
global ha_client
|
||||||
listener_task: asyncio.Task[None] | None = None
|
listener_task: asyncio.Task[None] | None = None
|
||||||
|
autopilot_task: asyncio.Task[None] | None = None
|
||||||
ha_client = _ha_client_from_env()
|
ha_client = _ha_client_from_env()
|
||||||
if ha_client is not None:
|
if ha_client is not None:
|
||||||
_load_initial_ha_states(ha_client)
|
_load_initial_ha_states(ha_client)
|
||||||
listener_task = asyncio.create_task(_ha_listener_loop(ha_client))
|
listener_task = asyncio.create_task(_ha_listener_loop(ha_client))
|
||||||
|
autopilot_task = asyncio.create_task(_autopilot_loop())
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
if listener_task is not None:
|
for task in (listener_task, autopilot_task):
|
||||||
listener_task.cancel()
|
if task is not None:
|
||||||
|
task.cancel()
|
||||||
with suppress(asyncio.CancelledError):
|
with suppress(asyncio.CancelledError):
|
||||||
await listener_task
|
await task
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SillyHome Future API",
|
title="SillyHome Future API",
|
||||||
description="SillyHome v2 event-core side project.",
|
description="SillyHome v2 event-core side project.",
|
||||||
version="2.0.0-alpha.12",
|
version="2.0.0-alpha.13",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -199,6 +211,9 @@ def dashboard_data() -> dict[str, object]:
|
|||||||
"automation_proposals": len(learning.automation_proposals),
|
"automation_proposals": len(learning.automation_proposals),
|
||||||
"models": len(learning.models),
|
"models": len(learning.models),
|
||||||
"jobs": len(learning.jobs),
|
"jobs": len(learning.jobs),
|
||||||
|
"candidates": len(learning.candidates),
|
||||||
|
"autopilot_enabled": control.autopilot.enabled,
|
||||||
|
"autopilot_last_run_at": control.autopilot.last_run_at,
|
||||||
"rooms": list(learning.rooms.values()),
|
"rooms": list(learning.rooms.values()),
|
||||||
"scenes": list(learning.scenes.values()),
|
"scenes": list(learning.scenes.values()),
|
||||||
"audit": latest_audit,
|
"audit": latest_audit,
|
||||||
@@ -231,11 +246,87 @@ def feature_parity() -> dict[str, object]:
|
|||||||
"lokales Modelltraining",
|
"lokales Modelltraining",
|
||||||
"Sensor-Gewichte",
|
"Sensor-Gewichte",
|
||||||
"Job-Queue",
|
"Job-Queue",
|
||||||
|
"Autopilot light",
|
||||||
|
"Kandidaten-Vorschlaege",
|
||||||
|
"periodisches Training/Bewertung",
|
||||||
],
|
],
|
||||||
"next_to_expand": ["Dashboard-Flaechen fuer History/Modelle"],
|
"next_to_expand": ["Dashboard-Flaechen fuer Autopilot/History/Modelle"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v2/autopilot/settings", response_model=AutopilotSettings)
|
||||||
|
def get_autopilot_settings() -> AutopilotSettings:
|
||||||
|
return stores.control().autopilot
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/v2/autopilot/settings", response_model=AutopilotSettings)
|
||||||
|
def put_autopilot_settings(settings: AutopilotSettings) -> AutopilotSettings:
|
||||||
|
control = stores.control().model_copy(update={"autopilot": settings})
|
||||||
|
stores.save_control(control)
|
||||||
|
_append_audit(
|
||||||
|
"autopilot",
|
||||||
|
None,
|
||||||
|
"Autopilot light aktiviert." if settings.enabled else "Autopilot light deaktiviert.",
|
||||||
|
)
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v2/autopilot/run", response_model=AutopilotRunResult)
|
||||||
|
def run_autopilot() -> AutopilotRunResult:
|
||||||
|
return _run_autopilot_once()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v2/autopilot/candidates", response_model=list[CandidateRecommendation])
|
||||||
|
def list_candidates() -> list[CandidateRecommendation]:
|
||||||
|
return list(stores.learning().candidates.values())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v2/autopilot/candidates/{candidate_id}/accept", response_model=LearningProfile)
|
||||||
|
def accept_candidate(candidate_id: str) -> LearningProfile:
|
||||||
|
state = stores.learning()
|
||||||
|
candidate = state.candidates[candidate_id].model_copy(
|
||||||
|
update={"status": CandidateStatus.ACCEPTED}
|
||||||
|
)
|
||||||
|
state.candidates[candidate_id] = candidate
|
||||||
|
profile = state.profiles.get(
|
||||||
|
candidate.actuator_entity_id,
|
||||||
|
LearningProfile(actuator_entity_id=candidate.actuator_entity_id),
|
||||||
|
)
|
||||||
|
profile = profile.model_copy(
|
||||||
|
update={
|
||||||
|
"patterns": [
|
||||||
|
*profile.patterns,
|
||||||
|
BehaviorPatternV2(
|
||||||
|
actuator_entity_id=candidate.actuator_entity_id,
|
||||||
|
target_state=candidate.target_state,
|
||||||
|
trigger_entity_id=candidate.trigger_entity_id,
|
||||||
|
trigger_state=candidate.trigger_state,
|
||||||
|
support=3,
|
||||||
|
confidence=candidate.confidence,
|
||||||
|
source="autopilot",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"model_version": "autopilot-light",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
state.profiles[candidate.actuator_entity_id] = profile
|
||||||
|
stores.save_learning(state)
|
||||||
|
_append_job("autopilot_candidate", f"Kandidat {candidate_id} akzeptiert.")
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v2/autopilot/candidates/{candidate_id}/dismiss", response_model=CandidateRecommendation)
|
||||||
|
def dismiss_candidate(candidate_id: str) -> CandidateRecommendation:
|
||||||
|
state = stores.learning()
|
||||||
|
candidate = state.candidates[candidate_id].model_copy(
|
||||||
|
update={"status": CandidateStatus.DISMISSED}
|
||||||
|
)
|
||||||
|
state.candidates[candidate_id] = candidate
|
||||||
|
stores.save_learning(state)
|
||||||
|
_append_job("autopilot_candidate", f"Kandidat {candidate_id} verworfen.")
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
@app.post("/v2/automations/proposals", response_model=AutomationProposal)
|
@app.post("/v2/automations/proposals", response_model=AutomationProposal)
|
||||||
def create_automation_proposal(payload: AutomationProposalRequest) -> AutomationProposal:
|
def create_automation_proposal(payload: AutomationProposalRequest) -> AutomationProposal:
|
||||||
state = stores.learning()
|
state = stores.learning()
|
||||||
@@ -845,6 +936,98 @@ async def _ha_listener_loop(client: FutureHaClient) -> None:
|
|||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
async def _autopilot_loop() -> None:
|
||||||
|
while True:
|
||||||
|
control = stores.control()
|
||||||
|
settings = control.autopilot
|
||||||
|
if settings.enabled:
|
||||||
|
await asyncio.to_thread(_run_autopilot_once)
|
||||||
|
await asyncio.sleep(stores.control().autopilot.interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_autopilot_once() -> AutopilotRunResult:
|
||||||
|
control = stores.control()
|
||||||
|
settings = control.autopilot
|
||||||
|
if not settings.enabled:
|
||||||
|
return AutopilotRunResult(candidates=[], trained_models=[], evaluations=[])
|
||||||
|
learning = stores.learning()
|
||||||
|
candidates = _generate_candidates(settings)
|
||||||
|
for candidate in candidates:
|
||||||
|
learning.candidates[candidate.candidate_id] = candidate
|
||||||
|
stores.save_learning(learning)
|
||||||
|
updated_settings = settings.model_copy(update={"last_run_at": datetime.now(timezone.utc)})
|
||||||
|
stores.save_control(control.model_copy(update={"autopilot": updated_settings}))
|
||||||
|
trained = train_models(TrainModelRequest()) if settings.auto_train else []
|
||||||
|
evaluations = evaluate_models(EvaluateModelRequest()) if settings.auto_evaluate else []
|
||||||
|
_append_job("autopilot", f"Autopilot light: {len(candidates)} Kandidaten erzeugt.")
|
||||||
|
return AutopilotRunResult(candidates=candidates, trained_models=trained, evaluations=evaluations)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_candidates(settings: AutopilotSettings) -> list[CandidateRecommendation]:
|
||||||
|
runtime = stores.runtime()
|
||||||
|
existing = stores.learning().candidates
|
||||||
|
actuators = [
|
||||||
|
entity
|
||||||
|
for entity in runtime.entities.values()
|
||||||
|
if entity.domain in {"light", "switch", "fan", "cover", "humidifier"}
|
||||||
|
][:150]
|
||||||
|
triggers = [
|
||||||
|
entity
|
||||||
|
for entity in runtime.entities.values()
|
||||||
|
if entity.domain in {"binary_sensor", "sensor"}
|
||||||
|
][:500]
|
||||||
|
result: list[CandidateRecommendation] = []
|
||||||
|
for actuator in actuators:
|
||||||
|
best_trigger: EntityState | None = None
|
||||||
|
best_score = 0.0
|
||||||
|
best_reason = ""
|
||||||
|
for trigger in triggers:
|
||||||
|
score, reason = _candidate_score(actuator, trigger)
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best_reason = reason
|
||||||
|
best_trigger = trigger
|
||||||
|
if best_trigger is None or best_score < settings.min_candidate_confidence:
|
||||||
|
continue
|
||||||
|
candidate_id = f"{actuator.entity_id}:{best_trigger.entity_id}:on"
|
||||||
|
if candidate_id in existing:
|
||||||
|
continue
|
||||||
|
result.append(
|
||||||
|
CandidateRecommendation(
|
||||||
|
candidate_id=candidate_id,
|
||||||
|
actuator_entity_id=actuator.entity_id,
|
||||||
|
trigger_entity_id=best_trigger.entity_id,
|
||||||
|
trigger_state="on" if best_trigger.domain == "binary_sensor" else None,
|
||||||
|
target_state="open" if actuator.domain == "cover" else "on",
|
||||||
|
confidence=round(best_score, 2),
|
||||||
|
reason=best_reason,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(result) >= 20:
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_score(actuator: EntityState, trigger: EntityState) -> tuple[float, str]:
|
||||||
|
if actuator.area_name and actuator.area_name == trigger.area_name:
|
||||||
|
return 0.82, f"Gleicher Raum: {actuator.area_name}"
|
||||||
|
actuator_tokens = _entity_tokens(actuator)
|
||||||
|
trigger_tokens = _entity_tokens(trigger)
|
||||||
|
overlap = actuator_tokens & trigger_tokens
|
||||||
|
if overlap:
|
||||||
|
return min(0.78, 0.55 + (0.08 * len(overlap))), (
|
||||||
|
"Aehnliche Namen: " + ", ".join(sorted(overlap)[:4])
|
||||||
|
)
|
||||||
|
if trigger.domain == "binary_sensor" and actuator.domain in {"light", "switch"}:
|
||||||
|
return 0.62, "Binary-Sensor passt grundsaetzlich zu Licht/Schalter."
|
||||||
|
return 0.0, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _entity_tokens(entity: EntityState) -> set[str]:
|
||||||
|
raw = f"{entity.entity_id} {entity.friendly_name or ''}".lower()
|
||||||
|
return {part for part in raw.replace(".", "_").split("_") if len(part) >= 4}
|
||||||
|
|
||||||
|
|
||||||
def _set_websocket_status(status: str) -> None:
|
def _set_websocket_status(status: str) -> None:
|
||||||
runtime = stores.runtime()
|
runtime = stores.runtime()
|
||||||
if runtime.websocket_status == status:
|
if runtime.websocket_status == status:
|
||||||
@@ -1167,6 +1350,8 @@ def _dashboard_html() -> str:
|
|||||||
['Vorschlaege', data.automation_proposals],
|
['Vorschlaege', data.automation_proposals],
|
||||||
['Modelle', data.models],
|
['Modelle', data.models],
|
||||||
['Jobs', data.jobs],
|
['Jobs', data.jobs],
|
||||||
|
['Kandidaten', data.candidates],
|
||||||
|
['Autopilot', data.autopilot_enabled ? 'aktiv' : 'aus'],
|
||||||
['Räume', data.rooms.length],
|
['Räume', data.rooms.length],
|
||||||
['Szenen', data.scenes.length],
|
['Szenen', data.scenes.length],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-future"
|
name = "sillyhome-future"
|
||||||
version = "2.0.0-alpha.12"
|
version = "2.0.0-alpha.13"
|
||||||
description = "SillyHome v2 event-core prototype"
|
description = "SillyHome v2 event-core prototype"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ def test_addon_config_declares_future_addon() -> None:
|
|||||||
config = yaml.safe_load(Path("addon/config.yaml").read_text(encoding="utf-8"))
|
config = yaml.safe_load(Path("addon/config.yaml").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert config["slug"] == "sillyhome_future"
|
assert config["slug"] == "sillyhome_future"
|
||||||
assert config["version"] == "2.0.0-alpha.12"
|
assert config["version"] == "2.0.0-alpha.13"
|
||||||
assert config["ingress"] is True
|
assert config["ingress"] is True
|
||||||
assert config["ingress_port"] == 8099
|
assert config["ingress_port"] == 8099
|
||||||
assert config["homeassistant_api"] is True
|
assert config["homeassistant_api"] is True
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def test_health_and_backup_roundtrip(tmp_path, monkeypatch) -> None: # type: ig
|
|||||||
restore = client.post("/v2/backup/restore", json=backup.json())
|
restore = client.post("/v2/backup/restore", json=backup.json())
|
||||||
|
|
||||||
assert health.status_code == 200
|
assert health.status_code == 200
|
||||||
assert health.json()["version"] == "2.0.0-alpha.12"
|
assert health.json()["version"] == "2.0.0-alpha.13"
|
||||||
assert backup.status_code == 200
|
assert backup.status_code == 200
|
||||||
assert restore.status_code == 200
|
assert restore.status_code == 200
|
||||||
assert restore.json() == {"status": "restored"}
|
assert restore.json() == {"status": "restored"}
|
||||||
@@ -39,6 +39,13 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None
|
|||||||
entity_id="binary_sensor.storage_door",
|
entity_id="binary_sensor.storage_door",
|
||||||
domain="binary_sensor",
|
domain="binary_sensor",
|
||||||
state="off",
|
state="off",
|
||||||
|
area_name="Storage",
|
||||||
|
),
|
||||||
|
"light.storage_door": EntityState(
|
||||||
|
entity_id="light.storage_door",
|
||||||
|
domain="light",
|
||||||
|
state="off",
|
||||||
|
area_name="Storage",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -104,6 +111,11 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None
|
|||||||
models = client.post("/v2/models/train", json={"actuator_entity_id": "light.storage"})
|
models = client.post("/v2/models/train", json={"actuator_entity_id": "light.storage"})
|
||||||
evaluation = client.post("/v2/models/evaluate", json={"actuator_entity_id": "light.storage"})
|
evaluation = client.post("/v2/models/evaluate", json={"actuator_entity_id": "light.storage"})
|
||||||
evaluations = client.get("/v2/models/evaluations")
|
evaluations = client.get("/v2/models/evaluations")
|
||||||
|
autopilot = client.post("/v2/autopilot/run")
|
||||||
|
candidates = client.get("/v2/autopilot/candidates")
|
||||||
|
accepted = client.post(
|
||||||
|
f"/v2/autopilot/candidates/{autopilot.json()['candidates'][0]['candidate_id']}/accept"
|
||||||
|
)
|
||||||
jobs = client.get("/v2/jobs")
|
jobs = client.get("/v2/jobs")
|
||||||
dashboard = client.get("/v2/dashboard")
|
dashboard = client.get("/v2/dashboard")
|
||||||
|
|
||||||
@@ -111,6 +123,7 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None
|
|||||||
assert [item["entity_id"] for item in entities.json()] == [
|
assert [item["entity_id"] for item in entities.json()] == [
|
||||||
"binary_sensor.storage_door",
|
"binary_sensor.storage_door",
|
||||||
"light.storage",
|
"light.storage",
|
||||||
|
"light.storage_door",
|
||||||
]
|
]
|
||||||
assert control.status_code == 200
|
assert control.status_code == 200
|
||||||
assert control.json()["stage"] == "dry_run"
|
assert control.json()["stage"] == "dry_run"
|
||||||
@@ -147,8 +160,12 @@ def test_dashboard_control_and_learning_endpoints(tmp_path, monkeypatch) -> None
|
|||||||
assert evaluation.json()[0]["verdict"] in {"bereit", "weiter testen"}
|
assert evaluation.json()[0]["verdict"] in {"bereit", "weiter testen"}
|
||||||
assert evaluations.status_code == 200
|
assert evaluations.status_code == 200
|
||||||
assert evaluations.json()
|
assert evaluations.json()
|
||||||
|
assert autopilot.status_code == 200
|
||||||
|
assert autopilot.json()["candidates"]
|
||||||
|
assert candidates.status_code == 200
|
||||||
|
assert accepted.status_code == 200
|
||||||
assert jobs.status_code == 200
|
assert jobs.status_code == 200
|
||||||
assert jobs.json()
|
assert jobs.json()
|
||||||
assert dashboard.status_code == 200
|
assert dashboard.status_code == 200
|
||||||
assert dashboard.json()["actuator_count"] == 1
|
assert dashboard.json()["actuator_count"] == 2
|
||||||
assert dashboard.json()["automation_proposals"] == 1
|
assert dashboard.json()["automation_proposals"] == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user