mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: add sleep mode
This commit is contained in:
parent
f767d39c8f
commit
4b748b102b
5 changed files with 232 additions and 59 deletions
|
|
@ -23,6 +23,10 @@ class Device extends Model
|
|||
'height' => 'integer',
|
||||
'rotate' => 'integer',
|
||||
'last_refreshed_at' => 'datetime',
|
||||
'sleep_mode_enabled' => 'boolean',
|
||||
'sleep_mode_from' => 'datetime:H:i',
|
||||
'sleep_mode_to' => 'datetime:H:i',
|
||||
'special_function' => 'string',
|
||||
];
|
||||
|
||||
public function getBatteryPercentAttribute()
|
||||
|
|
@ -185,4 +189,36 @@ class Device extends Model
|
|||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isSleepModeActive(?\DateTimeInterface $now = null): bool
|
||||
{
|
||||
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);
|
||||
// Handle overnight ranges (e.g. 22:00 to 06:00)
|
||||
return $from < $to
|
||||
? $now->between($from, $to)
|
||||
: ($now->gte($from) || $now->lte($to));
|
||||
}
|
||||
|
||||
public function getSleepModeEndsInSeconds(?\DateTimeInterface $now = null): ?int
|
||||
{
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$table->boolean('sleep_mode_enabled')->default(false);
|
||||
$table->time('sleep_mode_from')->nullable();
|
||||
$table->time('sleep_mode_to')->nullable();
|
||||
$table->string('special_function')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$table->dropColumn(['sleep_mode_enabled', 'sleep_mode_from', 'sleep_mode_to', 'special_function']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -20,6 +20,12 @@ new class extends Component {
|
|||
public $rotate;
|
||||
public $image_format;
|
||||
|
||||
// Sleep mode and special function
|
||||
public $sleep_mode_enabled = false;
|
||||
public $sleep_mode_from;
|
||||
public $sleep_mode_to;
|
||||
public $special_function;
|
||||
|
||||
// Playlist properties
|
||||
public $playlists;
|
||||
public $playlist_name;
|
||||
|
|
@ -53,6 +59,10 @@ new class extends Component {
|
|||
$this->playlists = $device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
$this->firmwares = \App\Models\Firmware::orderBy('latest', 'desc')->orderBy('created_at', 'desc')->get();
|
||||
$this->selected_firmware_id = $this->firmwares->where('latest', true)->first()?->id;
|
||||
$this->sleep_mode_enabled = $device->sleep_mode_enabled ?? false;
|
||||
$this->sleep_mode_from = optional($device->sleep_mode_from)->format('H:i');
|
||||
$this->sleep_mode_to = optional($device->sleep_mode_to)->format('H:i');
|
||||
$this->special_function = $device->special_function;
|
||||
|
||||
return view('livewire.devices.configure', [
|
||||
'image' => ($current_image_uuid) ? url($current_image_path) : null,
|
||||
|
|
@ -80,6 +90,10 @@ new class extends Component {
|
|||
'height' => 'required|integer|min:1',
|
||||
'rotate' => 'required|integer|min:0|max:359',
|
||||
'image_format' => 'required|string',
|
||||
'sleep_mode_enabled' => 'boolean',
|
||||
'sleep_mode_from' => 'nullable|date_format:H:i',
|
||||
'sleep_mode_to' => 'nullable|date_format:H:i',
|
||||
'special_function' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$this->device->update([
|
||||
|
|
@ -91,6 +105,10 @@ new class extends Component {
|
|||
'height' => $this->height,
|
||||
'rotate' => $this->rotate,
|
||||
'image_format' => $this->image_format,
|
||||
'sleep_mode_enabled' => $this->sleep_mode_enabled,
|
||||
'sleep_mode_from' => $this->sleep_mode_from,
|
||||
'sleep_mode_to' => $this->sleep_mode_to,
|
||||
'special_function' => $this->special_function,
|
||||
]);
|
||||
|
||||
Flux::modal('edit-device')->close();
|
||||
|
|
@ -332,6 +350,7 @@ new class extends Component {
|
|||
|
||||
<flux:input label="Friendly ID" wire:model="friendly_id"/>
|
||||
<flux:input label="MAC Address" wire:model="mac_address"/>
|
||||
<flux:separator class="my-4" text="Advanced Device Settings" />
|
||||
<div class="flex gap-4">
|
||||
<flux:input label="Width (px)" wire:model="width" type="number" />
|
||||
<flux:input label="Height (px)" wire:model="height" type="number"/>
|
||||
|
|
@ -345,6 +364,28 @@ new class extends Component {
|
|||
<flux:input label="Default Refresh Interval (seconds)" wire:model="default_refresh_interval"
|
||||
type="number"/>
|
||||
|
||||
<flux:separator class="my-4" text="Special Functions" />
|
||||
<flux:select label="Special Function" wire:model="special_function">
|
||||
<flux:select.option value="identify">Identify</flux:select.option>
|
||||
<flux:select.option value="sleep">Sleep</flux:select.option>
|
||||
<flux:select.option value="add_wifi">Add WiFi</flux:select.option>
|
||||
</flux:select>
|
||||
|
||||
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<flux:switch wire:model.live="sleep_mode_enabled"/>
|
||||
<div>
|
||||
<div class="font-semibold">Sleep Mode</div>
|
||||
<div class="text-zinc-500 text-sm">Enabling Sleep Mode extends battery life</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($sleep_mode_enabled)
|
||||
<div class="flex gap-4 mb-4">
|
||||
<flux:input type="time" label="From" wire:model="sleep_mode_from"/>
|
||||
<flux:input type="time" label="To" wire:model="sleep_mode_to" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
|
||||
|
|
|
|||
124
routes/api.php
124
routes/api.php
|
|
@ -56,76 +56,82 @@ Route::get('/display', function (Request $request) {
|
|||
]);
|
||||
}
|
||||
|
||||
// Get current screen image from a mirror device or continue if not available
|
||||
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
|
||||
$refreshTimeOverride = null;
|
||||
// Skip if cloud proxy is enabled for the device
|
||||
if (! $device->proxy_cloud || $device->getNextPlaylistItem()) {
|
||||
$playlistItem = $device->getNextPlaylistItem();
|
||||
if ($device->isSleepModeActive()) {
|
||||
$image_path = 'images/sleep.png';
|
||||
$filename = 'sleep.png';
|
||||
$refreshTimeOverride = $device->getSleepModeEndsInSeconds() ?? $device->default_refresh_interval;
|
||||
} else {
|
||||
// Get current screen image from a mirror device or continue if not available
|
||||
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
|
||||
$refreshTimeOverride = null;
|
||||
// Skip if cloud proxy is enabled for the device
|
||||
if (! $device->proxy_cloud || $device->getNextPlaylistItem()) {
|
||||
$playlistItem = $device->getNextPlaylistItem();
|
||||
|
||||
if ($playlistItem && ! $playlistItem->isMashup()) {
|
||||
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
|
||||
$plugin = $playlistItem->plugin;
|
||||
if ($playlistItem && ! $playlistItem->isMashup()) {
|
||||
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
|
||||
$plugin = $playlistItem->plugin;
|
||||
|
||||
// Reset cache if Devices with different dimensions exist
|
||||
ImageGenerationService::resetIfNotCacheable($plugin);
|
||||
|
||||
// Check and update stale data if needed
|
||||
if ($plugin->isDataStale() || $plugin->current_image === null) {
|
||||
$plugin->updateDataPayload();
|
||||
$markup = $plugin->render();
|
||||
|
||||
GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
|
||||
}
|
||||
|
||||
$plugin->refresh();
|
||||
|
||||
if ($plugin->current_image !== null) {
|
||||
$playlistItem->update(['last_displayed_at' => now()]);
|
||||
$device->update(['current_screen_image' => $plugin->current_image]);
|
||||
}
|
||||
} elseif ($playlistItem) {
|
||||
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
|
||||
|
||||
// Get all plugins for the mashup
|
||||
$plugins = Plugin::whereIn('id', $playlistItem->getMashupPluginIds())->get();
|
||||
|
||||
foreach ($plugins as $plugin) {
|
||||
// Reset cache if Devices with different dimensions exist
|
||||
ImageGenerationService::resetIfNotCacheable($plugin);
|
||||
|
||||
// Check and update stale data if needed
|
||||
if ($plugin->isDataStale() || $plugin->current_image === null) {
|
||||
$plugin->updateDataPayload();
|
||||
$markup = $plugin->render();
|
||||
|
||||
GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
|
||||
}
|
||||
|
||||
$plugin->refresh();
|
||||
|
||||
if ($plugin->current_image !== null) {
|
||||
$playlistItem->update(['last_displayed_at' => now()]);
|
||||
$device->update(['current_screen_image' => $plugin->current_image]);
|
||||
}
|
||||
} elseif ($playlistItem) {
|
||||
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
|
||||
|
||||
// Get all plugins for the mashup
|
||||
$plugins = Plugin::whereIn('id', $playlistItem->getMashupPluginIds())->get();
|
||||
|
||||
foreach ($plugins as $plugin) {
|
||||
// Reset cache if Devices with different dimensions exist
|
||||
ImageGenerationService::resetIfNotCacheable($plugin);
|
||||
if ($plugin->isDataStale() || $plugin->current_image === null) {
|
||||
$plugin->updateDataPayload();
|
||||
}
|
||||
}
|
||||
|
||||
$markup = $playlistItem->render();
|
||||
GenerateScreenJob::dispatchSync($device->id, null, $markup);
|
||||
|
||||
$device->refresh();
|
||||
|
||||
if ($device->current_screen_image !== null) {
|
||||
$playlistItem->update(['last_displayed_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
$markup = $playlistItem->render();
|
||||
GenerateScreenJob::dispatchSync($device->id, null, $markup);
|
||||
|
||||
$device->refresh();
|
||||
|
||||
if ($device->current_screen_image !== null) {
|
||||
$playlistItem->update(['last_displayed_at' => now()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$device->refresh();
|
||||
$image_uuid = $device->current_screen_image;
|
||||
}
|
||||
if (! $image_uuid) {
|
||||
$image_path = 'images/setup-logo.bmp';
|
||||
$filename = 'setup-logo.bmp';
|
||||
} else {
|
||||
if (isset($device->last_firmware_version)
|
||||
&& version_compare($device->last_firmware_version, '1.5.2', '<')
|
||||
&& Storage::disk('public')->exists('images/generated/'.$image_uuid.'.bmp')) {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.bmp';
|
||||
} elseif (Storage::disk('public')->exists('images/generated/'.$image_uuid.'.png')) {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.png';
|
||||
} else {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.bmp';
|
||||
$device->refresh();
|
||||
$image_uuid = $device->current_screen_image;
|
||||
}
|
||||
if (! $image_uuid) {
|
||||
$image_path = 'images/setup-logo.bmp';
|
||||
$filename = 'setup-logo.bmp';
|
||||
} else {
|
||||
if (isset($device->last_firmware_version)
|
||||
&& version_compare($device->last_firmware_version, '1.5.2', '<')
|
||||
&& Storage::disk('public')->exists('images/generated/'.$image_uuid.'.bmp')) {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.bmp';
|
||||
} elseif (Storage::disk('public')->exists('images/generated/'.$image_uuid.'.png')) {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.png';
|
||||
} else {
|
||||
$image_path = 'images/generated/'.$image_uuid.'.bmp';
|
||||
}
|
||||
$filename = basename($image_path);
|
||||
}
|
||||
$filename = basename($image_path);
|
||||
}
|
||||
|
||||
$response = [
|
||||
|
|
|
|||
|
|
@ -757,3 +757,62 @@ test('display endpoint handles mashup playlist items correctly', function () {
|
|||
$playlistItem->refresh();
|
||||
expect($playlistItem->last_displayed_at)->not->toBeNull();
|
||||
})->skipOnGitHubActions();
|
||||
|
||||
test('device in sleep mode returns sleep image and correct refresh rate', function () {
|
||||
$device = Device::factory()->create([
|
||||
'mac_address' => '00:11:22:33:44:55',
|
||||
'api_key' => 'test-api-key',
|
||||
'sleep_mode_enabled' => true,
|
||||
'sleep_mode_from' => '19:00',
|
||||
'sleep_mode_to' => '23:00',
|
||||
'current_screen_image' => 'test-image',
|
||||
]);
|
||||
|
||||
// Freeze time to 20:00 (within sleep window)
|
||||
\Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2000-01-01 20:00:00'));
|
||||
|
||||
$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()
|
||||
->assertJson([
|
||||
'filename' => 'sleep.png',
|
||||
]);
|
||||
expect($response['refresh_rate'])->toBeGreaterThan(0);
|
||||
|
||||
\Carbon\Carbon::setTestNow(); // Clear test time
|
||||
});
|
||||
|
||||
test('device not in sleep mode returns normal image', function () {
|
||||
$device = Device::factory()->create([
|
||||
'mac_address' => '00:11:22:33:44:55',
|
||||
'api_key' => 'test-api-key',
|
||||
'sleep_mode_enabled' => true,
|
||||
'sleep_mode_from' => '19:00',
|
||||
'sleep_mode_to' => '23:00',
|
||||
'current_screen_image' => 'test-image',
|
||||
]);
|
||||
|
||||
// Freeze time to 18:00 (outside sleep window)
|
||||
\Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2000-01-01 18:00:00'));
|
||||
|
||||
$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()
|
||||
->assertJson([
|
||||
'filename' => 'test-image.bmp',
|
||||
]);
|
||||
|
||||
\Carbon\Carbon::setTestNow(); // Clear test time
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue