Fix R15: Shelly prefix from mDNS instance, discovery broadcast once, targeted confirm

- AssignDevice derives the Shelly MQTT prefix from the mDNS service instance name
  (the device id) instead of the slugified topic identifier (which carries the
  _shelly._tcp suffix); non-Shelly devices get no prefix.
- IngestDiscoveryMessage broadcasts DeviceDiscovered only on first creation, not on
  every retained/periodic re-publish.
- The generic Confirm modal can target its event at the opening component (->to),
  so a shared event name can't be caught by an unrelated component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/phase-1-bootstrap
HomeOS Bootstrap 2026-07-18 00:34:56 +02:00
parent b85d2f60fe
commit 7d4c9c024e
4 changed files with 19 additions and 6 deletions

View File

@ -46,7 +46,9 @@ class IngestDiscoveryMessage implements ShouldQueue
], ],
); );
if (($existing?->status ?? 'new') === 'new') { // Announce only on FIRST discovery — the sidecar re-publishes retained/periodic
// messages, so broadcasting on every message would spam the "Neue Geräte" panel.
if ($existing === null) {
DeviceDiscovered::dispatch($finding->uuid); DeviceDiscovered::dispatch($finding->uuid);
} }
} }

View File

@ -42,13 +42,18 @@ class AssignDevice extends ModalComponent
]); ]);
$finding = $this->finding(); $finding = $this->finding();
$isShelly = $finding->vendor === 'Shelly';
// The Shelly MQTT topic prefix is its device id — the mDNS service INSTANCE name
// (finding->name), not the slugified topic identifier which carries the _shelly._tcp suffix.
$prefix = $isShelly ? ($finding->name ?: $finding->identifier) : null;
$device = Device::create([ $device = Device::create([
'name' => $this->name, 'name' => $this->name,
'vendor' => $finding->vendor, 'vendor' => $finding->vendor,
'model' => $finding->model, 'model' => $finding->model,
'protocol' => $finding->vendor === 'Shelly' ? 'mqtt' : null, 'protocol' => $isShelly ? 'mqtt' : null,
'config' => ['mqtt_prefix' => $finding->identifier], 'config' => $prefix ? ['mqtt_prefix' => $prefix] : null,
'room_id' => $this->roomId ?: null, 'room_id' => $this->roomId ?: null,
'status' => 'active', 'status' => 'active',
]); ]);

View File

@ -17,6 +17,7 @@ class Confirm extends ModalComponent
public string $body = ''; public string $body = '';
public string $confirmLabel = ''; public string $confirmLabel = '';
public string $event = ''; public string $event = '';
public string $to = '';
public bool $danger = false; public bool $danger = false;
public static function modalMaxWidth(): string public static function modalMaxWidth(): string
@ -24,18 +25,23 @@ class Confirm extends ModalComponent
return 'md'; return 'md';
} }
public function mount(string $title, string $body, string $confirmLabel, string $event, bool $danger = false): void public function mount(string $title, string $body, string $confirmLabel, string $event, string $to = '', bool $danger = false): void
{ {
$this->title = $title; $this->title = $title;
$this->body = $body; $this->body = $body;
$this->confirmLabel = $confirmLabel; $this->confirmLabel = $confirmLabel;
$this->event = $event; $this->event = $event;
$this->to = $to;
$this->danger = $danger; $this->danger = $danger;
} }
public function confirm(): void public function confirm(): void
{ {
$this->dispatch($this->event); // Target the opener (if given) so an event name can't collide with another component.
$this->to !== ''
? $this->dispatch($this->event)->to($this->to)
: $this->dispatch($this->event);
$this->closeModal(); $this->closeModal();
} }

View File

@ -93,7 +93,7 @@
<x-panel :title="__('devices.actions')"> <x-panel :title="__('devices.actions')">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<button type="button" <button type="button"
wire:click="$dispatch('openModal', { component: 'modals.confirm', arguments: { title: @js(__('devices.restart_confirm_title')), body: @js(__('devices.restart_confirm_body', ['name' => $device->name])), confirmLabel: @js(__('devices.restart')), event: 'restartDevice', danger: true } })" wire:click="$dispatch('openModal', { component: 'modals.confirm', arguments: { title: @js(__('devices.restart_confirm_title')), body: @js(__('devices.restart_confirm_body', ['name' => $device->name])), confirmLabel: @js(__('devices.restart')), event: 'restartDevice', to: 'devices.show', danger: true } })"
class="flex items-center gap-2.5 rounded-lg border border-line-soft bg-raised px-3 py-2.5 text-[13px] font-semibold text-ink hover:border-line transition-colors"> class="flex items-center gap-2.5 rounded-lg border border-line-soft bg-raised px-3 py-2.5 text-[13px] font-semibold text-ink hover:border-line transition-colors">
<x-icon name="activity" :size="16" class="text-ink-2" /> {{ __('devices.restart') }} <x-icon name="activity" :size="16" class="text-ink-2" /> {{ __('devices.restart') }}
</button> </button>