feat(device): replace daily wakeHour with multi-time wakeTimes (minutes)
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
Frame settings now offer two update-frequency modes: "at specific times" or "every X minutes". Times are stored as an int[] of minutes-since-midnight, allowing multiple slots per day at minute granularity. Backend computes the earliest upcoming slot for X-Interval-Ms and uses the most-recent-past slot as the rotation-due boundary. PWA settings sheet has hour/minute/AM-PM dropdowns with + Add / trash, a live "next update" preview, and a note that changes only take effect at the device's next sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -209,19 +209,52 @@ class DeviceApiControllerTest extends AppWebTestCase
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function test_patch_sets_wake_hour(): void
|
||||
public function test_patch_sets_wake_times(): void
|
||||
{
|
||||
$user = $this->createUser('patchwake@example.com');
|
||||
$device = $this->makeDevice('AA:BB:CC:DD:EE:B4', $user);
|
||||
$client = $this->loginAs($user);
|
||||
|
||||
// 6:00 AM, 3:00 PM, 7:30 PM expressed as minutes since midnight
|
||||
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['wakeHour' => 8]));
|
||||
], json_encode(['wakeTimes' => [6 * 60, 15 * 60, 19 * 60 + 30]]));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(8, $data['wakeHour']);
|
||||
$this->assertSame([360, 900, 1170], $data['wakeTimes']);
|
||||
}
|
||||
|
||||
public function test_patch_rejects_out_of_range_wake_times(): void
|
||||
{
|
||||
$user = $this->createUser('patchwakebad@example.com');
|
||||
$device = $this->makeDevice('AA:BB:CC:DD:EE:B7', $user);
|
||||
$client = $this->loginAs($user);
|
||||
|
||||
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['wakeTimes' => [1500]]));
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function test_patch_clears_wake_times_with_empty_array(): void
|
||||
{
|
||||
$user = $this->createUser('patchwakeclear@example.com');
|
||||
$device = $this->makeDevice('AA:BB:CC:DD:EE:B8', $user);
|
||||
$device->setWakeTimes([6 * 60, 18 * 60]);
|
||||
$em = static::getContainer()->get('doctrine')->getManager();
|
||||
$em->flush();
|
||||
|
||||
$client = $this->loginAs($user);
|
||||
|
||||
$client->request('PATCH', '/api/devices/' . $device->getId(), [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], json_encode(['wakeTimes' => []]));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame([], $data['wakeTimes']);
|
||||
}
|
||||
|
||||
public function test_patch_sets_uniqueness_window(): void
|
||||
|
||||
@@ -336,13 +336,13 @@ class DeviceImageControllerTest extends AppWebTestCase
|
||||
$this->assertResponseStatusCodeSame(204);
|
||||
}
|
||||
|
||||
// When wakeHour is set, X-Interval-Ms should be > 0 and <= 24h in ms
|
||||
public function test_wake_hour_interval_used_when_set(): void
|
||||
// When wakeTimes is set, X-Interval-Ms should be > 0 and <= 24h in ms
|
||||
public function test_wake_times_interval_used_when_set(): void
|
||||
{
|
||||
$setup = $this->createTestSetup();
|
||||
$device = $setup['device'];
|
||||
|
||||
$device->setWakeHour(3)->setTimezone('UTC');
|
||||
$device->setWakeTimes([3 * 60])->setTimezone('UTC');
|
||||
$this->em()->flush();
|
||||
|
||||
$this->client->request('GET', '/api/device/' . self::MAC . '/image');
|
||||
@@ -353,6 +353,26 @@ class DeviceImageControllerTest extends AppWebTestCase
|
||||
$this->assertLessThanOrEqual(24 * 60 * 60 * 1000, $intervalMs);
|
||||
}
|
||||
|
||||
// With multiple wake times, X-Interval-Ms must point to the *earliest*
|
||||
// upcoming time, not just the first in the list.
|
||||
public function test_wake_times_picks_earliest_upcoming(): void
|
||||
{
|
||||
$setup = $this->createTestSetup();
|
||||
$device = $setup['device'];
|
||||
|
||||
// Use a fixed UTC tz; with three slots evenly spread, the gap to the
|
||||
// next slot can never exceed 24h / count = 8h.
|
||||
$device->setWakeTimes([6 * 60, 14 * 60, 22 * 60])->setTimezone('UTC');
|
||||
$this->em()->flush();
|
||||
|
||||
$this->client->request('GET', '/api/device/' . self::MAC . '/image');
|
||||
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$intervalMs = (int) $this->client->getResponse()->headers->get('X-Interval-Ms');
|
||||
$this->assertGreaterThan(0, $intervalMs);
|
||||
$this->assertLessThanOrEqual(8 * 60 * 60 * 1000, $intervalMs);
|
||||
}
|
||||
|
||||
// Returns 204 when RenderedAsset has Ready status but filePath is null (device.poll.no_asset path)
|
||||
public function test_returns_204_when_ready_asset_has_null_file_path(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user