// Each button maps to (room, light) via global roomConfig.switchBindings. See /root/.node-red/room-config.js. // Supports single-device payload (action, friendly_name at top level) and multi-device payload (keyed by IEEE address). const actionToSwitch = { single: 1, double: 2, hold: 3, release: 4, triple: 5, quad: 6 }; const config = global.get('roomConfig'); const switchBindings = (config && config.switchBindings) ? config.switchBindings : {}; const deviceToRoom = (config && config.deviceToRoom) ? config.deviceToRoom : { 'Office Switch': 'cmd_livingroom' }; // Optional: map IEEE address to friendly name when multi-device node doesn't provide it. e.g. deviceIdToName: { '0xa4c138a5b9771b05': 'Office Switch' } const deviceIdToName = (config && config.deviceIdToName) ? config.deviceIdToName : {}; const payload = msg.payload != null ? msg.payload : {}; // Payload can be: single-device { action, friendly_name }, multi-device { "0x...": { ... } }, or plain action string "1_single" let friendlyName = (typeof payload === 'object' && payload.friendly_name != null) ? payload.friendly_name.toString() : ''; let actionRaw = (typeof payload === 'string' ? payload : (payload.action || payload.click || msg.action || (payload && payload.action)) || '').toString().toLowerCase(); // Multi-device payload: prefer the device that has an action AND a configured binding (so we don't pick another device's stale action) if (!actionRaw && typeof payload === 'object' && !Array.isArray(payload)) { const keys = Object.keys(payload); let fallback = null; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const v = payload[key]; if (key && key.toLowerCase().indexOf('0x') === 0 && v && typeof v === 'object') { const inner = v.payload || v.message || v.state || v; const a = (inner.action || inner.click || v.action || v.click || '').toString().toLowerCase(); if (!a) continue; const name = (v.friendly_name || v.name || (v.item && (v.item.friendly_name || v.item.name)) || deviceIdToName[key] || key).toString(); if (switchBindings[name] || deviceToRoom[name]) { actionRaw = a; friendlyName = name; break; } if (!fallback) fallback = { actionRaw: a, friendlyName: name }; } } if (!actionRaw && fallback) { actionRaw = fallback.actionRaw; friendlyName = fallback.friendlyName; } } // If still no action, try msg.data or msg.topic (some nodes put action in topic, e.g. "1_single") if (!actionRaw && msg.data && msg.data.action) actionRaw = msg.data.action.toString().toLowerCase(); if (!actionRaw && typeof msg.topic === 'string' && /^\d_[a-z_]+$/.test(msg.topic.trim())) actionRaw = msg.topic.trim().toLowerCase(); // If payload was the action string, get friendly_name from topic e.g. zigbee2mqtt/Office Switch/action if (actionRaw && !friendlyName && typeof msg.topic === 'string') { const parts = msg.topic.split('/'); if (parts.length >= 2) friendlyName = parts[1].trim(); // zigbee2mqtt/Office Switch/action → Office Switch } if (!actionRaw) { node.warn('[Zigbee to NVL] no action in payload. Keys: ' + (typeof payload === 'object' ? Object.keys(payload).join(', ') : 'n/a')); return null; } const parts = actionRaw.split('_'); let buttonIndex = null; if (parts.length >= 2) { const n = parseInt(parts[0], 10); if (n >= 1 && n <= 6) buttonIndex = n; } if (buttonIndex == null) buttonIndex = actionToSwitch[actionRaw]; if (buttonIndex == null && parts.length >= 2) buttonIndex = actionToSwitch[parts.slice(1).join('_')]; if (buttonIndex == null) buttonIndex = actionToSwitch[parts[parts.length - 1]]; if (buttonIndex == null) { node.warn('[Zigbee to NVL] unknown action: ' + JSON.stringify(actionRaw)); return null; } node.warn('[Zigbee to NVL] action=' + actionRaw + ' buttonIndex=' + buttonIndex + ' device=' + friendlyName); // Prefer switchBindings by friendly name; fallback by device ID (IEEE) if we have switchBindingsByDeviceId const bindingByDeviceId = (config && config.switchBindingsByDeviceId) ? config.switchBindingsByDeviceId : null; let targets = []; let binding = switchBindings[friendlyName] && switchBindings[friendlyName][buttonIndex]; if ((binding === undefined || binding === null) && bindingByDeviceId) { const deviceId = typeof payload === 'object' && Object.keys(payload).some(function (k) { return k.indexOf('0x') === 0; }) ? Object.keys(payload).find(function (k) { return k.indexOf('0x') === 0; }) : null; if (deviceId) binding = bindingByDeviceId[deviceId] && bindingByDeviceId[deviceId][buttonIndex]; } if (binding !== undefined && binding !== null) { targets = Array.isArray(binding) ? binding : [binding]; targets = targets.filter(function (t) { return t && t.room && t.light >= 1 && t.light <= 6; }); } if (targets.length === 0) { const room = deviceToRoom[friendlyName] || 'cmd_livingroom'; targets = [{ room: room, light: buttonIndex }]; node.warn('[Zigbee to NVL] no switchBindings for ' + friendlyName + ' btn' + buttonIndex + ', using fallback: ' + room + ' light ' + buttonIndex); } if (!flow.get('nvlInState')) flow.set('nvlInState', { rooms: {}, boiler: {} }); const state = flow.get('nvlInState'); const clearList = []; for (const t of targets) { const key = 'zigbee_sw' + t.light; if (!state.rooms[t.room]) state.rooms[t.room] = {}; state.rooms[t.room][key] = true; clearList.push({ room: t.room, key: key }); } flow.set('nvlInState', state); node.warn('[Zigbee to NVL] ' + (friendlyName || 'device') + ' btn' + buttonIndex + ' → ' + targets.map(function (t) { return t.room + ' L' + t.light; }).join(', ')); msg.payload = { buildAndSend: true }; msg.zigbeeClear = clearList; return msg;