feat: add Z-Wave network scan via MQTT gateway

Import a Z-Wave JS UI (zwavejs2mqtt) network over the MQTT gateway API,
mirroring the existing Zigbee pipeline:

- New Z-Wave Import modal + sidebar entry (broker, prefix, gateway name)
- coordinator/router/end-device typing with mesh tree from node neighbors
- import to Pending section or straight to canvas
- Pending Devices gains a Z-Wave source filter
- shared mqtt_common helpers extracted from the zigbee service

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-26 11:19:37 +02:00
parent c6076d133a
commit 8faf5c1c79
23 changed files with 2638 additions and 53 deletions
@@ -67,6 +67,23 @@ const DEVICE_ZIGBEE = {
discovered_at: '2026-01-02T00:00:00Z',
}
const DEVICE_ZWAVE = {
id: 'dev-c',
ip: null,
hostname: null,
mac: null,
os: null,
services: [],
suggested_type: 'zwave_router',
status: 'pending',
discovery_source: 'zwave',
ieee_address: 'zwave-0xh-2',
friendly_name: 'wall-plug',
vendor: 'Aeotec',
model: 'ZW100',
discovered_at: '2026-01-03T00:00:00Z',
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useCanvasStore).mockReturnValue({
@@ -129,6 +146,22 @@ describe('PendingDevicesModal', () => {
expect(screen.getByTestId('pending-card-dev-b')).toBeInTheDocument()
})
it('shows source chip Z-WAVE for zwave device', async () => {
mockPending.mockResolvedValue({ data: [DEVICE_IP, DEVICE_ZWAVE] })
render(<PendingDevicesModal {...baseProps} />)
await waitFor(() => expect(screen.getByTestId('pending-card-dev-c')).toBeInTheDocument())
expect(screen.getByText('Z-WAVE')).toBeInTheDocument()
})
it('filters by source (zwave only)', async () => {
mockPending.mockResolvedValue({ data: [DEVICE_IP, DEVICE_ZWAVE] })
render(<PendingDevicesModal {...baseProps} />)
await waitFor(() => expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: 'Z-Wave' }))
expect(screen.queryByTestId('pending-card-dev-a')).not.toBeInTheDocument()
expect(screen.getByTestId('pending-card-dev-c')).toBeInTheDocument()
})
it('filters by suggested type', async () => {
render(<PendingDevicesModal {...baseProps} />)
await waitFor(() => expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument())