ha-smoke-detection-notify/node-red/flows/smoke_detector.json

861 lines
41 KiB
JSON
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[
{
"id": "smoke_flow_tab",
"type": "tab",
"label": "Rauchmelder System",
"disabled": false,
"info": "Rauchmelder-Benachrichtigungen V8 - Gruppen-Support\n\n✅ Mehrere Gruppen (z.B. Haus 1, Haus 2)\n✅ Alarm nur für Geräte der betroffenen Gruppe\n✅ STOPP pro Gruppe"
},
{
"id": "comment_header",
"type": "comment",
"z": "smoke_flow_tab",
"name": "🔥 Rauchmelder V8 - Multi-Gruppen Support",
"info": "Jede Gruppe hat eigene Rauchmelder, Sirenen und Geräte.\nAlarm in Gruppe A benachrichtigt nur Geräte in Gruppe A.",
"x": 250,
"y": 40,
"wires": []
},
{
"id": "comment_config",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ CONFIG MANAGEMENT ━━━━━━━━━━",
"info": "",
"x": 220,
"y": 80,
"wires": []
},
{
"id": "inject_load_config",
"type": "inject",
"z": "smoke_flow_tab",
"name": "▶️ Config laden (Auto-Start)",
"props": [],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "3",
"topic": "",
"x": 180,
"y": 120,
"wires": [["file_read_config"]]
},
{
"id": "inject_reload_config",
"type": "inject",
"z": "smoke_flow_tab",
"name": "🔄 Config neu laden",
"props": [],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 160,
"y": 160,
"wires": [["file_read_config"]],
"icon": "font-awesome/fa-refresh"
},
{
"id": "file_read_config",
"type": "file in",
"z": "smoke_flow_tab",
"name": "📄 Config lesen",
"filename": "/homeassistant/www/smoke_config.json",
"filenameType": "str",
"format": "utf8",
"chunk": false,
"sendError": true,
"encoding": "utf8",
"allProps": false,
"x": 420,
"y": 140,
"wires": [["func_parse_config"]]
},
{
"id": "func_parse_config",
"type": "function",
"z": "smoke_flow_tab",
"name": "📋 Config parsen (V8 Gruppen)",
"func": "// =====================================================\n// PARSE CONFIG MIT GRUPPEN-SUPPORT - V8\n// =====================================================\n\nconst ha = global.get('homeassistant.homeAssistant.states');\nif (!ha) {\n node.error('Home Assistant nicht verbunden!');\n node.status({fill:'red', shape:'ring', text:'HA nicht verbunden!'});\n return null;\n}\n\nlet config;\n\n// Prüfe ob Datei gelesen wurde oder Fehler\nif (msg.error) {\n node.warn('Config-Datei nicht gefunden. Warte auf erste Speicherung.');\n node.status({fill:'yellow', shape:'ring', text:'⚠️ Keine Config'});\n config = { groups: [] };\n global.set('smoke_config', config);\n return null;\n}\n\ntry {\n config = JSON.parse(msg.payload);\n} catch (e) {\n node.error('Config JSON ungültig: ' + e.message);\n node.status({fill:'red', shape:'ring', text:'❌ JSON ungültig!'});\n return null;\n}\n\n// Migration: Alte Config ohne groups\nif (!config.groups && (config.devices || config.detectors)) {\n node.log('Migriere alte Config zu Gruppen-Struktur...');\n config = {\n groups: [{\n id: 1,\n name: 'Zuhause',\n notification_text: config.notification_text || '🔥 RAUCHMELDER ALARM! {detector} hat Rauch erkannt!',\n tts_interval: config.tts_interval || 10,\n tts_language: config.tts_language || 'de',\n detectors: config.detectors || [],\n devices: config.devices || []\n }]\n };\n}\n\n// Sicherstellen dass groups existiert\nconfig.groups = config.groups || [];\n\n// Config global speichern\nglobal.set('smoke_config', config);\n\n// TTS-Status für alle Geräte in allen Gruppen initialisieren\nconfig.groups.forEach(group => {\n (group.devices || []).forEach(device => {\n const key = `tts_active_${group.id}_${device.device_id}`;\n if (flow.get(key) === undefined) {\n flow.set(key, false);\n }\n });\n});\n\n// Statistik\nconst totalDetectors = config.groups.reduce((sum, g) => sum + (g.detectors?.length || 0), 0);\nconst totalDevices = config.groups.reduce((sum, g) => sum + (g.devices?.length || 0), 0);\n\nconst statusText = `✅ ${config.groups.length} Gruppen, ${totalDetectors} RM, ${totalDevices} Geräte`;\nnode.status({fill:'green', shape:'dot', text: statusText});\n\nif (config.groups.length === 0) {\n node.warn('Keine Gruppen konfiguriert!');\n node.status({fill:'yellow', shape:'ring', text:'⚠️ Keine Gruppen'});\n}\n\nnode.log('Config geladen: ' + statusText);\n\nmsg.config = config;\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 680,
"y": 140,
"wires": [["debug_config"]]
},
{
"id": "event_config_update",
"type": "server-events",
"z": "smoke_flow_tab",
"name": "📥 Config Update Event",
"server": "",
"version": 2,
"eventType": "smoke_config_update",
"exposeToHomeAssistant": false,
"haConfig": [],
"waitForRunning": true,
"outputProperties": [
{"property": "payload", "propertyType": "msg", "value": "", "valueType": "eventData"}
],
"x": 180,
"y": 220,
"wires": [["func_prepare_save"]]
},
{
"id": "func_prepare_save",
"type": "function",
"z": "smoke_flow_tab",
"name": "💾 Config vorbereiten",
"func": "const eventData = msg.payload;\n\nlet config = null;\n\nif (eventData && eventData.config) {\n config = eventData.config;\n} else if (eventData && eventData.event && eventData.event.config) {\n config = eventData.event.config;\n} else if (eventData && eventData.groups) {\n config = eventData;\n}\n\nif (!config) {\n node.warn('Keine Config im Event gefunden!');\n node.status({fill:'red', shape:'ring', text:'❌ Keine Config!'});\n return null;\n}\n\n// Sicherstellen dass groups existiert\nif (!config.groups) {\n config = { groups: [] };\n}\n\nmsg.payload = JSON.stringify(config, null, 2);\n\nconst totalGroups = config.groups?.length || 0;\nnode.status({fill:'blue', shape:'dot', text:'Speichere ' + totalGroups + ' Gruppen...'});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 220,
"wires": [["file_write_config"]]
},
{
"id": "file_write_config",
"type": "file",
"z": "smoke_flow_tab",
"name": "💾 In Datei speichern",
"filename": "/homeassistant/www/smoke_config.json",
"filenameType": "str",
"appendNewline": false,
"createDir": true,
"overwriteFile": "true",
"encoding": "utf8",
"x": 640,
"y": 220,
"wires": [["func_save_success"]]
},
{
"id": "func_save_success",
"type": "function",
"z": "smoke_flow_tab",
"name": "✅ Gespeichert + Neu laden",
"func": "node.status({fill:'green', shape:'dot', text:'✅ Config gespeichert!'});\nnode.log('Config gespeichert');\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 860,
"y": 220,
"wires": [["file_read_config"]]
},
{
"id": "debug_config",
"type": "debug",
"z": "smoke_flow_tab",
"name": "Config Debug",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "config",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 910,
"y": 140,
"wires": []
},
{
"id": "comment_trigger",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ RAUCHMELDER TRIGGER ━━━━━━━━━━",
"info": "",
"x": 220,
"y": 300,
"wires": []
},
{
"id": "trigger_smoke_all",
"type": "server-state-changed",
"z": "smoke_flow_tab",
"name": "🔥 Alle binary_sensor.*",
"server": "",
"version": 4,
"exposeToHomeAssistant": false,
"haConfig": [],
"entityidfilter": "binary_sensor.",
"entityidfiltertype": "substring",
"outputinitially": false,
"state_type": "str",
"haltifstate": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"outputs": 1,
"output_only_on_state_change": true,
"for": "0",
"forType": "num",
"forUnits": "minutes",
"ignorePrevStateNull": false,
"ignorePrevStateUnknown": false,
"ignorePrevStateUnavailable": false,
"ignoreCurrentStateUnknown": false,
"ignoreCurrentStateUnavailable": false,
"outputProperties": [
{"property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState"},
{"property": "data", "propertyType": "msg", "value": "", "valueType": "eventData"}
],
"x": 160,
"y": 340,
"wires": [["func_filter_smoke_groups"]]
},
{
"id": "func_filter_smoke_groups",
"type": "function",
"z": "smoke_flow_tab",
"name": "🎯 Filter: Welche Gruppe?",
"func": "// =====================================================\n// FINDE GRUPPE FÜR DIESEN RAUCHMELDER\n// =====================================================\n\nconst config = global.get('smoke_config');\n\nif (!config || !config.groups || config.groups.length === 0) {\n return null;\n}\n\nconst entityId = msg.data.entity_id || (msg.data.new_state ? msg.data.new_state.entity_id : null);\n\nif (!entityId) {\n return null;\n}\n\n// Finde die Gruppe, die diesen Sensor enthält\nlet foundGroup = null;\nlet foundDetector = null;\n\nfor (let group of config.groups) {\n const detector = (group.detectors || []).find(d => d.sensor === entityId);\n if (detector) {\n foundGroup = group;\n foundDetector = detector;\n break;\n }\n}\n\nif (!foundGroup) {\n // Sensor ist in keiner Gruppe konfiguriert\n return null;\n}\n\nconst newState = msg.payload;\nconst oldState = msg.data.old_state ? msg.data.old_state.state : 'off';\n\nmsg.entity_id = entityId;\nmsg.group = foundGroup;\nmsg.detector = foundDetector;\nmsg.config = config;\n\nif (newState === 'on' && oldState !== 'on') {\n node.status({fill:'red', shape:'dot', text:'🔥 ' + foundGroup.name + ': ' + entityId});\n msg.event = 'smoke_detected';\n return [msg, null];\n}\n\nif (newState === 'off' && oldState === 'on') {\n node.status({fill:'green', shape:'dot', text:'✅ ' + foundGroup.name + ': ' + entityId});\n msg.event = 'smoke_cleared';\n return [null, msg];\n}\n\nreturn null;",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 430,
"y": 340,
"wires": [["func_process_alarm_group"], ["func_check_group_clear"]],
"outputLabels": ["Rauch erkannt", "Rauch weg"]
},
{
"id": "func_process_alarm_group",
"type": "function",
"z": "smoke_flow_tab",
"name": "🚨 Alarm für Gruppe",
"func": "const group = msg.group;\nconst detector = msg.detector;\nconst triggered = msg.entity_id;\n\n// Detector Name ermitteln\nlet detector_name = detector.name;\nif (!detector_name && msg.data.new_state && msg.data.new_state.attributes) {\n detector_name = msg.data.new_state.attributes.friendly_name;\n}\nif (!detector_name) {\n detector_name = triggered.replace('binary_sensor.', '').replace(/_/g, ' ');\n}\n\nconst message = group.notification_text.replace(/{detector}/g, detector_name);\n\nmsg.triggered_detector = triggered;\nmsg.detector_name = detector_name;\nmsg.message = message;\n\nnode.status({fill:'red', shape:'dot', text:'🔥 ' + group.name + ': ' + detector_name});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 710,
"y": 320,
"wires": [["func_turn_on_group_siren"]]
},
{
"id": "func_turn_on_group_siren",
"type": "function",
"z": "smoke_flow_tab",
"name": "📢 Gruppen-Sirene EIN",
"func": "// =====================================================\n// ALLE SIRENEN DER GRUPPE EINSCHALTEN\n// =====================================================\n\nconst group = msg.group;\n\n// Sammle ALLE Sirenen der Gruppe (nicht nur die des auslösenden Detectors)\nconst sirens = (group.detectors || [])\n .filter(d => d.siren)\n .map(d => d.siren);\n\nif (sirens.length === 0) {\n node.status({fill:'yellow', shape:'dot', text:'Keine Sirenen in Gruppe ' + group.name});\n return [null, msg];\n}\n\n// Duplikate entfernen (falls mehrere Detectors die gleiche Sirene haben)\nconst uniqueSirens = [...new Set(sirens)];\n\nconst alarmMsgs = uniqueSirens.map(siren => ({\n payload: {\n data: {\n entity_id: siren\n }\n }\n}));\n\nnode.status({fill:'red', shape:'dot', text: group.name + ': ' + uniqueSirens.length + ' Sirene(n) EIN'});\nnode.log('Gruppe ' + group.name + ': Schalte ' + uniqueSirens.length + ' Sirene(n) ein: ' + uniqueSirens.join(', '));\n\nreturn [alarmMsgs, msg];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 940,
"y": 320,
"wires": [["ha_switch_on"], ["func_notify_group_devices"]],
"outputLabels": ["Sirenen", "Weiter"]
},
{
"id": "ha_switch_on",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🔔 Switch EIN",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "switch",
"service": "turn_on",
"areaId": [],
"deviceId": [],
"entityId": ["{{payload.data.entity_id}}"],
"data": "",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 1160,
"y": 300,
"wires": [[]]
},
{
"id": "comment_notifications",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ BENACHRICHTIGUNGEN (NUR GRUPPEN-GERÄTE) ━━━━━━━━━━",
"info": "",
"x": 300,
"y": 400,
"wires": []
},
{
"id": "func_notify_group_devices",
"type": "function",
"z": "smoke_flow_tab",
"name": "📱 Nur Geräte dieser Gruppe",
"func": "// =====================================================\n// NUR GERÄTE DER BETROFFENEN GRUPPE BENACHRICHTIGEN\n// =====================================================\n\nconst group = msg.group;\nconst devices = group.devices || [];\n\nif (devices.length === 0) {\n node.warn('Keine Geräte in Gruppe \"' + group.name + '\" konfiguriert!');\n node.status({fill:'yellow', shape:'dot', text:'Keine Geräte in ' + group.name});\n return null;\n}\n\nconst messages = [];\n\nfor (let device of devices) {\n // TTS-Key enthält jetzt auch die Group ID\n const ttsKey = `tts_active_${group.id}_${device.device_id}`;\n const isActive = flow.get(ttsKey);\n \n if (isActive) {\n node.log('TTS bereits aktiv für: ' + device.device_name + ' in Gruppe ' + group.name);\n continue;\n }\n \n const newMsg = RED.util.cloneMessage(msg);\n newMsg.device = device;\n messages.push(newMsg);\n}\n\nif (messages.length === 0) {\n node.status({fill:'yellow', shape:'dot', text:'Alle Geräte in ' + group.name + ' bereits aktiv'});\n return null;\n}\n\nnode.status({fill:'blue', shape:'dot', text: group.name + ': ' + messages.length + ' Geräte'});\n\nreturn [messages];",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 190,
"y": 460,
"wires": [["func_mark_group_active"]]
},
{
"id": "func_mark_group_active",
"type": "function",
"z": "smoke_flow_tab",
"name": "✅ Markiere als aktiv",
"func": "const device = msg.device;\nconst group = msg.group;\n\n// TTS-Key mit Group ID\nconst ttsKey = `tts_active_${group.id}_${device.device_id}`;\nflow.set(ttsKey, true);\n\nnode.status({fill:'green', shape:'dot', text: '✅ ' + group.name + ': ' + device.device_name});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 460,
"y": 460,
"wires": [["func_send_group_notification"]]
},
{
"id": "func_send_group_notification",
"type": "function",
"z": "smoke_flow_tab",
"name": "📝 Gruppen-Notification",
"func": "const device = msg.device;\nconst group = msg.group;\nconst message = msg.message;\nconst triggered = msg.triggered_detector;\n\nconst serviceName = 'mobile_app_' + device.device_id;\n\n// Tag enthält Group ID für späteres Löschen\nconst tag = 'smoke_alarm_' + group.id + '_' + triggered.replace(/\\./g, '_');\n\n// Action ID enthält Group ID und Device ID\nconst actionId = 'STOP_SMOKE_' + group.id + '_' + device.device_id;\n\nmsg.notify_service = serviceName;\nmsg.notify_message = message;\nmsg.notify_title = '🚨 ' + group.name + ' - RAUCHMELDER ALARM';\nmsg.notify_tag = tag;\nmsg.notify_action_id = actionId;\n\nmsg.notification_tag = tag;\nmsg.service_name = serviceName;\nmsg.our_device_id = device.device_id;\n\nnode.status({fill:'green', shape:'dot', text: group.name + ': ' + serviceName});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 460,
"wires": [["switch_notify_platform", "func_start_group_tts"]]
},
{
"id": "switch_notify_platform",
"type": "switch",
"z": "smoke_flow_tab",
"name": "📱 iOS / Android?",
"property": "device.is_ios",
"propertyType": "msg",
"rules": [
{"t": "true"},
{"t": "else"}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 170,
"y": 520,
"wires": [["ha_notify_ios"], ["ha_notify_android"]],
"outputLabels": ["iOS", "Android"]
},
{
"id": "ha_notify_ios",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🍎 iOS Notification",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"{{notify_message}}\\n\\n👆 Lange drücken für STOPP\", \"title\": \"{{notify_title}}\", \"data\": {\"tag\": \"{{notify_tag}}\", \"push\": {\"sound\": {\"name\": \"default\", \"critical\": 0, \"volume\": 0.5}, \"interruption-level\": \"time-sensitive\"}, \"presentation_options\": [\"alert\", \"badge\"], \"actions\": [{\"action\": \"SILENCE\", \"title\": \"✓ OK\", \"authenticationRequired\": false}, {\"action\": \"{{notify_action_id}}\", \"title\": \"🛑 STOPP\", \"destructive\": true}]}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 420,
"y": 500,
"wires": [[]]
},
{
"id": "ha_notify_android",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🤖 Android Notification",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"{{notify_message}}\\n\\n⬇ Runterziehen für STOPP-Button\", \"title\": \"{{notify_title}}\", \"data\": {\"tag\": \"{{notify_tag}}\", \"channel\": \"alarm_stream_max\", \"importance\": \"high\", \"priority\": \"high\", \"sticky\": true, \"persistent\": true, \"color\": \"#ff0000\", \"clickAction\": \"noAction\", \"notification_icon\": \"mdi:fire\", \"visibility\": \"public\", \"sound\": \"none\", \"actions\": [{\"action\": \"{{notify_action_id}}\", \"title\": \"🛑 STOPP TTS\"}]}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 430,
"y": 540,
"wires": [[]]
},
{
"id": "comment_tts",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ TTS LOOP ━━━━━━━━━━",
"info": "",
"x": 190,
"y": 580,
"wires": []
},
{
"id": "func_start_group_tts",
"type": "function",
"z": "smoke_flow_tab",
"name": "🔊 Gruppen-TTS starten",
"func": "const device = msg.device;\nconst group = msg.group;\n\nmsg.tts_device = device;\nmsg.tts_group = group;\nmsg.loop_count = 0;\n\n// TTS Intervall aus Gruppe\nmsg.delay = (group.tts_interval || 10) * 1000;\n\nnode.status({fill:'blue', shape:'dot', text: group.name + ': TTS Start ' + device.device_name});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 180,
"y": 640,
"wires": [["func_check_group_tts_active"]]
},
{
"id": "func_check_group_tts_active",
"type": "function",
"z": "smoke_flow_tab",
"name": "🔄 Gruppen-TTS aktiv?",
"func": "const device = msg.tts_device;\nconst group = msg.tts_group;\n\nif (!device || !group) {\n node.warn('TTS Check: device oder group fehlt!');\n return null;\n}\n\n// TTS-Key mit Group ID prüfen\nconst ttsKey = `tts_active_${group.id}_${device.device_id}`;\nconst isActive = flow.get(ttsKey);\n\nnode.log('TTS Check - Key: ' + ttsKey + ', Active: ' + isActive);\n\nif (!isActive) {\n node.status({fill:'green', shape:'dot', text: group.name + ': TTS gestoppt ' + device.device_name});\n node.log('TTS Loop beendet für ' + device.device_name);\n return null;\n}\n\nmsg.loop_count = (msg.loop_count || 0) + 1;\nnode.status({fill:'blue', shape:'ring', text: group.name + ' #' + msg.loop_count + ': ' + device.device_name});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 640,
"wires": [["switch_platform"]]
},
{
"id": "switch_platform",
"type": "switch",
"z": "smoke_flow_tab",
"name": "📱 iOS / Android?",
"property": "tts_device.is_ios",
"propertyType": "msg",
"rules": [
{"t": "true"},
{"t": "else"}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 690,
"y": 640,
"wires": [["func_tts_ios"], ["func_tts_android"]],
"outputLabels": ["iOS", "Android"]
},
{
"id": "func_tts_ios",
"type": "function",
"z": "smoke_flow_tab",
"name": "🍎 iOS Critical Alert",
"func": "const device = msg.tts_device;\nconst group = msg.tts_group;\n\nconst serviceName = 'mobile_app_' + device.device_id;\n\nmsg.notify_service = serviceName;\nmsg.tts_message = msg.message;\nmsg.tts_title = '🚨 ' + group.name + ' - ALARM';\n\nnode.status({fill:'red', shape:'dot', text: group.name + ': Critical ' + device.device_name});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 190,
"y": 720,
"wires": [["ha_tts_ios", "delay_tts"]]
},
{
"id": "ha_tts_ios",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🍎 iOS Critical Alert",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"{{tts_message}}\", \"title\": \"{{tts_title}}\", \"data\": {\"push\": {\"sound\": {\"name\": \"default\", \"critical\": 1, \"volume\": 1.0}}, \"tag\": \"smoke_critical\"}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 440,
"y": 720,
"wires": [["delay_clear_ios"]]
},
{
"id": "delay_clear_ios",
"type": "delay",
"z": "smoke_flow_tab",
"name": "⏱️ 2s warten",
"pauseType": "delay",
"timeout": "2",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 720,
"wires": [["ha_clear_ios"]]
},
{
"id": "ha_clear_ios",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🗑️ iOS Alert löschen",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"clear_notification\", \"data\": {\"tag\": \"smoke_critical\"}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 830,
"y": 720,
"wires": [[]]
},
{
"id": "func_tts_android",
"type": "function",
"z": "smoke_flow_tab",
"name": "🤖 Android TTS",
"func": "const device = msg.tts_device;\nconst group = msg.tts_group;\n\nconst serviceName = 'mobile_app_' + device.device_id;\n\nmsg.notify_service = serviceName;\nmsg.tts_text = msg.message;\n\nnode.status({fill:'blue', shape:'dot', text: group.name + ': ' + serviceName});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 180,
"y": 780,
"wires": [["ha_tts_android", "delay_tts"]]
},
{
"id": "ha_tts_android",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🤖 TTS Android",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"TTS\", \"data\": {\"media_stream\": \"alarm_stream\", \"tts_text\": \"{{tts_text}}\"}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 400,
"y": 780,
"wires": [[]]
},
{
"id": "delay_tts",
"type": "delay",
"z": "smoke_flow_tab",
"name": "⏱️ Warte TTS Intervall",
"pauseType": "delayv",
"timeout": "10",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 750,
"wires": [["func_check_group_tts_active"]]
},
{
"id": "comment_stop",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ STOPP BUTTON (PRO GRUPPE) ━━━━━━━━━━",
"info": "",
"x": 250,
"y": 860,
"wires": []
},
{
"id": "event_stop_button",
"type": "server-events",
"z": "smoke_flow_tab",
"name": "🛑 STOPP Button Event",
"server": "",
"version": 2,
"eventType": "mobile_app_notification_action",
"exposeToHomeAssistant": false,
"haConfig": [],
"waitForRunning": true,
"outputProperties": [
{"property": "payload", "propertyType": "msg", "value": "", "valueType": "eventData"}
],
"x": 180,
"y": 920,
"wires": [["func_check_group_stop"]]
},
{
"id": "func_check_group_stop",
"type": "function",
"z": "smoke_flow_tab",
"name": "🔍 STOP_SMOKE Gruppe prüfen",
"func": "// =====================================================\n// STOPP NUR FÜR DIESE GRUPPE + GERÄT\n// =====================================================\n\nnode.log('STOPP Event empfangen: ' + JSON.stringify(msg.payload));\n\nconst action = msg.payload.event ? msg.payload.event.action : msg.payload.action;\n\nnode.log('Action: ' + action);\n\n// Format: STOP_SMOKE_{groupId}_{deviceId}\nif (!action || !action.startsWith('STOP_SMOKE_')) {\n node.log('Ignoriere Action (kein STOP_SMOKE_): ' + action);\n return null;\n}\n\n// Parse Group ID und Device ID\nconst parts = action.replace('STOP_SMOKE_', '').split('_');\nnode.log('Parts: ' + JSON.stringify(parts));\n\nif (parts.length < 2) {\n node.warn('Ungültiges Action-Format: ' + action);\n return null;\n}\n\n// Erste Zahl ist die Group ID, Rest ist Device ID\nconst groupId = parseInt(parts[0]);\nconst deviceId = parts.slice(1).join('_');\n\nnode.log('Parsed - GroupID: ' + groupId + ', DeviceID: ' + deviceId);\n\nif (isNaN(groupId)) {\n node.warn('Ungültige Group ID: ' + parts[0]);\n return null;\n}\n\nmsg.stop_group_id = groupId;\nmsg.stop_device_id = deviceId;\n\nnode.status({fill:'yellow', shape:'dot', text:'STOPP Gruppe ' + groupId + ': ' + deviceId});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 920,
"wires": [["func_stop_group_tts"]]
},
{
"id": "func_stop_group_tts",
"type": "function",
"z": "smoke_flow_tab",
"name": "⏹️ Gruppen-TTS stoppen",
"func": "const groupId = msg.stop_group_id;\nconst deviceId = msg.stop_device_id;\nconst config = global.get('smoke_config');\n\nif (!config || !config.groups) {\n node.warn('Keine Config vorhanden!');\n return null;\n}\n\nconst group = config.groups.find(g => g.id === groupId);\nif (!group) {\n node.warn('Gruppe nicht gefunden: ' + groupId);\n node.warn('Verfügbare Gruppen: ' + config.groups.map(g => g.id + ':' + g.name).join(', '));\n return null;\n}\n\nconst device = group.devices.find(d => d.device_id === deviceId);\nif (!device) {\n node.warn('Gerät nicht in Gruppe gefunden: ' + deviceId);\n node.warn('Verfügbare Geräte: ' + group.devices.map(d => d.device_id).join(', '));\n return null;\n}\n\n// TTS-Key mit Group ID\nconst ttsKey = `tts_active_${groupId}_${deviceId}`;\nflow.set(ttsKey, false);\n\nnode.status({fill:'green', shape:'dot', text: group.name + ': TTS gestoppt ' + device.device_name});\nnode.log('TTS gestoppt für ' + device.device_name + ' in Gruppe ' + group.name + ' (Key: ' + ttsKey + ')');\n\nmsg.device = device;\nmsg.group = group;\n\n// Reset-Message für Delay-Node senden (löscht wartende Messages)\nconst resetMsg = { reset: true };\n\nreturn [msg, resetMsg];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 750,
"y": 920,
"wires": [["func_clear_group_notifications"], ["delay_tts"]]
},
{
"id": "func_clear_group_notifications",
"type": "function",
"z": "smoke_flow_tab",
"name": "🗑️ Gruppen-Notifications löschen",
"func": "const device = msg.device;\nconst group = msg.group;\n\nconst serviceName = 'mobile_app_' + device.device_id;\n\nconst messages = [];\n\nfor (let det of group.detectors) {\n // Tag enthält Group ID\n const tag = 'smoke_alarm_' + group.id + '_' + det.sensor.replace(/\\./g, '_');\n \n messages.push({\n notify_service: serviceName,\n clear_tag: tag\n });\n}\n\nnode.status({fill:'green', shape:'dot', text: group.name + ': ' + messages.length + ' Notifications gelöscht'});\n\nreturn [messages];",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 230,
"y": 980,
"wires": [["ha_clear_notification"]]
},
{
"id": "ha_clear_notification",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🗑️ Notification löschen",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "notify",
"service": "{{notify_service}}",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "{\"message\": \"clear_notification\", \"data\": {\"tag\": \"{{clear_tag}}\"}}",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 520,
"y": 980,
"wires": [[]]
},
{
"id": "comment_clear",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ RAUCH WEG (PRO GRUPPE) ━━━━━━━━━━",
"info": "",
"x": 250,
"y": 1040,
"wires": []
},
{
"id": "func_check_group_clear",
"type": "function",
"z": "smoke_flow_tab",
"name": "🔍 Alle RM in Gruppe klar?",
"func": "// =====================================================\n// PRÜFE OB ALLE RAUCHMELDER IN DIESER GRUPPE KLAR SIND\n// =====================================================\n\nconst group = msg.group;\nconst cleared = msg.entity_id;\nconst ha = global.get('homeassistant.homeAssistant.states');\n\n// Prüfe nur Rauchmelder in dieser Gruppe\nfor (let det of group.detectors) {\n if (det.sensor !== cleared) {\n const state = ha[det.sensor];\n if (state && state.state === 'on') {\n node.status({fill:'yellow', shape:'dot', text: group.name + ': Noch aktiv ' + det.sensor});\n return null;\n }\n }\n}\n\nnode.status({fill:'green', shape:'dot', text:'✅ ' + group.name + ': Alle klar'});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 360,
"wires": [["func_turn_off_group_sirens"]]
},
{
"id": "func_turn_off_group_sirens",
"type": "function",
"z": "smoke_flow_tab",
"name": "🔇 Gruppen-Sirenen AUS",
"func": "// =====================================================\n// NUR SIRENEN DIESER GRUPPE AUSSCHALTEN\n// =====================================================\n\nconst group = msg.group;\n\nconst sirens = (group.detectors || [])\n .filter(d => d.siren)\n .map(d => d.siren);\n\nif (sirens.length === 0) {\n return null;\n}\n\nconst uniqueSirens = [...new Set(sirens)];\n\nconst messages = uniqueSirens.map(siren => ({\n payload: {\n data: {\n entity_id: siren\n }\n }\n}));\n\nnode.status({fill:'green', shape:'dot', text: group.name + ': ' + uniqueSirens.length + ' Sirenen aus'});\n\nreturn [messages];",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 190,
"y": 1100,
"wires": [["ha_switch_off"]]
},
{
"id": "ha_switch_off",
"type": "api-call-service",
"z": "smoke_flow_tab",
"name": "🔕 Switch AUS",
"server": "",
"version": 5,
"debugenabled": false,
"domain": "switch",
"service": "turn_off",
"areaId": [],
"deviceId": [],
"entityId": ["{{payload.data.entity_id}}"],
"data": "",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"x": 440,
"y": 1100,
"wires": [[]]
},
{
"id": "comment_emergency",
"type": "comment",
"z": "smoke_flow_tab",
"name": "━━━━━━━━━━ NOTFALL-STOPP (PRO GRUPPE) ━━━━━━━━━━",
"info": "",
"x": 260,
"y": 1160,
"wires": []
},
{
"id": "event_stop_group",
"type": "server-events",
"z": "smoke_flow_tab",
"name": "🛑 STOPP GRUPPE Event",
"server": "",
"version": 2,
"eventType": "smoke_stop_group",
"exposeToHomeAssistant": false,
"haConfig": [],
"waitForRunning": true,
"outputProperties": [
{"property": "payload", "propertyType": "msg", "value": "", "valueType": "eventData"}
],
"x": 190,
"y": 1220,
"wires": [["func_stop_group_all_tts"]]
},
{
"id": "func_stop_group_all_tts",
"type": "function",
"z": "smoke_flow_tab",
"name": "⏹️ Alle TTS in Gruppe stoppen",
"func": "// =====================================================\n// STOPPE ALLE TTS-LOOPS IN EINER BESTIMMTEN GRUPPE\n// =====================================================\n\nconst eventData = msg.payload;\nconst groupId = eventData.group_id || (eventData.event && eventData.event.group_id);\nconst groupName = eventData.group_name || (eventData.event && eventData.event.group_name) || 'Unbekannt';\n\nif (!groupId) {\n node.warn('Keine group_id im Event!');\n return null;\n}\n\nconst config = global.get('smoke_config');\n\nif (!config || !config.groups) {\n node.warn('Keine Config vorhanden!');\n return null;\n}\n\nconst group = config.groups.find(g => g.id === groupId);\nif (!group) {\n node.warn('Gruppe nicht gefunden: ' + groupId);\n return null;\n}\n\nlet stoppedCount = 0;\nfor (let device of group.devices) {\n const ttsKey = `tts_active_${groupId}_${device.device_id}`;\n if (flow.get(ttsKey)) {\n flow.set(ttsKey, false);\n stoppedCount++;\n }\n}\n\nnode.status({fill:'green', shape:'dot', text: `✅ ${group.name}: ${stoppedCount} TTS gestoppt`});\nnode.log('Gruppe ' + group.name + ': ' + stoppedCount + ' TTS-Loops gestoppt');\n\n// Reset-Message für Delay-Node senden (löscht wartende Messages)\nconst resetMsg = { reset: true };\n\nreturn [null, resetMsg];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 1220,
"wires": [[], ["delay_tts"]]
},
{
"id": "event_stop_all",
"type": "server-events",
"z": "smoke_flow_tab",
"name": "🛑 STOPP ALLE Event",
"server": "",
"version": 2,
"eventType": "smoke_stop_all",
"exposeToHomeAssistant": false,
"haConfig": [],
"waitForRunning": true,
"outputProperties": [
{"property": "payload", "propertyType": "msg", "value": "", "valueType": "eventData"}
],
"x": 180,
"y": 1280,
"wires": [["func_stop_all_groups_tts"]]
},
{
"id": "func_stop_all_groups_tts",
"type": "function",
"z": "smoke_flow_tab",
"name": "⏹️ ALLE TTS stoppen (alle Gruppen)",
"func": "// =====================================================\n// STOPPE ALLE TTS-LOOPS IN ALLEN GRUPPEN\n// =====================================================\n\nconst config = global.get('smoke_config');\n\nif (!config || !config.groups) {\n node.warn('Keine Config vorhanden!');\n return null;\n}\n\nlet stoppedCount = 0;\nfor (let group of config.groups) {\n for (let device of group.devices) {\n const ttsKey = `tts_active_${group.id}_${device.device_id}`;\n if (flow.get(ttsKey)) {\n flow.set(ttsKey, false);\n stoppedCount++;\n }\n }\n}\n\nnode.status({fill:'green', shape:'dot', text: `✅ ${stoppedCount} TTS gestoppt (alle Gruppen)`});\nnode.log('NOTFALL: ' + stoppedCount + ' TTS-Loops in allen Gruppen gestoppt');\n\n// Reset-Message für Delay-Node senden (löscht wartende Messages)\nconst resetMsg = { reset: true };\n\nreturn [null, resetMsg];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 520,
"y": 1280,
"wires": [[], ["delay_tts"]]
}
]