diff --git a/app/Livewire/Concerns/TogglesEntities.php b/app/Livewire/Concerns/TogglesEntities.php
new file mode 100644
index 0000000..229dbd9
--- /dev/null
+++ b/app/Livewire/Concerns/TogglesEntities.php
@@ -0,0 +1,25 @@
+find($entityId);
+
+ if ($entity === null || ! in_array($entity->type, ['switch', 'light'], true)) {
+ return;
+ }
+
+ app(DeviceCommandService::class)->toggle($entity);
+ }
+}
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index 1fce672..a4b7d81 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -2,6 +2,7 @@
namespace App\Livewire;
+use App\Livewire\Concerns\TogglesEntities;
use App\Models\Room;
use App\Services\HomeStatus;
use Livewire\Attributes\Layout;
@@ -11,6 +12,8 @@ use Livewire\Component;
#[Layout('layouts.app')]
class Dashboard extends Component
{
+ use TogglesEntities;
+
/** Live update: any device state change re-renders (re-queries) the dashboard. */
#[On('echo-private:home,.DeviceStateChanged')]
public function onDeviceStateChanged(): void
diff --git a/app/Livewire/Devices/Index.php b/app/Livewire/Devices/Index.php
index b709672..277b8ad 100644
--- a/app/Livewire/Devices/Index.php
+++ b/app/Livewire/Devices/Index.php
@@ -2,15 +2,22 @@
namespace App\Livewire\Devices;
+use App\Livewire\Concerns\TogglesEntities;
use App\Models\Device;
use App\Models\Room;
use Livewire\Attributes\Layout;
+use Livewire\Attributes\On;
use Livewire\Attributes\Url;
use Livewire\Component;
#[Layout('layouts.app')]
class Index extends Component
{
+ use TogglesEntities;
+
+ #[On('echo-private:home,.DeviceStateChanged')]
+ public function onDeviceStateChanged(): void {}
+
#[Url(as: 'q')]
public string $search = '';
diff --git a/resources/views/components/device-card.blade.php b/resources/views/components/device-card.blade.php
new file mode 100644
index 0000000..994dfd8
--- /dev/null
+++ b/resources/views/components/device-card.blade.php
@@ -0,0 +1,59 @@
+@props(['device', 'controllable' => true])
+
+@php
+ $entities = $device->entities;
+ $primary = $controllable ? $entities->first(fn ($e) => in_array($e->type, ['switch', 'light'], true)) : null;
+ $on = $primary ? (data_get($primary->state?->state, 'on') === true) : false;
+ $secondary = $primary ? $entities->reject(fn ($e) => $e->id === $primary->id) : $entities;
+ $online = $device->isOnline();
+ $icon = match (true) {
+ $device->isCloud() => 'doorbell',
+ $primary?->type === 'light' => 'bolt',
+ $entities->contains(fn ($e) => $e->type === 'contact') => 'window',
+ $entities->contains(fn ($e) => $e->type === 'motion') => 'activity',
+ default => 'devices',
+ };
+@endphp
+
+
$on,
+ 'border-line-soft bg-surface hover:border-line' => ! $on,
+])>
+
+
$on,
+ 'bg-inset text-ink-2' => ! $on,
+ ])>
+
+
+
{{ $device->name }}
+
+ {{ $device->room?->name ?? __('devices.no_room') }}
+ @if ($device->isCloud())
+
+ @endif
+
+
+
+
+
+
+ @if ($secondary->isNotEmpty())
+
+ @foreach ($secondary as $entity)
+
+ @endforeach
+
+ @endif
+
+ @if ($primary)
+
+ $on, 'text-ink-3' => ! $on])>{{ $on ? __('devices.on') : __('devices.off') }}
+
+
+ @endif
+
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php
index 4f3fd3c..6585b3b 100644
--- a/resources/views/livewire/dashboard.blade.php
+++ b/resources/views/livewire/dashboard.blade.php
@@ -26,33 +26,21 @@
- {{-- rooms + devices --}}
-
-
{{ __('dashboard.rooms_title') }}
-
- @foreach ($groups as $group)
-
-
-
- @endforeach
-
+ {{-- rooms + devices — a card per device, grouped by room --}}
+
+ @foreach ($groups as $group)
+
+
+
{{ $group['name'] }}
+ {{ count($group['devices']) }}
+
+
+ @foreach ($group['devices'] as $device)
+
+ @endforeach
+
+
+ @endforeach
diff --git a/resources/views/livewire/devices/index.blade.php b/resources/views/livewire/devices/index.blade.php
index 0a5dcf3..27bc310 100644
--- a/resources/views/livewire/devices/index.blade.php
+++ b/resources/views/livewire/devices/index.blade.php
@@ -31,23 +31,17 @@
{{ trans_choice('devices.count', $total, ['count' => $total]) }}
@forelse ($groups as $roomName => $devices)
-
- {{ $devices->count() }}
-
+
+
+
{{ $roomName }}
+ {{ $devices->count() }}
+
+
-
+
@empty
{{ __('devices.none_match') }}
diff --git a/resources/views/livewire/rooms/show.blade.php b/resources/views/livewire/rooms/show.blade.php
index 8b595fc..06af1cc 100644
--- a/resources/views/livewire/rooms/show.blade.php
+++ b/resources/views/livewire/rooms/show.blade.php
@@ -9,30 +9,15 @@
-
- @forelse ($room->devices as $device)
-
-
-
-
- @foreach ($device->entities as $entity)
-
-
- @if (in_array($entity->type, ['switch', 'light']))
-
- @endif
-
- @endforeach
-
-
-
- @empty
+
+ @if ($room->devices->isEmpty())
{{ __('rooms.no_devices') }}
- @endforelse
+ @else
+
+ @foreach ($room->devices as $device)
+
+ @endforeach
+
+ @endif
diff --git a/resources/views/livewire/windows.blade.php b/resources/views/livewire/windows.blade.php
index 74557e6..97e010b 100644
--- a/resources/views/livewire/windows.blade.php
+++ b/resources/views/livewire/windows.blade.php
@@ -22,28 +22,49 @@
@foreach ($groups as $roomName => $sensors)
-
-
+
+
+
{{ $roomName }}
+ {{ $sensors->count() }}
+
+
@foreach ($sensors as $sensor)
@php
$battery = $sensor->device->entities->firstWhere('type', 'battery');
$pct = $battery ? (int) data_get($battery->state, 'state.percent', 0) : null;
+ $pos = data_get($sensor->state, 'state.position') ?? (data_get($sensor->state, 'state.open') === true ? 'open' : 'closed');
+ $isMotion = $sensor->type === 'motion';
+ $alert = $isMotion ? (data_get($sensor->state, 'state.on') === true || data_get($sensor->state, 'state.active') === true) : $pos !== 'closed';
@endphp
-
-
-
-
-
{{ $sensor->device->name }}
-
+
+ @if ($isMotion)
+ $alert, 'text-ink-3' => ! $alert])>{{ $alert ? __('devices.motion') : __('devices.no_motion') }}
+ @else
+ $alert, 'text-online' => ! $alert])>{{ __('devices.contact_'.$pos) }}
+ @endif
+ @if (! is_null($pct))
+ {{ $pct }}%
+ @endif
+
+
@endforeach
-
+
@endforeach
@endif