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
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest'
import { isZwaveType, buildZwaveProperties } from '../zwaveProperties'
describe('isZwaveType', () => {
it('returns true for zwave types', () => {
expect(isZwaveType('zwave_coordinator')).toBe(true)
expect(isZwaveType('zwave_router')).toBe(true)
expect(isZwaveType('zwave_enddevice')).toBe(true)
})
it('returns false for non-zwave types', () => {
expect(isZwaveType('zigbee_router')).toBe(false)
expect(isZwaveType('server')).toBe(false)
expect(isZwaveType(undefined)).toBe(false)
expect(isZwaveType(null)).toBe(false)
})
})
describe('buildZwaveProperties', () => {
it('builds Z-Wave ID / Vendor / Model rows, all hidden', () => {
const props = buildZwaveProperties({ ieee_address: 'zwave-0xh-2', vendor: 'Aeotec', model: 'ZW100' })
expect(props).toEqual([
{ key: 'Z-Wave ID', value: 'zwave-0xh-2', icon: null, visible: false },
{ key: 'Vendor', value: 'Aeotec', icon: null, visible: false },
{ key: 'Model', value: 'ZW100', icon: null, visible: false },
])
})
it('omits empty fields', () => {
const props = buildZwaveProperties({ ieee_address: 'zwave-0xh-2', vendor: null, model: undefined })
expect(props.map((p) => p.key)).toEqual(['Z-Wave ID'])
})
it('never adds an LQI row', () => {
const props = buildZwaveProperties({ ieee_address: 'x', vendor: 'v', model: 'm' })
expect(props.some((p) => p.key === 'LQI')).toBe(false)
})
})
+4 -1
View File
@@ -11,7 +11,7 @@ import {
// Security & Auth
Shield, ShieldCheck, Lock, Key, Users, UserCheck, Flame,
// Automation & IoT
Zap, Workflow, Bot, Home, Thermometer, Lightbulb, Radio, BotMessageSquare, Webhook,
Zap, Workflow, Bot, Home, Thermometer, Lightbulb, Radio, RadioTower, Share2, BotMessageSquare, Webhook,
// Smart Home / Sensors
Plug, Power, BatteryCharging, Sun, DoorOpen, KeyRound, AlarmSmoke, Siren,
Radar, PersonStanding, Vibrate, Droplet, Droplets, Wind, AirVent, Fan,
@@ -178,6 +178,9 @@ export const NODE_TYPE_DEFAULT_ICONS: Record<NodeType, LucideIcon> = {
zigbee_coordinator: Radio,
zigbee_router: Zap,
zigbee_enddevice: Lightbulb,
zwave_coordinator: RadioTower,
zwave_router: Share2,
zwave_enddevice: Lightbulb,
generic: Circle,
group: Circle,
groupRect: Circle,
+21
View File
@@ -0,0 +1,21 @@
import type { NodeProperty, NodeType } from '@/types'
const ZWAVE_TYPES: NodeType[] = ['zwave_coordinator', 'zwave_router', 'zwave_enddevice']
export function isZwaveType(type: NodeType | string | undefined | null): boolean {
return !!type && (ZWAVE_TYPES as string[]).includes(type)
}
/** Build the Z-Wave ID/Vendor/Model property rows shown in the right panel.
* Matches backend `build_zwave_properties` (no LQI — Z-Wave has none). */
export function buildZwaveProperties(input: {
ieee_address?: string | null
vendor?: string | null
model?: string | null
}): NodeProperty[] {
const props: NodeProperty[] = []
if (input.ieee_address) props.push({ key: 'Z-Wave ID', value: input.ieee_address, icon: null, visible: false })
if (input.vendor) props.push({ key: 'Vendor', value: input.vendor, icon: null, visible: false })
if (input.model) props.push({ key: 'Model', value: input.model, icon: null, visible: false })
return props
}