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);
}
}

View File

@ -42,13 +42,18 @@ class AssignDevice extends ModalComponent
]);
$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([
'name' => $this->name,
'vendor' => $finding->vendor,
'model' => $finding->model,
'protocol' => $finding->vendor === 'Shelly' ? 'mqtt' : null,
'config' => ['mqtt_prefix' => $finding->identifier],
'protocol' => $isShelly ? 'mqtt' : null,
'config' => $prefix ? ['mqtt_prefix' => $prefix] : null,
'room_id' => $this->roomId ?: null,
'status' => 'active',
]);

View File

@ -17,6 +17,7 @@ class Confirm extends ModalComponent
public string $body = '';
public string $confirmLabel = '';
public string $event = '';
public string $to = '';
public bool $danger = false;
public static function modalMaxWidth(): string
@ -24,18 +25,23 @@ class Confirm extends ModalComponent
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->body = $body;
$this->confirmLabel = $confirmLabel;
$this->event = $event;
$this->to = $to;
$this->danger = $danger;
}
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();
}

View File

@ -93,7 +93,7 @@
<x-panel :title="__('devices.actions')">
<div class="flex flex-col gap-2">
<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">
<x-icon name="activity" :size="16" class="text-ink-2" /> {{ __('devices.restart') }}
</button>