'array', 'demo' => 'boolean', 'last_seen_at' => 'datetime', ]; public function room(): BelongsTo { return $this->belongsTo(Room::class); } public function entities(): HasMany { return $this->hasMany(Entity::class); } /** * Reachable now. Real devices must have been seen recently (kept fresh by MQTT); a * never-seen real device is offline. Only demo devices are "assumed reachable" when * they have no last_seen, so the seeded demo doesn't rot. */ public function isOnline(): bool { if ($this->status !== 'active') { return false; } if ($this->last_seen_at !== null) { return $this->last_seen_at->gt(now()->subMinutes(10)); } return (bool) $this->demo; } /** Cloud-bridged (e.g. Ring) — surfaced with a "Cloud" badge so the dependency is visible. */ public function isCloud(): bool { return (bool) data_get($this->config, 'cloud', false); } /** Icons the user can pick for a device (keys map to the x-icon set). */ public const ICON_CHOICES = ['bolt', 'lamp', 'led', 'plug', 'window', 'door', 'doorbell', 'activity', 'temp', 'devices']; /** The device's display icon — a user override (config.icon) or one derived from its entities. */ public function displayIcon(): string { $override = data_get($this->config, 'icon'); if (filled($override) && in_array($override, self::ICON_CHOICES, true)) { return $override; } $types = $this->relationLoaded('entities') ? $this->entities->pluck('type') : collect(); return match (true) { $this->isCloud() => 'doorbell', $types->contains('light') => 'bolt', $types->contains('switch') => 'plug', $types->contains('contact') => 'window', $types->contains('motion') => 'activity', default => 'devices', }; } }