From 7e355c2d92b052b9f2a2defa07d3b4fcfc352dc5 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 10 Jul 2025 17:47:42 +0200 Subject: [PATCH] feat: add function to pause screen generation for up to 480min chore: code quality --- app/Models/Device.php | 43 +++++++----- app/Models/Playlist.php | 1 + app/Notifications/Channels/WebhookChannel.php | 2 +- app/Notifications/Messages/WebhookMessage.php | 4 +- ...64606_add_pause_until_to_devices_table.php | 22 ++++++ .../livewire/devices/configure.blade.php | 7 ++ .../views/livewire/devices/manage.blade.php | 68 +++++++++++++++++-- .../views/livewire/plugins/api.blade.php | 10 ++- routes/api.php | 53 ++++++++++++++- tests/Feature/Api/DeviceEndpointsTest.php | 22 ++++++ 10 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php diff --git a/app/Models/Device.php b/app/Models/Device.php index 6d5993b..21f5c6c 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -2,6 +2,8 @@ namespace App\Models; +use Carbon\Carbon; +use DateTimeInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -27,6 +29,7 @@ class Device extends Model 'sleep_mode_from' => 'datetime:H:i', 'sleep_mode_to' => 'datetime:H:i', 'special_function' => 'string', + 'pause_until' => 'datetime', ]; public function getBatteryPercentAttribute() @@ -190,35 +193,41 @@ class Device extends Model return $this->belongsTo(User::class); } - public function isSleepModeActive(?\DateTimeInterface $now = null): bool + public function isSleepModeActive(?DateTimeInterface $now = null): bool { - if (!$this->sleep_mode_enabled || !$this->sleep_mode_from || !$this->sleep_mode_to) { + if (! $this->sleep_mode_enabled || ! $this->sleep_mode_from || ! $this->sleep_mode_to) { return false; } - $now = $now ? \Carbon\Carbon::instance($now) : now(); - $from = $this->sleep_mode_from instanceof \Carbon\Carbon ? $this->sleep_mode_from : \Carbon\Carbon::createFromFormat('H:i:s', $this->sleep_mode_from); - $to = $this->sleep_mode_to instanceof \Carbon\Carbon ? $this->sleep_mode_to : \Carbon\Carbon::createFromFormat('H:i:s', $this->sleep_mode_to); + + $now = $now ? Carbon::instance($now) : now(); + // Handle overnight ranges (e.g. 22:00 to 06:00) - return $from < $to - ? $now->between($from, $to) - : ($now->gte($from) || $now->lte($to)); + return $this->sleep_mode_from < $this->sleep_mode_to + ? $now->between($this->sleep_mode_from, $this->sleep_mode_to) + : ($now->gte($this->sleep_mode_from) || $now->lte($this->sleep_mode_to)); } - public function getSleepModeEndsInSeconds(?\DateTimeInterface $now = null): ?int + public function getSleepModeEndsInSeconds(?DateTimeInterface $now = null): ?int { - if (!$this->sleep_mode_enabled || !$this->sleep_mode_from || !$this->sleep_mode_to) { + if (! $this->sleep_mode_enabled || ! $this->sleep_mode_from || ! $this->sleep_mode_to) { return null; } - $now = $now ? \Carbon\Carbon::instance($now) : now(); - $from = $this->sleep_mode_from instanceof \Carbon\Carbon ? $this->sleep_mode_from : \Carbon\Carbon::createFromFormat('H:i:s', $this->sleep_mode_from); - $to = $this->sleep_mode_to instanceof \Carbon\Carbon ? $this->sleep_mode_to : \Carbon\Carbon::createFromFormat('H:i:s', $this->sleep_mode_to); + $now = $now ? Carbon::instance($now) : now(); + $from = $this->sleep_mode_from; + $to = $this->sleep_mode_to; // Handle overnight ranges (e.g. 22:00 to 06:00) - if ($from < $to) { - return $now->between($from, $to) ? $now->diffInSeconds($to, false) : null; - } else { - return ($now->gte($from) || $now->lt($to)) ? $now->diffInSeconds($to->addDay(), false) : null; + if ($this->sleep_mode_from < $to) { + return $now->between($from, $to) ? (int) $now->diffInSeconds($to, false) : null; } + + return ($now->gte($from) || $now->lt($to)) ? (int) $now->diffInSeconds($to->addDay(), false) : null; + + } + + public function isPauseActive(): bool + { + return $this->pause_until && $this->pause_until->isFuture(); } } diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index d24356f..d20798c 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -58,6 +58,7 @@ class Playlist extends Model return true; } } + return false; } diff --git a/app/Notifications/Channels/WebhookChannel.php b/app/Notifications/Channels/WebhookChannel.php index f115c44..d116200 100644 --- a/app/Notifications/Channels/WebhookChannel.php +++ b/app/Notifications/Channels/WebhookChannel.php @@ -9,7 +9,7 @@ use GuzzleHttp\Psr7\Response; use Illuminate\Notifications\Notification; use Illuminate\Support\Arr; -class WebhookChannel +class WebhookChannel extends Notification { /** @var Client */ protected $client; diff --git a/app/Notifications/Messages/WebhookMessage.php b/app/Notifications/Messages/WebhookMessage.php index 6da9f55..920c16d 100644 --- a/app/Notifications/Messages/WebhookMessage.php +++ b/app/Notifications/Messages/WebhookMessage.php @@ -2,7 +2,9 @@ namespace App\Notifications\Messages; -final class WebhookMessage +use Illuminate\Notifications\Notification; + +final class WebhookMessage extends Notification { /** * The GET parameters of the request. diff --git a/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php b/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php new file mode 100644 index 0000000..69181df --- /dev/null +++ b/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php @@ -0,0 +1,22 @@ +dateTime('pause_until')->nullable()->after('last_refreshed_at'); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('pause_until'); + }); + } +}; diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index 6d9bb8a..bbfa7d3 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -314,6 +314,13 @@ new class extends Component { @endif + @if($device->isPauseActive()) + + + + + @endif
diff --git a/resources/views/livewire/devices/manage.blade.php b/resources/views/livewire/devices/manage.blade.php index e807190..2ff699d 100644 --- a/resources/views/livewire/devices/manage.blade.php +++ b/resources/views/livewire/devices/manage.blade.php @@ -23,6 +23,8 @@ new class extends Component { public $mirror_device_id = null; + public ?int $pause_duration; + protected $rules = [ 'mac_address' => 'required', 'api_key' => 'required', @@ -75,6 +77,20 @@ new class extends Component { // \App\Jobs\FetchProxyCloudResponses::dispatch(); // } } + + public function pauseDevice($deviceId): void + { + $this->validate([ + 'pause_duration' => 'required|integer', + ]); + $device = auth()->user()->devices()->findOrFail($deviceId); + $pauseUntil = now()->addMinutes($this->pause_duration); + $device->update(['pause_until' => $pauseUntil]); + $this->reset('pause_duration'); + \Flux::modal('pause-device-' . $deviceId)->close(); + $this->devices = auth()->user()->devices; + session()->flash('message', 'Device paused until ' . $pauseUntil->format('H:i')); + } } ?> @@ -93,7 +109,8 @@ new class extends Component {
- +
@@ -138,7 +155,7 @@ new class extends Component {
- +
@if($is_mirror) @@ -216,14 +233,27 @@ new class extends Component {
- + + + + @if($device->isPauseActive()) + + + + @else + + + + + @endif + @@ -238,4 +268,34 @@ new class extends Component {
+ @foreach ($devices as $device) + +
+
+ Pause +
Select how long to pause screen generation for {{ $device->name }}. +
+
+
+
+ + + + + + + +
+
+ + + Cancel + + Save +
+
+
+
+ @endforeach diff --git a/resources/views/livewire/plugins/api.blade.php b/resources/views/livewire/plugins/api.blade.php index 08bcafc..e445dbf 100644 --- a/resources/views/livewire/plugins/api.blade.php +++ b/resources/views/livewire/plugins/api.blade.php @@ -77,7 +77,7 @@ new class extends Component {

- GET + GETPOST {{ route('display.status') }}?device_id={{ $selected_device }}

@@ -88,6 +88,14 @@ new class extends Component {
+
+

Body POST

+
+
+{"default_refresh_interval": 900, "sleep_mode_enabled": true, "pause_until": "2025-07-10T22:00:00+02:00"}
+                    
+
+
diff --git a/routes/api.php b/routes/api.php index 50ed665..519d633 100644 --- a/routes/api.php +++ b/routes/api.php @@ -56,7 +56,11 @@ Route::get('/display', function (Request $request) { ]); } - if ($device->isSleepModeActive()) { + if ($device->isPauseActive()) { + $image_path = 'images/sleep.png'; + $filename = 'sleep.png'; + $refreshTimeOverride = (int) now()->diffInSeconds($device->pause_until); + } elseif ($device->isSleepModeActive()) { $image_path = 'images/sleep.png'; $filename = 'sleep.png'; $refreshTimeOverride = $device->getSleepModeEndsInSeconds() ?? $device->default_refresh_interval; @@ -293,6 +297,11 @@ Route::get('/display/status', function (Request $request) { 'wifi_strength', 'current_screen_image', 'default_refresh_interval', + 'sleep_mode_enabled', + 'sleep_mode_from', + 'sleep_mode_to', + 'special_function', + 'pause_until', 'updated_at', ]), ); @@ -300,6 +309,48 @@ Route::get('/display/status', function (Request $request) { ->name('display.status') ->middleware('auth:sanctum'); +Route::post('/display/status', function (Request $request) { + $request->validate([ + 'device_id' => 'required|exists:devices,id', + 'name' => 'string|max:255', + 'default_refresh_interval' => 'integer|min:1', + 'sleep_mode_enabled' => 'boolean', + 'sleep_mode_from' => 'nullable|date_format:H:i', + 'sleep_mode_to' => 'nullable|date_format:H:i', + 'pause_until' => 'nullable|date|after:now', + ]); + + $deviceId = $request['device_id']; + abort_unless($request->user()->devices->contains($deviceId), 403); + + $fieldsToUpdate = $request->only(['name', 'default_refresh_interval', 'sleep_mode_enabled', 'sleep_mode_from', 'sleep_mode_to', 'pause_until']); + Device::find($deviceId)->update($fieldsToUpdate); + + return response()->json( + Device::find($deviceId)->only([ + 'id', + 'mac_address', + 'name', + 'friendly_id', + 'last_rssi_level', + 'last_battery_voltage', + 'last_firmware_version', + 'battery_percent', + 'wifi_strength', + 'current_screen_image', + 'default_refresh_interval', + 'sleep_mode_enabled', + 'sleep_mode_from', + 'sleep_mode_to', + 'special_function', + 'pause_until', + 'updated_at', + ]), + ); +}) + ->name('display.status.post') + ->middleware('auth:sanctum'); + Route::get('/current_screen', function (Request $request) { $access_token = $request->header('access-token'); $device = Device::where('api_key', $access_token)->first(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index 7cecb65..53fe724 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -816,3 +816,25 @@ test('device not in sleep mode returns normal image', function () { \Carbon\Carbon::setTestNow(); // Clear test time }); + +test('device returns sleep.png and correct refresh time when paused', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'pause_until' => now()->addMinutes(60), + ]); + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk(); + $json = $response->json(); + expect($json['filename'])->toBe('sleep.png'); + expect($json['image_url'])->toContain('sleep.png'); + expect($json['refresh_rate'])->toBeLessThanOrEqual(3600); // ~60 min +});