homeos/app/Services/AutomationEngine.php

96 lines
3.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\Automation;
use App\Models\Entity;
use Illuminate\Support\Facades\Log;
/**
* Minimal trigger → condition → action engine (handoff §13.6). Every automation carries a
* cooldown (motion sensors flood the queue otherwise) and can run in dry-run mode, which
* logs the action instead of switching — never develop against the live home (H5).
*/
class AutomationEngine
{
public function __construct(private readonly DeviceCommandService $commands) {}
/** Evaluate state-change automations after a device entity changed. */
public function onStateChange(string $deviceUuid, string $entityKey, array $state): void
{
Automation::where('enabled', true)->where('trigger_type', 'state_change')->get()
->each(function (Automation $automation) use ($deviceUuid, $entityKey, $state) {
if ($this->matchesStateTrigger($automation, $deviceUuid, $entityKey, $state)) {
$this->maybeRun($automation);
}
});
}
/** Evaluate time-based automations (called every minute by the scheduler). */
public function tick(): void
{
$hhmm = now()->format('H:i');
Automation::where('enabled', true)->where('trigger_type', 'time')->get()
->each(function (Automation $automation) use ($hhmm) {
if (data_get($automation->trigger_config, 'at') === $hhmm) {
$this->maybeRun($automation);
}
});
}
private function matchesStateTrigger(Automation $automation, string $deviceUuid, string $entityKey, array $state): bool
{
$cfg = $automation->trigger_config ?? [];
if (($cfg['device'] ?? null) !== $deviceUuid || ($cfg['entity_key'] ?? null) !== $entityKey) {
return false;
}
$field = $cfg['field'] ?? 'on';
return array_key_exists($field, $state) && $state[$field] == ($cfg['equals'] ?? true);
}
private function maybeRun(Automation $automation): void
{
if ($automation->cooldown_seconds > 0
&& $automation->last_triggered_at !== null
&& $automation->last_triggered_at->gt(now()->subSeconds($automation->cooldown_seconds))) {
return;
}
foreach ($automation->actions ?? [] as $action) {
$this->runAction($automation, $action);
}
$automation->forceFill(['last_triggered_at' => now()])->save();
}
private function runAction(Automation $automation, array $action): void
{
$type = $action['type'] ?? null;
if ($type === 'switch') {
$entity = Entity::query()
->whereHas('device', fn ($q) => $q->where('uuid', $action['device'] ?? null))
->where('key', $action['entity_key'] ?? null)
->with('device', 'state')->first();
if ($entity === null) {
return;
}
if ($automation->dry_run) {
Log::info("[automation dry-run] {$automation->name}: switch {$entity->key}", $action);
return;
}
$this->commands->setOn($entity, (bool) ($action['on'] ?? true), 'automation');
} elseif ($type === 'notify') {
Log::info("[automation".($automation->dry_run ? ' dry-run' : '')."] {$automation->name}: ".($action['message'] ?? ''));
}
}
}