'Lampe', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'config' => ['mqtt_prefix' => 'shelly-lampe'], 'status' => 'active', 'last_seen_at' => now()->subHour(), ]); (new IngestShellyMessage('shelly-lampe/status/switch:0', json_encode(['output' => true, 'apower' => 12.7])))->handle(); $switch = $device->entities()->where('key', 'switch:0')->first(); $power = $device->entities()->where('key', 'power:0')->first(); $this->assertTrue(data_get($switch->state->state, 'on')); $this->assertSame(13, data_get($power->state->state, 'watts')); $this->assertTrue($device->refresh()->last_seen_at->gt(now()->subMinute())); } public function test_out_of_order_messages_do_not_overwrite_newer_state(): void { $device = Device::create([ 'name' => 'Lampe', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'config' => ['mqtt_prefix' => 'shelly-lampe'], 'status' => 'active', ]); // Newer message (higher observed_at) says on=true … (new IngestShellyMessage('shelly-lampe/status/switch:0', json_encode(['output' => true]), 2_000))->handle(); // … an older one arriving late must NOT overwrite it. (new IngestShellyMessage('shelly-lampe/status/switch:0', json_encode(['output' => false]), 1_000))->handle(); $switch = $device->entities()->where('key', 'switch:0')->first(); $this->assertTrue(data_get($switch->state->state, 'on')); } public function test_unknown_device_auto_onboards_on_a_recognized_component(): void { (new IngestShellyMessage('shelly1minig3-abc/status/switch:0', json_encode(['output' => true])))->handle(); $device = Device::where('config->mqtt_prefix', 'shelly1minig3-abc')->first(); $this->assertNotNull($device); $this->assertSame('Shelly', $device->vendor); $this->assertSame('mqtt', $device->protocol); $this->assertTrue((bool) data_get($device->config, 'auto_onboarded')); $this->assertTrue(data_get($device->entities()->where('key', 'switch:0')->first()->state->state, 'on')); } public function test_housekeeping_topic_does_not_onboard_a_device(): void { // `sys`/`wifi`/`cloud` status is noise — it must not create a device on its own. (new IngestShellyMessage('shelly1minig3-abc/status/sys', json_encode(['restart_required' => false])))->handle(); $this->assertDatabaseCount('devices', 0); } public function test_auto_onboard_can_be_disabled(): void { config()->set('homeos.mqtt.auto_onboard', false); (new IngestShellyMessage('shelly1minig3-abc/status/switch:0', json_encode(['output' => true])))->handle(); $this->assertDatabaseCount('devices', 0); } public function test_concurrent_onboarding_does_not_duplicate(): void { (new IngestShellyMessage('shelly-dup/status/switch:0', json_encode(['output' => true])))->handle(); (new IngestShellyMessage('shelly-dup/status/switch:0', json_encode(['output' => false]), 2_000))->handle(); $this->assertSame(1, Device::where('config->mqtt_prefix', 'shelly-dup')->count()); } public function test_command_service_audits_every_command(): void { // Fake driver so the test never touches a real broker. $this->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 []; } }); $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' => false]]); $command = app(DeviceCommandService::class)->toggle($entity->load('state')); $this->assertSame('turn_on', $command->command); $this->assertSame('ok', $command->result); $this->assertDatabaseHas('commands', ['command' => 'turn_on', 'source' => 'user', 'result' => 'ok']); } }