function ($request) use ($switchOn) { $method = $request->data()['method'] ?? ''; $result = match ($method) { 'Shelly.GetDeviceInfo' => ['id' => 'shellyplus1-abc', 'model' => 'SNSW-001X16EU', 'name' => 'Flur'], 'Shelly.GetStatus' => [ 'switch:0' => ['id' => 0, 'output' => $switchOn, 'apower' => 5.0], 'input:0' => ['id' => 0, 'state' => false], 'sys' => ['restart_required' => false], ], default => [], }; return Http::response(['id' => 1, 'result' => $result]); }]); } public function test_local_onboarder_creates_an_http_device_from_the_api(): void { $this->fakeShelly(); $device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78', 'Deckenlicht', null); $this->assertSame('http', $device->protocol); $this->assertSame('10.10.30.78', data_get($device->config, 'ip')); $this->assertSame('shellyplus1-abc', data_get($device->config, 'mqtt_prefix')); $this->assertSame('SNSW-001X16EU', $device->model); $this->assertTrue(data_get($device->entities()->where('key', 'switch:0')->first()->state->state, 'on')); $this->assertSame(5, data_get($device->entities()->where('key', 'power:0')->first()->state->state, 'watts')); } public function test_onboarding_reuses_a_device_auto_created_via_mqtt(): void { $this->fakeShelly(); Device::create(['name' => 'x', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'config' => ['mqtt_prefix' => 'shellyplus1-abc']]); app(ShellyLocalOnboarder::class)->onboard('10.10.30.78'); $this->assertSame(1, Device::where('config->mqtt_prefix', 'shellyplus1-abc')->count()); $this->assertSame('http', Device::where('config->mqtt_prefix', 'shellyplus1-abc')->first()->protocol); } public function test_onboarding_does_not_hijack_a_different_device_on_a_reused_ip(): void { // Device A previously held this IP (a different Shelly). After DHCP reassignment a new // Shelly answers at the same IP — it must NOT overwrite A. $a = Device::create(['name' => 'A', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.78', 'mqtt_prefix' => 'other-shelly']]); $this->fakeShelly(); // identifies as shellyplus1-abc $device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78'); $this->assertNotSame($a->id, $device->id); $this->assertSame('other-shelly', data_get($a->fresh()->config, 'mqtt_prefix')); $this->assertSame('shellyplus1-abc', data_get($device->config, 'mqtt_prefix')); } public function test_http_driver_controls_via_rpc_and_audits(): void { $this->fakeShelly(); $device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78'); Http::fake(['*/rpc' => Http::response(['id' => 1, 'result' => []])]); // reset recorded calls target $entity = $device->entities()->where('key', 'switch:0')->first(); $command = app(DeviceCommandService::class)->setOn($entity->load('device', 'state'), false); $this->assertSame('ok', $command->result); Http::assertSent(fn ($request) => str_contains($request->url(), '/rpc') && ($request->data()['method'] ?? '') === 'Switch.Set' && ($request->data()['params']['on'] ?? null) === false); } public function test_poller_applies_status_and_marks_reachable(): void { $device = Device::create(['name' => 'Flur', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.78']]); $this->fakeShelly(switchOn: false); $this->assertTrue(app(ShellyPoller::class)->poll($device)); $this->assertFalse(data_get($device->entities()->where('key', 'switch:0')->first()->state->state, 'on')); $this->assertNotNull($device->fresh()->last_seen_at); } public function test_poller_returns_false_when_unreachable(): void { $device = Device::create(['name' => 'Flur', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.99']]); Http::fake(['*/rpc' => fn () => throw new \Illuminate\Http\Client\ConnectionException('unreachable')]); $this->assertFalse(app(ShellyPoller::class)->poll($device)); } public function test_rpc_reachable_reflects_availability(): void { $this->fakeShelly(); $this->assertTrue(app(ShellyRpc::class)->reachable('10.10.30.78')); Http::fake(['*/rpc' => fn () => throw new \Illuminate\Http\Client\ConnectionException('nope')]); $this->assertFalse(app(ShellyRpc::class)->reachable('10.10.30.99')); } }