diff --git a/app/Models/Device.php b/app/Models/Device.php index 2eeb25b..3583f48 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -20,6 +20,14 @@ 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 7b55a73..b4daf5e 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -37,21 +37,32 @@ class Playlist extends Model return false; } - // Check weekday - if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) { + // 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)) { return false; } if ($this->active_from !== null && $this->active_until !== null) { - $now = now(); + // 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); // Handle time ranges that span across midnight - if ($this->active_from > $this->active_until) { + if ($activeFrom > $activeUntil) { // Time range spans midnight (e.g., 09:01 to 03:58) - if ($now >= $this->active_from || $now <= $this->active_until) { + if ($now >= $activeFrom || $now <= $activeUntil) { return true; } - } elseif ($now >= $this->active_from && $now <= $this->active_until) { + } elseif ($now >= $activeFrom && $now <= $activeUntil) { return true; } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 2915247..9132d6c 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -55,6 +55,13 @@ 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 f87e71c..c8f2b58 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\Carbon) { + if (! $startDate instanceof Carbon) { return false; } diff --git a/composer.lock b/composer.lock index a2e83af..1b578bf 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.1", + "version": "3.369.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3" + "reference": "2aa1ef195e90140d733382e4341732ce113024f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", - "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5", + "reference": "2aa1ef195e90140d733382e4341732ce113024f5", "shasum": "" }, "require": { @@ -85,7 +85,7 @@ "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", - "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" + "symfony/filesystem": "^v5.4.45 || ^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.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.4" }, - "time": "2025-12-22T19:13:21+00:00" + "time": "2025-12-29T19:07:47+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -950,24 +950,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "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.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -1008,7 +1008,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1617,16 +1617,16 @@ }, { "name": "laravel/framework", - "version": "v12.43.1", + "version": "v12.44.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "195b893593a9298edee177c0844132ebaa02102f" + "reference": "592bbf1c036042958332eb98e3e8131b29102f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f", - "reference": "195b893593a9298edee177c0844132ebaa02102f", + "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", + "reference": "592bbf1c036042958332eb98e3e8131b29102f33", "shasum": "" }, "require": { @@ -1835,7 +1835,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-12-16T18:53:08+00:00" + "time": "2025-12-23T15:29:43+00:00" }, { "name": "laravel/prompts", @@ -3885,16 +3885,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "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.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -3956,7 +3956,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", @@ -7723,26 +7723,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "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.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -7803,7 +7803,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -10122,16 +10122,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.1", + "version": "12.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57" + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57", - "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", "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" + "theseer/tokenizer": "^2.0.1" }, "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.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" }, "funding": [ { @@ -10207,7 +10207,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:17:58+00:00" + "time": "2025-12-24T07:03:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10561,16 +10561,16 @@ }, { "name": "rector/rector", - "version": "2.2.14", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d" + "reference": "f7166355dcf47482f27be59169b0825995f51c7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/6d56bb0e94d4df4f57a78610550ac76ab403657d", - "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", + "reference": "f7166355dcf47482f27be59169b0825995f51c7d", "shasum": "" }, "require": { @@ -10609,7 +10609,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.14" + "source": "https://github.com/rectorphp/rector/tree/2.3.0" }, "funding": [ { @@ -10617,7 +10617,7 @@ "type": "github" } ], - "time": "2025-12-09T10:57:55+00:00" + "time": "2025-12-25T22:00:18+00:00" }, { "name": "sebastian/cli-parser", diff --git a/database/factories/DevicePaletteFactory.php b/database/factories/DevicePaletteFactory.php index a672873..1d7ed2d 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 9721a0f..d1dbcac 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', $mac_address) + $device = Device::where('mac_address', mb_strtoupper($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' => $mac_address, + 'mac_address' => mb_strtoupper($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', $mac_address)->first(); + $device = Device::where('mac_address', mb_strtoupper($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' => $mac_address, + 'mac_address' => mb_strtoupper($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', $mac_address) + $device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) ->where('api_key', $access_token) ->first(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index 726f313..aff6758 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -954,3 +954,72 @@ 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' => '