app->bind(ShellyMqttDriver::class, fn () => new class implements DeviceDriver { public function turnOn(Entity $entity): void {} public function turnOff(Entity $entity): void {} public function setState(Entity $entity, array $state): void {} public function setLight(Entity $entity, array $params): void {} public function reboot(Device $device): void {} public function capabilities(): array { return []; } }); } /** @return array{0:Device,1:Entity} */ private function makeSwitch(): array { $device = Device::create(['name' => 'Lampe', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'status' => 'active']); $entity = Entity::create(['device_id' => $device->id, 'type' => 'switch', 'key' => 'switch:0']); DeviceState::create(['entity_id' => $entity->id, 'state' => ['on' => true]]); return [$device, $entity]; } public function test_state_change_trigger_runs_switch_action_and_audits_as_automation(): void { [$trigger, $triggerEntity] = $this->makeSwitch(); [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'Motion turns light off', 'enabled' => true, 'trigger_type' => 'state_change', 'trigger_config' => ['device' => $trigger->uuid, 'entity_key' => $triggerEntity->key, 'field' => 'on', 'equals' => true], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => false]], 'cooldown_seconds' => 0, ]); app(AutomationEngine::class)->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => true]); $this->assertDatabaseHas('commands', ['source' => 'automation', 'command' => 'turn_off', 'result' => 'ok']); } public function test_trigger_does_not_fire_when_value_differs(): void { [$trigger, $triggerEntity] = $this->makeSwitch(); [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'On only', 'enabled' => true, 'trigger_type' => 'state_change', 'trigger_config' => ['device' => $trigger->uuid, 'entity_key' => $triggerEntity->key, 'field' => 'on', 'equals' => true], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => false]], 'cooldown_seconds' => 0, ]); app(AutomationEngine::class)->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => false]); $this->assertDatabaseCount('commands', 0); } public function test_cooldown_prevents_a_second_run(): void { [$trigger, $triggerEntity] = $this->makeSwitch(); [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'Debounced', 'enabled' => true, 'trigger_type' => 'state_change', 'trigger_config' => ['device' => $trigger->uuid, 'entity_key' => $triggerEntity->key, 'field' => 'on', 'equals' => true], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => false]], 'cooldown_seconds' => 300, ]); $engine = app(AutomationEngine::class); $engine->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => true]); $engine->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => true]); $this->assertDatabaseCount('commands', 1); } public function test_dry_run_does_not_issue_a_command(): void { [$trigger, $triggerEntity] = $this->makeSwitch(); [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'Dry', 'enabled' => true, 'trigger_type' => 'state_change', 'trigger_config' => ['device' => $trigger->uuid, 'entity_key' => $triggerEntity->key, 'field' => 'on', 'equals' => true], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => false]], 'cooldown_seconds' => 0, 'dry_run' => true, ]); app(AutomationEngine::class)->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => true]); $this->assertDatabaseCount('commands', 0); } public function test_time_trigger_fires_on_matching_minute(): void { [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'Morning light', 'enabled' => true, 'trigger_type' => 'time', 'trigger_config' => ['at' => now()->format('H:i')], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => true]], 'cooldown_seconds' => 0, ]); app(AutomationEngine::class)->tick(); $this->assertDatabaseHas('commands', ['source' => 'automation', 'command' => 'turn_on']); } public function test_disabled_automation_never_runs(): void { [$trigger, $triggerEntity] = $this->makeSwitch(); [$target, $targetEntity] = $this->makeSwitch(); Automation::create([ 'name' => 'Off', 'enabled' => false, 'trigger_type' => 'state_change', 'trigger_config' => ['device' => $trigger->uuid, 'entity_key' => $triggerEntity->key, 'field' => 'on', 'equals' => true], 'actions' => [['type' => 'switch', 'device' => $target->uuid, 'entity_key' => $targetEntity->key, 'on' => false]], 'cooldown_seconds' => 0, ]); app(AutomationEngine::class)->onStateChange($trigger->uuid, $triggerEntity->key, ['on' => true]); $this->assertDatabaseCount('commands', 0); } }