setInventoryFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices_status' => [ 'device-key-1' => [ '_dev_info' => [ 'id' => 'shelly-plus-01', 'code' => 'SPSW-001PE16EU', 'model' => 'Shelly Plus 1PM', 'online' => 1, ], 'name' => 'Entry Gate', 'wifi_sta' => [ 'ip' => '10.32.0.11', ], 'status' => [ 'switch:0' => [ 'output' => false, 'name' => 'Entrance Relay', ], ], ], 'device-key-2' => [ '_dev_info' => [ 'id' => 'shelly-pro-03', 'code' => 'SPSW-201XE16EU', 'model' => 'Shelly Pro 2PM', ], 'name' => 'Machine Cabinet', 'status' => [ 'eth' => [ 'ip' => '10.32.0.12', ], ], 'relays' => [ ['ison' => false, 'name' => 'Program Picker'], ], ], 'device-key-3' => [ '_dev_info' => [ 'id' => 'shelly-legacy-02', 'code' => 'SHSW-1', 'model' => 'Shelly 1', 'online' => 0, ], 'relays' => [ ['ison' => false], ], ], 'device-key-4' => [ '_dev_info' => [ 'id' => 'shelly-sensor-01', 'code' => 'SHHT-1', 'model' => 'Shelly H&T', 'online' => 1, ], 'sensor' => [ 'temperature' => 21.5, ], ], ], ], ]; }) ->setDeviceListFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices' => [ 'shelly-plus-01' => [ 'id' => 'shelly-plus-01', 'name' => 'Gate From Shelly Cloud', 'type' => 'SPSW-001PE16EU', 'cloud_online' => true, ], 'shelly-pro-03' => [ 'id' => 'shelly-pro-03', 'name' => 'Program Picker From Shelly Cloud', 'type' => 'SPSW-201XE16EU', 'cloud_online' => false, ], ], ], ]; }); expect($inventory->listRelayOptions())->toBe([ [ 'id' => 'shelly-plus-01', 'name' => 'Gate From Shelly Cloud (Shelly Plus 1PM)', 'device_id' => 'shelly-plus-01', 'device_name' => 'Gate From Shelly Cloud', 'cloud_name' => 'Gate From Shelly Cloud', 'device_type' => 'Shelly Plus 1PM', 'code' => 'SPSW-001PE16EU', 'device_model' => 'SPSW-001PE16EU', 'device_generation' => 2, 'control_type' => 'Switch', 'control_name' => 'Entrance Relay', 'status_color' => 'Green', 'online' => true, ], [ 'id' => 'shelly-pro-03', 'name' => 'Program Picker From Shelly Cloud (Shelly Pro 2PM)', 'device_id' => 'shelly-pro-03', 'device_name' => 'Program Picker From Shelly Cloud', 'cloud_name' => 'Program Picker From Shelly Cloud', 'device_type' => 'Shelly Pro 2PM', 'code' => 'SPSW-201XE16EU', 'device_model' => 'SPSW-201XE16EU', 'device_generation' => 2, 'control_type' => 'Relay', 'control_name' => 'Program Picker', 'status_color' => 'Red', 'online' => false, ], [ 'id' => 'shelly-legacy-02', 'name' => 'shelly-legacy-02 (Shelly 1)', 'device_id' => 'shelly-legacy-02', 'device_name' => null, 'cloud_name' => null, 'device_type' => 'Shelly 1', 'code' => 'SHSW-1', 'device_model' => 'SHSW-1', 'device_generation' => 1, 'control_type' => 'Relay', 'control_name' => null, 'status_color' => 'Red', 'online' => false, ], ]); }); it('falls back to local device and control names when Shelly cloud list metadata is unavailable', function (): void { $inventory = (new shelly_relay_inventory()) ->setInventoryFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices_status' => [ 'device-key-1' => [ '_dev_info' => [ 'id' => 'shelly-plus-01', 'code' => 'SPSW-001PE16EU', 'model' => 'Shelly Plus 1PM', 'online' => 1, ], 'ip' => '10.32.0.21', 'name' => 'Entry Gate', 'status' => [ 'switch:0' => [ 'output' => false, 'name' => 'Entrance Relay', ], ], ], ], ], ]; }) ->setDeviceListFetcher(static function (): array { return [ 'isok' => false, 'errors' => [ '404' => 'Requested method was not found', ], ]; }); expect($inventory->listRelayOptions())->toBe([ [ 'id' => 'shelly-plus-01', 'name' => 'Entry Gate / Entrance Relay (Shelly Plus 1PM)', 'device_id' => 'shelly-plus-01', 'device_name' => 'Entry Gate', 'cloud_name' => null, 'device_type' => 'Shelly Plus 1PM', 'code' => 'SPSW-001PE16EU', 'device_model' => 'SPSW-001PE16EU', 'device_generation' => 2, 'control_type' => 'Switch', 'control_name' => 'Entrance Relay', 'status_color' => 'Green', 'online' => true, ], ]); }); it('keeps Shelly 1 Mini Gen3 type, model, and generation aligned with Shelly metadata', function (): void { $inventory = (new shelly_relay_inventory()) ->setInventoryFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices_status' => [ 'device-key-1' => [ '_dev_info' => [ 'id' => 'e4b3231f6410', 'model' => 'S3SW-001X8EU', 'type' => 'Shelly 1 Mini Gen3', 'online' => 1, ], 'name' => 'Roskilde indkørselsport', 'wifi_sta' => [ 'ip' => '192.168.1.2', ], 'status' => [ 'switch:0' => [ 'output' => false, ], ], ], ], ], ]; }) ->setDeviceListFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices' => [ 'e4b3231f6410' => [ 'id' => 'e4b3231f6410', 'name' => 'Roskilde indkørselsport', 'type' => 'S3SW-001X8EU', 'cloud_online' => true, ], ], ], ]; }); expect($inventory->listRelayOptions())->toBe([[ 'id' => 'e4b3231f6410', 'name' => 'Roskilde indkørselsport (Shelly 1 Mini Gen3)', 'device_id' => 'e4b3231f6410', 'device_name' => 'Roskilde indkørselsport', 'cloud_name' => 'Roskilde indkørselsport', 'device_type' => 'Shelly 1 Mini Gen3', 'code' => 'S3SW-001X8EU', 'device_model' => 'S3SW-001X8EU', 'device_generation' => 3, 'control_type' => 'Switch', 'control_name' => null, 'status_color' => 'Green', 'online' => true, ]]); }); it('only includes local IP addresses for internal relay option lookups', function (): void { $inventory = (new shelly_relay_inventory()) ->setInventoryFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices_status' => [ 'device-key-1' => [ '_dev_info' => [ 'id' => 'shelly-plus-01', 'code' => 'SPSW-001PE16EU', 'model' => 'Shelly Plus 1PM', 'online' => 1, ], 'wifi_sta' => [ 'ip' => '10.32.0.11', ], 'status' => [ 'switch:0' => [ 'output' => false, ], ], ], ], ], ]; }) ->setDeviceListFetcher(static function (): array { return [ 'isok' => true, 'data' => [ 'devices' => [], ], ]; }); $publicOptions = $inventory->listRelayOptions(); $internalOptions = $inventory->listRelayOptions(true); expect($publicOptions[0])->not->toHaveKey('local_ip'); expect($internalOptions[0])->toHaveKey('local_ip'); expect($internalOptions[0]['local_ip'])->toBe('10.32.0.11'); }); it('fails fast when Shelly inventory does not include owned devices status', function (): void { $inventory = (new shelly_relay_inventory())->setInventoryFetcher(static function (): array { return [ 'isok' => true, 'data' => [], ]; }); expect(fn() => $inventory->listRelayOptions()) ->toThrow(Exception::class, 'Shelly relay inventory response was missing devices_status'); });