From fb33e5cb16162c9bd29049e0171eb70972375c3d Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 30 Jun 2026 14:38:02 +0200 Subject: [PATCH] feat: add Z-Wave node types to add/edit node modal Z-Wave (controller/router/end device) types existed in the model, theme accents, default icons and the custom-style editor, but were missing from the node Add/Edit type selector, so they could not be placed manually like Zigbee nodes. Add a Z-Wave group and default mesh-radio nodes to no status check (not IP-reachable), matching Zigbee behaviour. --- frontend/src/components/modals/NodeModal.tsx | 8 ++++++-- .../components/modals/__tests__/NodeModal.test.tsx | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index f7e6b95..246a314 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -18,6 +18,7 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ { label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host', 'docker_container'] }, { label: 'IoT', types: ['iot', 'camera', 'cpl'] }, { label: 'Zigbee', types: ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] }, + { label: 'Z-Wave', types: ['zwave_coordinator', 'zwave_router', 'zwave_enddevice'] }, { label: 'Personal', types: ['computer', 'laptop', 'mobile'] }, { label: 'Electrical', types: ['grid', 'ups', 'battery', 'generator', 'solar_panel', 'inverter', 'circuit_breaker', 'contactor', 'electrical_switch', 'socket', 'light', 'meter', 'transformer', 'load'] }, { label: 'Generic', types: ['generic', 'groupRect'] }, @@ -26,6 +27,9 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health'] const CONTAINER_MODE_TYPES: NodeType[] = ['proxmox', 'vm', 'lxc', 'docker_host'] const ZIGBEE_TYPES: NodeType[] = ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] +const ZWAVE_TYPES: NodeType[] = ['zwave_coordinator', 'zwave_router', 'zwave_enddevice'] +// Mesh radio devices aren't IP-reachable, so they default to no status check. +const MESH_TYPES: NodeType[] = [...ZIGBEE_TYPES, ...ZWAVE_TYPES] const CHECK_METHOD_LABELS: Record = { none: 'None', @@ -73,7 +77,7 @@ interface NodeModalProps { // initial value is enough - no need for a reset effect. export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', parentCandidates = [], currentNodeId }: NodeModalProps) { const merged = { ...DEFAULT_DATA, ...initial } - if (ZIGBEE_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none' + if (MESH_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none' const [form, setForm] = useState>(merged) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) @@ -133,7 +137,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' const t = v as NodeType setForm((f) => { const next: Partial = { ...f, type: t } - if (ZIGBEE_TYPES.includes(t)) next.check_method = 'none' as CheckMethod + if (MESH_TYPES.includes(t)) next.check_method = 'none' as CheckMethod // Drop the parent only if it's no longer a valid target for the // new type — keep container-mode parents (any node can nest). const parent = parentCandidates.find((n) => n.id === f.parent_id) diff --git a/frontend/src/components/modals/__tests__/NodeModal.test.tsx b/frontend/src/components/modals/__tests__/NodeModal.test.tsx index 9f187ba..be4c81c 100644 --- a/frontend/src/components/modals/__tests__/NodeModal.test.tsx +++ b/frontend/src/components/modals/__tests__/NodeModal.test.tsx @@ -188,6 +188,20 @@ describe('NodeModal', () => { expect((onSubmit.mock.calls[0][0] as Partial).type).toBe('nas') }) + it('offers Z-Wave node types and submits one', () => { + const { onSubmit } = renderModal({ initial: BASE }) + fireEvent.change(selects()[0], { target: { value: 'zwave_enddevice' } }) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + expect((onSubmit.mock.calls[0][0] as Partial).type).toBe('zwave_enddevice') + }) + + it('forces check_method to none when a Z-Wave type is selected', () => { + const { onSubmit } = renderModal({ initial: { ...BASE, check_method: 'ping' } }) + fireEvent.change(selects()[0], { target: { value: 'zwave_coordinator' } }) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + expect((onSubmit.mock.calls[0][0] as Partial).check_method).toBe('none') + }) + // ── Check method ────────────────────────────────────────────────────── it('pre-fills check_method from initial', () => {