From 81fe1ac188fbd657b5fb0e353cac7541401d3b7b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 24 Dec 2025 03:23:23 +0100 Subject: [PATCH] tts for ios removed, browser mod removes, only critical for ios and only tts for android --- README.md | 200 +++- esphome/test_smoke_detector.yaml | 182 ++++ node-red/flows/smoke_detector.json | 784 ++++++++++++++++ node-red/settings.html | 1358 ++++++++++++++++++++++++++++ 4 files changed, 2523 insertions(+), 1 deletion(-) create mode 100644 esphome/test_smoke_detector.yaml create mode 100644 node-red/flows/smoke_detector.json create mode 100644 node-red/settings.html diff --git a/README.md b/README.md index 9c891b4..98cff9b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,200 @@ -# ha-smoke-detection-notify +# Rauchmelder-Benachrichtigungen für Home Assistant +**Node-RED Lösung mit integrierter Settings-Seite** + +## Features + +- **Eine Settings-Seite** - Alles zentral konfigurieren (auch als Dashboard-Karte) +- **KEIN Helper nötig** - Kein YAML editieren, keine Entities erstellen +- **Sirene pro Rauchmelder** - Jeder Rauchmelder kann seine eigene Sirene haben +- **Dropdown-Auswahl** - Entities werden automatisch aus Home Assistant geladen +- **iOS & Android** - Unterschiedliche TTS-Methoden +- **STOPP pro Gerät** - Individuelles Stoppen +- **Visuelles Debugging** - Node-RED Debug-Panel + +--- + +## Voraussetzungen + +- Home Assistant +- Node-RED Add-on +- Mobile App auf allen Geräten (iOS & Android) + +--- + +## Installation + +### 1. Settings-Seite in Home Assistant ablegen + +Kopiere `node-red/settings.html` nach `/config/www/settings.html` + +Die Datei ist dann erreichbar unter: +``` +http://homeassistant.local:8123/local/settings.html +``` + +### 2. Node-RED Flow importieren + +1. Öffne **Node-RED** +2. **Menü** (☰) → **Import** +3. Kopiere Inhalt von `node-red/flows/smoke_detector.json` +4. **Importieren** → **Deploy** + +### 3. Long-Lived Token erstellen + +1. Home Assistant → **Profil** (unten links) → **Sicherheit** +2. Runterscrollen zu **"Langlebige Zugriffstokens"** +3. **Token erstellen** → Name: `Rauchmelder Settings` +4. Token kopieren (sieht so aus: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3Mi...`) + +### 4. Home Assistant Server verbinden + +**Einmalig Token einrichten:** +1. Doppelklick auf **einen** blauen Node +2. **Stift-Symbol** neben Server +3. **Base URL:** `http://homeassistant.local:8123` +4. **Access Token:** Den eben erstellten Token einfügen +5. **Update** → **Done** + +**Alle blauen Nodes konfigurieren:** + +Danach musst du **jeden blauen Node** einzeln durchgehen und den Server auswählen: +1. Doppelklick auf den blauen Node +2. Bei **Server**: Wähle den **zweiten Eintrag** in der Liste (der mit deiner URL) +3. **Done** +4. Wiederhole für alle blauen Nodes + +> **Hinweis:** Den Token musst du nur einmal eingeben. Aber jeder blaue Node muss manuell auf den Server verweisen. + +Wenn alle Nodes konfiguriert sind: **Deploy** + +### 5. Settings-Seite öffnen + +**Option A: Direkt im Browser** +``` +http://homeassistant.local:8123/local/settings.html?token=DEIN_TOKEN +``` + +**Option B: Als Dashboard-Karte (iframe)** - Empfohlen! + +Dashboard bearbeiten → Karte hinzufügen → Webpage: +```yaml +type: iframe +url: /local/settings.html?token=DEIN_TOKEN +aspect_ratio: 100% +``` + +**Option C: Als Seitenleisten-Panel** + +> **Hinweis:** Seit Home Assistant 2024.4.0 ist `panel_iframe` über YAML deprecated. Nutze stattdessen Option B (Dashboard-Karte) oder erstelle ein eigenes Dashboard: + +1. **Einstellungen** → **Dashboards** → **Dashboard hinzufügen** +2. Name: `Rauchmelder`, Icon: `mdi:fire` +3. **In Seitenleiste anzeigen** aktivieren +4. Dashboard öffnen → **Dashboard bearbeiten** → Karte hinzufügen → **Webpage** +5. URL: `/local/settings.html?token=DEIN_TOKEN` + +### 6. Konfigurieren & Testen + +1. Konfiguriere alles in der Settings-Seite: + - Benachrichtigungstext + - TTS Intervall & Sprache + - Rauchmelder (aus Dropdown wählen) + - Sirene pro Rauchmelder (optional) + - Anzeigenamen (optional) + - Geräte (iOS/Android) + +2. Klicke **"Speichern"** + +3. Die Config wird automatisch an Node-RED gesendet + +4. Nutze den **Test-Button** oder setze einen Rauchmelder auf `on` + +--- + +## Settings-Seite + +Die Settings-Seite bietet: + +| Bereich | Optionen | +|---------|----------| +| **Benachrichtigungen** | Text mit `{detector}` Platzhalter, TTS Intervall (5-60s), Sprache | +| **Rauchmelder** | Dropdown-Auswahl der Rauchmelder-Sensoren aus HA | +| **Sirene** | Pro Rauchmelder optional eine Sirene/Switch zuweisen | +| **Namen** | Optionaler Anzeigename für jeden Rauchmelder | +| **Geräte** | Android (Name + ID + TTS) oder iOS (Name + ID + Critical Alert) | + +--- + +## iOS Setup (Critical Alerts) + +Für iOS-Geräte werden **Critical Alerts** verwendet. Diese sind laute Benachrichtigungen die auch bei stummgeschaltetem Gerät durchkommen. + +### Critical Alerts aktivieren + +1. **Home Assistant Companion App** öffnen +2. **Einstellungen** → **Companion App** → **Benachrichtigungen** +3. **Critical Alerts erlauben** aktivieren + +### iOS Einstellungen prüfen + +1. **iOS Einstellungen** → **Mitteilungen** → **Home Assistant** +2. Prüfe ob **Kritische Hinweise** aktiviert ist + +### Wie es funktioniert + +- Bei Rauchmelder-Alarm bekommt das iOS-Gerät einen **Critical Alert** +- Der Alert spielt einen lauten Sound ab (auch bei Stumm-Modus!) +- Die Nachricht zeigt an welcher Rauchmelder ausgelöst hat +- Der Alert wiederholt sich alle X Sekunden bis STOPP gedrückt wird + +### Wichtig + +- Critical Alerts umgehen den Stumm-Modus und "Nicht Stören" +- Der Sound ist laut - perfekt für Notfälle +- Funktioniert auch im Hintergrund und bei gesperrtem Gerät + +--- + +## So funktioniert's + +1. **Trigger:** Rauchmelder geht auf `on` +2. **Filter:** Ist es ein konfigurierter Rauchmelder? +3. **Sirene:** Die zugehörige Sirene des Rauchmelders wird aktiviert +4. **Benachrichtigungen:** Jedes Gerät bekommt: + - Text-Notification mit STOPP-Button + - TTS-Loop (alle X Sekunden) +5. **STOPP:** User drückt Button → TTS stoppt für dieses Gerät +6. **Rauch weg:** Alle Sirenen werden ausgeschaltet + +**Die Config wird als JSON-Datei gespeichert (`/config/www/smoke_config.json`) und bleibt auch nach Neustarts erhalten.** + +--- + +## Troubleshooting + +| Problem | Lösung | +|---------|--------| +| Config nicht geladen | Öffne Settings-Seite und klicke "Speichern" | +| Keine Rauchmelder im Dropdown | Klicke "Entities laden" - prüfe ob Sensoren device_class "smoke" haben | +| Benachrichtigung kommt nicht | Prüfe Gerätename (muss mit Mobile App übereinstimmen) | +| TTS stoppt nicht | Prüfe ob STOPP-Event ankommt (Debug-Panel) | +| Settings-Seite zeigt Fehler | Prüfe Token in der URL | +| Sirene geht nicht an | Prüfe ob die richtige Switch-Entity zugewiesen ist | + +--- + +## Dateien + +``` +node-red/ +├── flows/ +│ └── smoke_detector.json # Node-RED Flow +└── settings.html # Einstellungsseite (→ nach /config/www/ kopieren) +``` + +--- + +## Lizenz + +MIT diff --git a/esphome/test_smoke_detector.yaml b/esphome/test_smoke_detector.yaml new file mode 100644 index 0000000..5075a0d --- /dev/null +++ b/esphome/test_smoke_detector.yaml @@ -0,0 +1,182 @@ +# ESP32 Test-Rauchmelder für Node-RED Smoke Detection Flow +# +# Funktionen: +# - Switch "Rauch erkannt setzen" -> steuert Binary Sensor +# - Switch "Sirene" -> steuert die onboard LED +# - Binary Sensor "Rauch erkannt" -> on/off basierend auf Switch +# - BOOT Button (GPIO0) -> Toggle für Rauch-Status +# +# Die meisten ESP32 Boards haben eine LED an GPIO2 +# Der BOOT Button ist typischerweise an GPIO0 + +esphome: + name: test-rauchmelder + friendly_name: Test Rauchmelder + +esp32: + board: esp32dev + framework: + type: arduino + +# WLAN-Konfiguration +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Fallback Access Point falls WLAN nicht erreichbar + ap: + ssid: "Test-Rauchmelder" + password: "12345678" + +captive_portal: + +# Logging aktivieren +logger: + +# Home Assistant API +api: + encryption: + key: !secret api_encryption_key + +# OTA Updates +ota: + - platform: esphome + password: !secret ota_password + +# Webserver für Debugging (optional) +web_server: + port: 80 + +# ============================================ +# GPIO für Onboard LED (meist GPIO2 bei ESP32) +# ============================================ +output: + - platform: gpio + pin: GPIO2 + id: onboard_led_output + +light: + - platform: binary + name: "Sirene LED" + id: sirene_led + output: onboard_led_output + # Wird intern gesteuert, nicht direkt exposed + +# ============================================ +# SWITCHES +# ============================================ + +# Globale Variable für Rauch-Status +globals: + - id: rauch_erkannt_status + type: bool + restore_value: no + initial_value: 'false' + +switch: + # Switch 1: Rauch erkannt setzen + - platform: template + name: "Rauch erkannt setzen" + id: switch_rauch_setzen + icon: "mdi:fire" + optimistic: true + restore_mode: ALWAYS_OFF + on_turn_on: + - globals.set: + id: rauch_erkannt_status + value: 'true' + - binary_sensor.template.publish: + id: binary_rauch_erkannt + state: ON + - logger.log: "Rauch erkannt wurde aktiviert!" + on_turn_off: + - globals.set: + id: rauch_erkannt_status + value: 'false' + - binary_sensor.template.publish: + id: binary_rauch_erkannt + state: OFF + - logger.log: "Rauch erkannt wurde deaktiviert!" + + # Switch 2: Sirene (steuert die Onboard LED) + - platform: template + name: "Sirene" + id: switch_sirene + icon: "mdi:alarm-light" + optimistic: true + restore_mode: ALWAYS_OFF + on_turn_on: + - light.turn_on: sirene_led + - logger.log: "Sirene AN - LED leuchtet!" + on_turn_off: + - light.turn_off: sirene_led + - logger.log: "Sirene AUS - LED aus!" + +# ============================================ +# BINARY SENSOR +# ============================================ + +binary_sensor: + # BOOT Button (GPIO0) als Toggle für Rauch-Status + - platform: gpio + pin: + number: GPIO0 + mode: INPUT_PULLUP + inverted: true + name: "Boot Button" + id: boot_button + internal: true # Nicht in HA anzeigen + filters: + - delayed_on: 10ms # Entprellen + on_press: + then: + - switch.toggle: switch_rauch_setzen + - logger.log: "Boot Button gedrückt - Toggle Rauch-Status!" + + # Binary Sensor: Rauch erkannt (gesteuert durch Switch) + - platform: template + name: "Rauch erkannt" + id: binary_rauch_erkannt + device_class: smoke + icon: "mdi:smoke-detector" + lambda: |- + return id(rauch_erkannt_status); + + # Status-Sensor für Verbindung + - platform: status + name: "Status" + +# ============================================ +# ZUSÄTZLICHE SENSOREN (optional) +# ============================================ + +sensor: + # WLAN Signalstärke + - platform: wifi_signal + name: "WLAN Signal" + update_interval: 60s + + # Uptime + - platform: uptime + name: "Laufzeit" + update_interval: 60s + +# ============================================ +# TEXT SENSOR +# ============================================ + +text_sensor: + # IP Adresse + - platform: wifi_info + ip_address: + name: "IP Adresse" + ssid: + name: "Verbundenes WLAN" + +# ============================================ +# BUTTON für Neustart +# ============================================ + +button: + - platform: restart + name: "Neustart" diff --git a/node-red/flows/smoke_detector.json b/node-red/flows/smoke_detector.json new file mode 100644 index 0000000..3b4d57d --- /dev/null +++ b/node-red/flows/smoke_detector.json @@ -0,0 +1,784 @@ +[ + { + "id": "smoke_flow_tab", + "type": "tab", + "label": "Rauchmelder System", + "disabled": false, + "info": "Rauchmelder-Benachrichtigungen V7\n\n✅ Config wird als JSON-Datei gespeichert\n✅ Persistiert über Neustarts" + }, + { + "id": "comment_header", + "type": "comment", + "z": "smoke_flow_tab", + "name": "🔥 Rauchmelder V7 - Config in /config/www/smoke_config.json", + "info": "Config wird als JSON-Datei gespeichert.\nSettings-Seite lädt und speichert Config aus/in der Datei.", + "x": 300, + "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 & laden", + "func": "// =====================================================\n// PARSE UND LADE CONFIG AUS JSON DATEI - V7\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 // Datei existiert nicht - warte auf erste Config\n node.warn('Config-Datei nicht gefunden. Warte auf erste Speicherung über Settings-Seite.');\n node.status({fill:'yellow', shape:'ring', text:'⚠️ Keine Config - Bitte in Settings speichern'});\n \n // Leere Default-Config setzen\n config = {\n devices: [],\n detectors: [],\n notification_text: '🔥 RAUCHMELDER ALARM! {detector} hat Rauch erkannt!',\n tts_interval: 10,\n tts_language: 'de'\n };\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:'❌ Config JSON ungültig!'});\n return null;\n}\n\n// Config validieren und aufbereiten\nconfig = {\n devices: config.devices || [],\n detectors: config.detectors || [],\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};\n\n// Config global speichern\nglobal.set('smoke_config', config);\n\n// TTS-Status für alle Geräte initialisieren\nconfig.devices.forEach(device => {\n const key = `tts_active_${device.device_id}`;\n if (flow.get(key) === undefined) {\n flow.set(key, false);\n }\n});\n\n// Status anzeigen\nconst statusText = `✅ ${config.detectors.length} RM, ${config.devices.length} Geräte`;\nnode.status({fill:'green', shape:'dot', text: statusText});\n\nif (config.devices.length === 0 || config.detectors.length === 0) {\n node.warn('Config unvollständig! Bitte in Settings-Seite konfigurieren.');\n node.status({fill:'yellow', shape:'ring', text:'⚠️ Config unvollständig'});\n}\n\nnode.log('Config geladen: ' + statusText);\n\nmsg.config = config;\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 650, + "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": "// Bereite Config zum Speichern vor\nconst eventData = msg.payload;\n\n// Debug: Was kommt an?\nnode.warn('Event empfangen: ' + JSON.stringify(eventData).substring(0, 200));\n\n// Das Event kann verschiedene Formate haben\nlet config = null;\n\nif (eventData && eventData.config) {\n // Format: { config: {...} }\n config = eventData.config;\n} else if (eventData && eventData.event && eventData.event.config) {\n // Format: { event: { config: {...} } }\n config = eventData.event.config;\n} else if (eventData && eventData.detectors) {\n // Format: Config direkt im payload\n config = eventData;\n}\n\nif (!config) {\n node.warn('Keine Config im Event gefunden! Struktur: ' + Object.keys(eventData || {}).join(', '));\n node.status({fill:'red', shape:'ring', text:'❌ Keine Config!'});\n return null;\n}\n\n// Config aufbereiten\nconst saveConfig = {\n devices: config.devices || [],\n detectors: config.detectors || [],\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};\n\n// Als JSON formatieren (schön lesbar)\nmsg.payload = JSON.stringify(saveConfig, null, 2);\n\nnode.status({fill:'blue', shape:'dot', text:'Speichere ' + saveConfig.detectors.length + ' RM, ' + saveConfig.devices.length + ' Geräte...'});\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 in /config/www/smoke_config.json gespeichert');\n\n// Trigger Neuladen der Config\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": 870, + "y": 140, + "wires": [] + }, + { + "id": "comment_trigger", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ RAUCHMELDER TRIGGER ━━━━━━━━━━", + "info": "", + "x": 220, + "y": 380, + "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": 420, + "wires": [["func_filter_smoke"]] + }, + { + "id": "func_filter_smoke", + "type": "function", + "z": "smoke_flow_tab", + "name": "🎯 Filter: Ist konfigurierter Rauchmelder?", + "func": "const config = global.get('smoke_config');\n\nif (!config || !config.detectors || config.detectors.length === 0) {\n return null;\n}\n\n// Entity ID aus den Event-Daten holen\nconst entityId = msg.data.entity_id || (msg.data.new_state ? msg.data.new_state.entity_id : null);\n\nif (!entityId) {\n node.warn('Keine entity_id gefunden!');\n return null;\n}\n\nconst detector = config.detectors.find(d => d.sensor === entityId);\nif (!detector) {\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;\n\nif (newState === 'on' && oldState !== 'on') {\n node.status({fill:'red', shape:'dot', text:'🔥 RAUCH: ' + entityId});\n msg.event = 'smoke_detected';\n msg.config = config;\n msg.detector = detector;\n return [msg, null];\n}\n\nif (newState === 'off' && oldState === 'on') {\n node.status({fill:'green', shape:'dot', text:'✅ Klar: ' + entityId});\n msg.event = 'smoke_cleared';\n msg.config = config;\n msg.detector = detector;\n return [null, msg];\n}\n\nreturn null;", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 460, + "y": 340, + "wires": [["func_process_alarm"], ["func_check_all_clear"]], + "outputLabels": ["Rauch erkannt", "Rauch weg"] + }, + { + "id": "func_process_alarm", + "type": "function", + "z": "smoke_flow_tab", + "name": "🚨 Verarbeite Alarm", + "func": "const config = msg.config;\nconst detector = msg.detector;\nconst triggered = msg.entity_id;\n\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 = config.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:'🔥 ' + detector_name});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 770, + "y": 320, + "wires": [["func_turn_on_alarms"]] + }, + { + "id": "func_turn_on_alarms", + "type": "function", + "z": "smoke_flow_tab", + "name": "📢 Sirene EIN", + "func": "const config = msg.config;\nconst detector = msg.detector;\n\nconst siren = detector.siren;\n\nif (!siren) {\n node.warn('Keine Sirene für ' + detector.sensor + ' konfiguriert');\n return [null, msg];\n}\n\nconst alarmMsg = {\n payload: {\n data: {\n entity_id: siren\n }\n }\n};\n\nnode.status({fill:'red', shape:'dot', text:'Sirene: ' + siren});\n\nreturn [[alarmMsg], msg];", + "outputs": 2, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 990, + "y": 300, + "wires": [["ha_switch_on"], ["func_notify_all_devices"]], + "outputLabels": ["Alarme", "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": 1180, + "y": 280, + "wires": [[]] + }, + { + "id": "comment_notifications", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ BENACHRICHTIGUNGEN ━━━━━━━━━━", + "info": "", + "x": 220, + "y": 480, + "wires": [] + }, + { + "id": "func_notify_all_devices", + "type": "function", + "z": "smoke_flow_tab", + "name": "📱 Für jedes Gerät", + "func": "const config = msg.config;\nconst devices = config.devices || [];\n\nif (devices.length === 0) {\n node.warn('Keine Geräte konfiguriert!');\n return null;\n}\n\nconst messages = [];\n\nfor (let device of devices) {\n const ttsKey = `tts_active_${device.device_id}`;\n const isActive = flow.get(ttsKey);\n \n if (isActive) {\n node.log('TTS bereits aktiv für: ' + device.device_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 bereits aktiv'});\n return null;\n}\n\nnode.status({fill:'blue', shape:'dot', text: messages.length + ' Geräte benachrichtigen'});\n\nreturn [messages];", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 170, + "y": 540, + "wires": [["func_mark_active"]] + }, + { + "id": "func_mark_active", + "type": "function", + "z": "smoke_flow_tab", + "name": "✅ Markiere als aktiv", + "func": "const device = msg.device;\nconst ttsKey = `tts_active_${device.device_id}`;\n\nflow.set(ttsKey, true);\n\nnode.status({fill:'green', shape:'dot', text: '✅ ' + device.device_name});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 540, + "wires": [["func_send_text_notification"]] + }, + { + "id": "func_send_text_notification", + "type": "function", + "z": "smoke_flow_tab", + "name": "📝 Text-Notification erstellen", + "func": "const device = msg.device;\nconst message = msg.message;\nconst triggered = msg.triggered_detector;\n\n// Device ID direkt verwenden für den Service-Namen\nconst serviceName = 'mobile_app_' + device.device_id;\n\nconst tag = 'smoke_alarm_' + triggered.replace(/\\./g, '_');\n// Action ID enthält unsere device_id für späteres Mapping\nconst actionId = 'STOP_SMOKE_' + device.device_id;\n\nmsg.notify_service = serviceName;\nmsg.notify_message = message;\nmsg.notify_title = '🚨 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: serviceName});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 680, + "y": 540, + "wires": [["ha_notify", "func_start_tts_loop"]] + }, + { + "id": "ha_notify", + "type": "api-call-service", + "z": "smoke_flow_tab", + "name": "📱 Benachrichtigung senden", + "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": 200, + "y": 600, + "wires": [[]] + }, + { + "id": "comment_tts", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ TTS LOOP ━━━━━━━━━━", + "info": "", + "x": 190, + "y": 660, + "wires": [] + }, + { + "id": "func_start_tts_loop", + "type": "function", + "z": "smoke_flow_tab", + "name": "🔊 TTS Loop starten", + "func": "const device = msg.device;\nconst config = msg.config;\n\nmsg.tts_device = device;\nmsg.tts_config = config;\nmsg.loop_count = 0;\n\nnode.status({fill:'blue', shape:'dot', text:'TTS Start: ' + device.device_name});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 170, + "y": 720, + "wires": [["func_check_tts_active"]] + }, + { + "id": "func_check_tts_active", + "type": "function", + "z": "smoke_flow_tab", + "name": "🔄 TTS noch aktiv?", + "func": "const device = msg.tts_device;\nconst ttsKey = `tts_active_${device.device_id}`;\nconst isActive = flow.get(ttsKey);\n\nif (!isActive) {\n node.status({fill:'green', shape:'dot', text:'TTS gestoppt: ' + device.device_name});\n return null;\n}\n\nmsg.loop_count = (msg.loop_count || 0) + 1;\nnode.status({fill:'blue', shape:'ring', text:'TTS #' + msg.loop_count + ': ' + device.device_name});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 410, + "y": 720, + "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": 650, + "y": 720, + "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;\n\n// iOS Critical Alert mit Alarm-Sound\nconst serviceName = 'mobile_app_' + device.device_id;\n\nmsg.notify_service = serviceName;\nmsg.tts_message = msg.message;\n\nnode.status({fill:'red', shape:'dot', text: 'Critical: ' + device.device_name});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 190, + "y": 800, + "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\": \"🚨 RAUCHMELDER ALARM\", \"data\": {\"push\": {\"sound\": {\"name\": \"default\", \"critical\": 1, \"volume\": 1.0}}, \"tag\": \"smoke_critical\"}}", + "dataType": "json", + "mergeContext": "", + "mustacheAltTags": false, + "outputProperties": [], + "queue": "none", + "x": 440, + "y": 800, + "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": 800, + "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": 800, + "wires": [[]] + }, + { + "id": "func_tts_android", + "type": "function", + "z": "smoke_flow_tab", + "name": "🤖 Android TTS", + "func": "const device = msg.tts_device;\n\n// Device ID direkt verwenden (ist bereits im richtigen Format)\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: serviceName});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 180, + "y": 860, + "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": 860, + "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": 830, + "wires": [["func_check_tts_active"]] + }, + { + "id": "comment_stop", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ STOPP BUTTON ━━━━━━━━━━", + "info": "", + "x": 200, + "y": 940, + "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": 1000, + "wires": [["func_check_stop_action"]] + }, + { + "id": "func_check_stop_action", + "type": "function", + "z": "smoke_flow_tab", + "name": "🔍 STOP_SMOKE prüfen", + "func": "const action = msg.payload.event.action;\n\nif (!action || !action.startsWith('STOP_SMOKE_')) {\n return null;\n}\n\n// Device ID aus der Action extrahieren (STOP_SMOKE_one_plus_7_pro_stefan -> one_plus_7_pro_stefan)\nconst ourDeviceId = action.replace('STOP_SMOKE_', '');\n\nif (!ourDeviceId) {\n node.warn('Keine Device ID in Action gefunden: ' + action);\n return null;\n}\n\nmsg.stop_device_id = ourDeviceId;\n\nnode.status({fill:'yellow', shape:'dot', text:'STOPP für: ' + ourDeviceId});\nnode.log('STOPP Action: ' + action + ' -> Device ID: ' + ourDeviceId);\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 1000, + "wires": [["func_stop_tts"]] + }, + { + "id": "func_stop_tts", + "type": "function", + "z": "smoke_flow_tab", + "name": "⏹️ TTS stoppen", + "func": "const deviceId = msg.stop_device_id;\nconst config = global.get('smoke_config');\n\nconst device = config.devices.find(d => d.device_id === deviceId);\n\nif (!device) {\n node.warn('Gerät nicht gefunden: ' + deviceId);\n return null;\n}\n\nconst ttsKey = `tts_active_${deviceId}`;\nflow.set(ttsKey, false);\n\nnode.status({fill:'green', shape:'dot', text:'TTS gestoppt: ' + device.device_name});\n\nmsg.device = device;\nmsg.config = config;\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 680, + "y": 1000, + "wires": [["func_clear_notifications"]] + }, + { + "id": "func_clear_notifications", + "type": "function", + "z": "smoke_flow_tab", + "name": "🗑️ Notifications löschen", + "func": "const device = msg.device;\nconst config = msg.config;\n\n// Device ID direkt verwenden für den Service-Namen\nconst serviceName = 'mobile_app_' + device.device_id;\n\nconst messages = [];\n\nfor (let det of config.detectors) {\n const tag = 'smoke_alarm_' + 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: messages.length + ' Notifications gelöscht'});\n\nreturn [messages];", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 190, + "y": 1060, + "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": 440, + "y": 1060, + "wires": [[]] + }, + { + "id": "comment_clear", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ RAUCH WEG ━━━━━━━━━━", + "info": "", + "x": 190, + "y": 1120, + "wires": [] + }, + { + "id": "func_check_all_clear", + "type": "function", + "z": "smoke_flow_tab", + "name": "🔍 Alle Rauchmelder klar?", + "func": "const config = msg.config;\nconst cleared = msg.entity_id;\nconst ha = global.get('homeassistant.homeAssistant.states');\n\nfor (let det of config.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:'Noch aktiv: ' + det.sensor});\n return null;\n }\n }\n}\n\nnode.status({fill:'green', shape:'dot', text:'✅ Alle Rauchmelder klar'});\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 780, + "y": 360, + "wires": [["func_turn_off_alarms"]] + }, + { + "id": "func_turn_off_alarms", + "type": "function", + "z": "smoke_flow_tab", + "name": "🔇 Alle Sirenen AUS", + "func": "const config = msg.config;\n\nconst sirens = config.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: uniqueSirens.length + ' Sirenen deaktiviert'});\n\nreturn [messages];", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 190, + "y": 1180, + "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": 400, + "y": 1180, + "wires": [[]] + }, + { + "id": "comment_emergency", + "type": "comment", + "z": "smoke_flow_tab", + "name": "━━━━━━━━━━ NOTFALL-STOPP ━━━━━━━━━━", + "info": "", + "x": 200, + "y": 1240, + "wires": [] + }, + { + "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": 1300, + "wires": [["func_stop_all_tts"]] + }, + { + "id": "func_stop_all_tts", + "type": "function", + "z": "smoke_flow_tab", + "name": "⏹️ ALLE TTS stoppen", + "func": "const config = global.get('smoke_config');\n\nif (!config || !config.devices) {\n node.warn('Keine Config vorhanden!');\n return null;\n}\n\nlet stoppedCount = 0;\nfor (let device of config.devices) {\n const ttsKey = `tts_active_${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: `✅ ${stoppedCount} TTS gestoppt`});\nnode.log('NOTFALL: Alle TTS-Loops gestoppt (' + stoppedCount + ' Geräte)');\n\nreturn msg;", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 420, + "y": 1300, + "wires": [[]] + } +] diff --git a/node-red/settings.html b/node-red/settings.html new file mode 100644 index 0000000..7d61c74 --- /dev/null +++ b/node-red/settings.html @@ -0,0 +1,1358 @@ + + + + + + Rauchmelder - Einstellungen + + + + + +
+

🔥 Rauchmelder Einstellungen

+

Konfiguriere dein Rauchmelder-Benachrichtigungssystem

+ +
+ + +
+
📝 Benachrichtigungen
+ +
+ + +
+ Platzhalter: {detector} wird durch den Rauchmelder-Namen ersetzt +
+
+ +
+ +
+ +
10s
+
+
Wie oft die Sprachausgabe wiederholt wird
+
+ +
+ + +
+
+ + +
+
+ 🔥 Rauchmelder + +
+ +
+ + +
+ + +
+
📱 Benachrichtigungs-Geräte
+ +
+ + +
+ + +
+
🚨 Notfall-Aktionen
+
+ + +
+
+ + +
+ +
+
+ + + +