diff --git a/app/Models/Device.php b/app/Models/Device.php index 3583f48..2eeb25b 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -20,14 +20,6 @@ class Device extends Model protected $guarded = ['id']; - /** - * Set the MAC address attribute, normalizing to uppercase. - */ - public function setMacAddressAttribute(?string $value): void - { - $this->attributes['mac_address'] = $value ? mb_strtoupper($value) : null; - } - protected $casts = [ 'battery_notification_sent' => 'boolean', 'proxy_cloud' => 'boolean', diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index b4daf5e..7b55a73 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -37,32 +37,21 @@ class Playlist extends Model return false; } - // Get user's timezone or fall back to app timezone - $timezone = $this->device->user->timezone ?? config('app.timezone'); - $now = now($timezone); - - // Check weekday (using timezone-aware time) - if ($this->weekdays !== null && ! in_array($now->dayOfWeek, $this->weekdays)) { + // Check weekday + if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) { return false; } if ($this->active_from !== null && $this->active_until !== null) { - // Create timezone-aware datetime objects for active_from and active_until - $activeFrom = $now->copy() - ->setTimeFrom($this->active_from) - ->timezone($timezone); - - $activeUntil = $now->copy() - ->setTimeFrom($this->active_until) - ->timezone($timezone); + $now = now(); // Handle time ranges that span across midnight - if ($activeFrom > $activeUntil) { + if ($this->active_from > $this->active_until) { // Time range spans midnight (e.g., 09:01 to 03:58) - if ($now >= $activeFrom || $now <= $activeUntil) { + if ($now >= $this->active_from || $now <= $this->active_until) { return true; } - } elseif ($now >= $activeFrom && $now <= $activeUntil) { + } elseif ($now >= $this->active_from && $now <= $this->active_until) { return true; } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 9132d6c..2915247 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -55,13 +55,6 @@ class Plugin extends Model $model->uuid = Str::uuid(); } }); - - static::updating(function ($model): void { - // Reset image cache when markup changes - if ($model->isDirty('render_markup')) { - $model->current_image = null; - } - }); } public function user() diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php index c8f2b58..f87e71c 100644 --- a/app/Services/Plugin/Parsers/IcalResponseParser.php +++ b/app/Services/Plugin/Parsers/IcalResponseParser.php @@ -34,7 +34,7 @@ class IcalResponseParser implements ResponseParser $filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool { $startDate = $this->asCarbon($event['DTSTART'] ?? null); - if (! $startDate instanceof Carbon) { + if (!$startDate instanceof \Carbon\Carbon) { return false; } diff --git a/composer.lock b/composer.lock index 1b578bf..a2e83af 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.4", + "version": "3.369.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2aa1ef195e90140d733382e4341732ce113024f5" + "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5", - "reference": "2aa1ef195e90140d733382e4341732ce113024f5", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", + "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", "shasum": "" }, "require": { @@ -85,7 +85,7 @@ "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", - "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" + "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.1" }, - "time": "2025-12-29T19:07:47+00:00" + "time": "2025-12-22T19:13:21+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -950,24 +950,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.4", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5" + "phpoption/phpoption": "^1.9.3" }, "require-dev": { - "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "autoload": { @@ -996,7 +996,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" }, "funding": [ { @@ -1008,7 +1008,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:43:20+00:00" + "time": "2024-07-20T21:45:45+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1617,16 +1617,16 @@ }, { "name": "laravel/framework", - "version": "v12.44.0", + "version": "v12.43.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "592bbf1c036042958332eb98e3e8131b29102f33" + "reference": "195b893593a9298edee177c0844132ebaa02102f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", - "reference": "592bbf1c036042958332eb98e3e8131b29102f33", + "url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f", + "reference": "195b893593a9298edee177c0844132ebaa02102f", "shasum": "" }, "require": { @@ -1835,7 +1835,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-12-23T15:29:43+00:00" + "time": "2025-12-16T18:53:08+00:00" }, { "name": "laravel/prompts", @@ -3885,16 +3885,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.5", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -3944,7 +3944,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -3956,7 +3956,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:41:33+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "phpseclib/phpseclib", @@ -7723,26 +7723,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.3", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "955e7815d677a3eaa7075231212f2110983adecc" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", - "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", + "graham-campbell/result-type": "^1.1.3", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", - "symfony/polyfill-ctype": "^1.26", - "symfony/polyfill-mbstring": "^1.26", - "symfony/polyfill-php80": "^1.26" + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -7791,7 +7791,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -7803,7 +7803,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:49:13+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "voku/portable-ascii", @@ -10122,16 +10122,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.2", + "version": "12.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57", + "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57", "shasum": "" }, "require": { @@ -10146,7 +10146,7 @@ "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^2.0.1" + "theseer/tokenizer": "^2.0" }, "require-dev": { "phpunit/phpunit": "^12.5.1" @@ -10187,7 +10187,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.1" }, "funding": [ { @@ -10207,7 +10207,7 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:03:04+00:00" + "time": "2025-12-08T07:17:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10561,16 +10561,16 @@ }, { "name": "rector/rector", - "version": "2.3.0", + "version": "2.2.14", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "f7166355dcf47482f27be59169b0825995f51c7d" + "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", - "reference": "f7166355dcf47482f27be59169b0825995f51c7d", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6d56bb0e94d4df4f57a78610550ac76ab403657d", + "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d", "shasum": "" }, "require": { @@ -10609,7 +10609,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.0" + "source": "https://github.com/rectorphp/rector/tree/2.2.14" }, "funding": [ { @@ -10617,7 +10617,7 @@ "type": "github" } ], - "time": "2025-12-25T22:00:18+00:00" + "time": "2025-12-09T10:57:55+00:00" }, { "name": "sebastian/cli-parser", diff --git a/database/factories/DevicePaletteFactory.php b/database/factories/DevicePaletteFactory.php index 1d7ed2d..a672873 100644 --- a/database/factories/DevicePaletteFactory.php +++ b/database/factories/DevicePaletteFactory.php @@ -20,7 +20,7 @@ class DevicePaletteFactory extends Factory public function definition(): array { return [ - 'id' => 'test-'.$this->faker->unique()->slug(), + 'id' => 'test-' . $this->faker->unique()->slug(), 'name' => $this->faker->words(3, true), 'grays' => $this->faker->randomElement([2, 4, 16, 256]), 'colors' => $this->faker->optional()->passthrough([ diff --git a/routes/api.php b/routes/api.php index d1dbcac..9721a0f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,7 +18,7 @@ use Illuminate\Support\Str; Route::get('/display', function (Request $request) { $mac_address = $request->header('id'); $access_token = $request->header('access-token'); - $device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) + $device = Device::where('mac_address', $mac_address) ->where('api_key', $access_token) ->first(); @@ -29,7 +29,7 @@ Route::get('/display', function (Request $request) { if ($auto_assign_user) { // Create a new device and assign it to this user $device = Device::create([ - 'mac_address' => mb_strtoupper($mac_address ?? ''), + 'mac_address' => $mac_address, 'api_key' => $access_token, 'user_id' => $auto_assign_user->id, 'name' => "{$auto_assign_user->name}'s TRMNL", @@ -204,7 +204,7 @@ Route::get('/setup', function (Request $request) { ], 404); } - $device = Device::where('mac_address', mb_strtoupper($mac_address))->first(); + $device = Device::where('mac_address', $mac_address)->first(); if (! $device) { // Check if there's a user with assign_new_devices enabled @@ -219,7 +219,7 @@ Route::get('/setup', function (Request $request) { // Create a new device and assign it to this user $device = Device::create([ - 'mac_address' => mb_strtoupper($mac_address), + 'mac_address' => $mac_address, 'api_key' => Str::random(22), 'user_id' => $auto_assign_user->id, 'name' => "{$auto_assign_user->name}'s TRMNL", @@ -345,7 +345,7 @@ Route::post('/display/update', function (Request $request) { Route::post('/screens', function (Request $request) { $mac_address = $request->header('id'); $access_token = $request->header('access-token'); - $device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) + $device = Device::where('mac_address', $mac_address) ->where('api_key', $access_token) ->first(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index aff6758..726f313 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -954,72 +954,3 @@ test('setup endpoint handles non-existent device model gracefully', function (): expect($device)->not->toBeNull() ->and($device->device_model_id)->toBeNull(); }); - -test('setup endpoint matches MAC address case-insensitively', function (): void { - // Create device with lowercase MAC address - $device = Device::factory()->create([ - 'mac_address' => 'a1:b2:c3:d4:e5:f6', - 'api_key' => 'test-api-key', - 'friendly_id' => 'test-device', - ]); - - // Request with uppercase MAC address should still match - $response = $this->withHeaders([ - 'id' => 'A1:B2:C3:D4:E5:F6', - ])->get('/api/setup'); - - $response->assertOk() - ->assertJson([ - 'status' => 200, - 'api_key' => 'test-api-key', - 'friendly_id' => 'test-device', - 'message' => 'Welcome to TRMNL BYOS', - ]); -}); - -test('display endpoint matches MAC address case-insensitively', function (): void { - // Create device with lowercase MAC address - $device = Device::factory()->create([ - 'mac_address' => 'a1:b2:c3:d4:e5:f6', - 'api_key' => 'test-api-key', - 'current_screen_image' => 'test-image', - ]); - - // Request with uppercase MAC address should still match - $response = $this->withHeaders([ - 'id' => 'A1:B2:C3:D4:E5:F6', - 'access-token' => $device->api_key, - 'rssi' => -70, - 'battery_voltage' => 3.8, - 'fw-version' => '1.0.0', - ])->get('/api/display'); - - $response->assertOk() - ->assertJson([ - 'status' => '0', - 'filename' => 'test-image.bmp', - ]); -}); - -test('screens endpoint matches MAC address case-insensitively', function (): void { - Queue::fake(); - - // Create device with uppercase MAC address - $device = Device::factory()->create([ - 'mac_address' => 'A1:B2:C3:D4:E5:F6', - 'api_key' => 'test-api-key', - ]); - - // Request with lowercase MAC address should still match - $response = $this->withHeaders([ - 'id' => 'a1:b2:c3:d4:e5:f6', - 'access-token' => $device->api_key, - ])->post('/api/screens', [ - 'image' => [ - 'content' => '
Test content
', - ], - ]); - - $response->assertOk(); - Queue::assertPushed(GenerateScreenJob::class); -}); diff --git a/tests/Feature/ImageGenerationServiceTest.php b/tests/Feature/ImageGenerationServiceTest.php index 07bb6a6..603205e 100644 --- a/tests/Feature/ImageGenerationServiceTest.php +++ b/tests/Feature/ImageGenerationServiceTest.php @@ -324,30 +324,6 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void expect($plugin->current_image)->toBe('test-uuid'); }); -it('cache is reset when plugin markup changes', function (): void { - // Create a plugin with cached image - $plugin = App\Models\Plugin::factory()->create([ - 'current_image' => 'cached-uuid', - 'render_markup' => '
Original markup
', - ]); - - // Create devices with standard dimensions (cacheable) - Device::factory()->count(2)->create([ - 'width' => 800, - 'height' => 480, - 'rotate' => 0, - ]); - - // Update the plugin markup - $plugin->update([ - 'render_markup' => '
Updated markup
', - ]); - - // Assert cache was reset when markup changed - $plugin->refresh(); - expect($plugin->current_image)->toBeNull(); -}); - it('determines correct image format from device model', function (): void { // Test BMP format detection $bmpModel = DeviceModel::factory()->create([ diff --git a/tests/Feature/PlaylistSchedulingTest.php b/tests/Feature/PlaylistSchedulingTest.php index 18d0032..aea4923 100644 --- a/tests/Feature/PlaylistSchedulingTest.php +++ b/tests/Feature/PlaylistSchedulingTest.php @@ -130,48 +130,3 @@ test('playlist isActiveNow handles normal time ranges correctly', function (): v Carbon::setTestNow(Carbon::create(2024, 1, 1, 20, 0, 0)); expect($playlist->isActiveNow())->toBeFalse(); }); - -test('playlist scheduling respects user timezone preference', function (): void { - // Create a user with a timezone that's UTC+1 (e.g., Europe/Berlin) - // This simulates the bug where setting 00:15 doesn't work until one hour later - $user = User::factory()->create([ - 'timezone' => 'Europe/Berlin', // UTC+1 in winter, UTC+2 in summer - ]); - - $device = Device::factory()->create(['user_id' => $user->id]); - - // Create a playlist that should be active from 00:15 to 01:00 in the user's timezone - $playlist = Playlist::factory()->create([ - 'device_id' => $device->id, - 'is_active' => true, - 'active_from' => '00:15', - 'active_until' => '01:00', - 'weekdays' => null, - ]); - - // Set test time to 00:15 in the user's timezone (Europe/Berlin) - // In January, Europe/Berlin is UTC+1, so 00:15 Berlin time = 23:15 UTC the previous day - // But Carbon::setTestNow uses UTC by default, so we need to set it to the UTC equivalent - // For January 1, 2024 at 00:15 Berlin time (UTC+1), that's December 31, 2023 at 23:15 UTC - $berlinTime = Carbon::create(2024, 1, 1, 0, 15, 0, 'Europe/Berlin'); - Carbon::setTestNow($berlinTime->utc()); - - // The playlist should be active at 00:15 in the user's timezone - // This test should pass after the fix, but will fail with the current bug - expect($playlist->isActiveNow())->toBeTrue(); - - // Test at 00:30 in user's timezone - should still be active - $berlinTime = Carbon::create(2024, 1, 1, 0, 30, 0, 'Europe/Berlin'); - Carbon::setTestNow($berlinTime->utc()); - expect($playlist->isActiveNow())->toBeTrue(); - - // Test at 01:15 in user's timezone - should NOT be active (past the end time) - $berlinTime = Carbon::create(2024, 1, 1, 1, 15, 0, 'Europe/Berlin'); - Carbon::setTestNow($berlinTime->utc()); - expect($playlist->isActiveNow())->toBeFalse(); - - // Test at 00:10 in user's timezone - should NOT be active (before start time) - $berlinTime = Carbon::create(2024, 1, 1, 0, 10, 0, 'Europe/Berlin'); - Carbon::setTestNow($berlinTime->utc()); - expect($playlist->isActiveNow())->toBeFalse(); -});