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
+4 -2
View File
@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react'
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Square, Settings, LogOut, Network, Type, PlusCircle, Pencil, Trash2 } from 'lucide-react'
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Square, Settings, LogOut, Network, RadioTower, Type, PlusCircle, Pencil, Trash2 } from 'lucide-react'
import { Logo } from '@/components/ui/Logo'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useCanvasStore } from '@/stores/canvasStore'
@@ -25,13 +25,14 @@ interface SidebarProps {
onAddText: () => void
onScan: () => void
onZigbeeImport: () => void
onZwaveImport: () => void
onSave: () => void
onOpenSettings: () => void
onOpenHistory: () => void
onOpenPending: (deviceId?: string, status?: 'pending' | 'hidden') => void
}
export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbeeImport, onSave, onOpenSettings, onOpenHistory, onOpenPending }: SidebarProps) {
export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbeeImport, onZwaveImport, onSave, onOpenSettings, onOpenHistory, onOpenPending }: SidebarProps) {
const [collapsed, setCollapsed] = useState(false)
const logout = useAuthStore((s) => s.logout)
const { designs, activeDesignId, setActiveDesign, addDesign, updateDesign, removeDesign } = useDesignStore()
@@ -217,6 +218,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
<SidebarItem icon={Type} label="Add Text" collapsed={collapsed} onClick={onAddText} />
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
{!STANDALONE && <SidebarItem icon={Network} label="Zigbee Import" collapsed={collapsed} onClick={onZigbeeImport} />}
{!STANDALONE && <SidebarItem icon={RadioTower} label="Z-Wave Import" collapsed={collapsed} onClick={onZwaveImport} />}
<SidebarItem
icon={Save}
label="Save Canvas"
@@ -70,6 +70,7 @@ const defaultProps = {
onAddText: vi.fn(),
onScan: vi.fn(),
onZigbeeImport: vi.fn(),
onZwaveImport: vi.fn(),
onSave: vi.fn(),
onOpenSettings: vi.fn(),
onOpenHistory: vi.fn(),
@@ -184,6 +185,12 @@ describe('Sidebar', () => {
expect(defaultProps.onAddGroupRect).toHaveBeenCalledOnce()
})
it('calls onZwaveImport when Z-Wave Import is clicked', () => {
render(<Sidebar {...defaultProps} />)
fireEvent.click(screen.getByText('Z-Wave Import'))
expect(defaultProps.onZwaveImport).toHaveBeenCalledOnce()
})
it('calls onSave when Save Canvas is clicked', () => {
render(<Sidebar {...defaultProps} />)
fireEvent.click(screen.getByText('Save Canvas'))