stop geräte benachrichtung card hinzugefügt
This commit is contained in:
parent
4c306181e4
commit
b131f78b8b
11
README.md
11
README.md
|
|
@ -29,11 +29,11 @@
|
||||||
|
|
||||||
### 1. Settings-Seite in Home Assistant ablegen
|
### 1. Settings-Seite in Home Assistant ablegen
|
||||||
|
|
||||||
Kopiere `node-red/settings.html` nach `/config/www/settings.html`
|
Kopiere `node-red/settings-smoke-notify.html` nach `/config/www/settings-smoke-notify.html`
|
||||||
|
|
||||||
Die Datei ist dann erreichbar unter:
|
Die Datei ist dann erreichbar unter:
|
||||||
```
|
```
|
||||||
http://homeassistant.local:8123/local/settings.html
|
http://homeassistant.local:8123/local/settings-smoke-notify.html
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Node-RED Flow importieren
|
### 2. Node-RED Flow importieren
|
||||||
|
|
@ -75,7 +75,7 @@ Wenn alle Nodes konfiguriert sind: **Deploy**
|
||||||
|
|
||||||
**Option A: Direkt im Browser**
|
**Option A: Direkt im Browser**
|
||||||
```
|
```
|
||||||
http://homeassistant.local:8123/local/settings.html?token=DEIN_TOKEN
|
http://homeassistant.local:8123/local/settings-smoke-notify.html?token=DEIN_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option B: Als Dashboard-Karte (iframe)** - Empfohlen!
|
**Option B: Als Dashboard-Karte (iframe)** - Empfohlen!
|
||||||
|
|
@ -83,7 +83,7 @@ http://homeassistant.local:8123/local/settings.html?token=DEIN_TOKEN
|
||||||
Dashboard bearbeiten → Karte hinzufügen → Webpage:
|
Dashboard bearbeiten → Karte hinzufügen → Webpage:
|
||||||
```yaml
|
```yaml
|
||||||
type: iframe
|
type: iframe
|
||||||
url: /local/settings.html?token=DEIN_TOKEN
|
url: /local/settings-smoke-notify.html?token=DEIN_TOKEN
|
||||||
aspect_ratio: 100%
|
aspect_ratio: 100%
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -205,7 +205,8 @@ Für iOS-Geräte werden **Critical Alerts** verwendet. Diese sind laute Benachri
|
||||||
node-red/
|
node-red/
|
||||||
├── flows/
|
├── flows/
|
||||||
│ └── smoke_detector.json # Node-RED Flow
|
│ └── smoke_detector.json # Node-RED Flow
|
||||||
└── settings.html # Einstellungsseite (→ nach /config/www/ kopieren)
|
├── settings-smoke-notify.html # Einstellungsseite (→ nach /config/www/ kopieren)
|
||||||
|
└── stop-smoke-notify-card.html # STOPP-Karte für Dashboard (optional)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Rauchmelder STOPP</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(135deg, #2a1a1a 0%, #1a1a2e 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #5c3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 1px solid #3a3a5a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
padding-right: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(238, 90, 36, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:disabled {
|
||||||
|
background: #555;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #1b4332;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #5c1a1a;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.info {
|
||||||
|
background: #1a3a5c;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #3a3a5a;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-all-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: linear-gradient(135deg, #ffa502 0%, #ff6348 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-all-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(255, 165, 2, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🛑 Rauchmelder STOPP</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gerät auswählen</label>
|
||||||
|
<select id="deviceSelect">
|
||||||
|
<option value="">-- Lade Geräte... --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="stop-btn" id="stopBtn" onclick="stopDevice()" disabled>
|
||||||
|
🛑 Benachrichtigungen stoppen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<button class="stop-all-btn" onclick="stopAll()">
|
||||||
|
🔇 ALLE Benachrichtigungen stoppen
|
||||||
|
</button>
|
||||||
|
<div class="hint">Stoppt Benachrichtigungen für alle Geräte in allen Gruppen</div>
|
||||||
|
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let config = null;
|
||||||
|
|
||||||
|
// Token aus URL
|
||||||
|
function getToken() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return params.get('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config laden
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/local/smoke_config.json?_=' + Date.now());
|
||||||
|
if (response.ok) {
|
||||||
|
config = await response.json();
|
||||||
|
populateDeviceSelect();
|
||||||
|
} else {
|
||||||
|
showStatus('error', 'Config nicht gefunden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showStatus('error', 'Fehler: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown befüllen
|
||||||
|
function populateDeviceSelect() {
|
||||||
|
const select = document.getElementById('deviceSelect');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
|
||||||
|
if (!config || !config.groups) {
|
||||||
|
select.innerHTML = '<option value="">Keine Konfiguration</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = '<option value="">-- Gerät wählen --</option>';
|
||||||
|
|
||||||
|
config.groups.forEach(group => {
|
||||||
|
if (group.devices && group.devices.length > 0) {
|
||||||
|
group.devices.forEach(device => {
|
||||||
|
const value = JSON.stringify({
|
||||||
|
groupId: group.id,
|
||||||
|
groupName: group.name,
|
||||||
|
deviceId: device.device_id,
|
||||||
|
deviceName: device.device_name,
|
||||||
|
isIos: device.is_ios
|
||||||
|
});
|
||||||
|
const icon = device.is_ios ? '🍎' : '🤖';
|
||||||
|
const label = `${icon} ${device.device_name} (${group.name})`;
|
||||||
|
options += `<option value='${value}'>${label}</option>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
select.innerHTML = options;
|
||||||
|
select.onchange = () => {
|
||||||
|
stopBtn.disabled = !select.value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einzelnes Gerät stoppen
|
||||||
|
async function stopDevice() {
|
||||||
|
const select = document.getElementById('deviceSelect');
|
||||||
|
if (!select.value) return;
|
||||||
|
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
showStatus('error', 'Kein Token in URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = JSON.parse(select.value);
|
||||||
|
showStatus('info', `Stoppe ${device.deviceName}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Event an Node-RED senden
|
||||||
|
await fetch('/api/events/mobile_app_notification_action', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: `STOP_SMOKE_${device.groupId}_${device.deviceId}`
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notifications löschen
|
||||||
|
const serviceName = 'mobile_app_' + device.deviceId;
|
||||||
|
const group = config.groups.find(g => g.id === device.groupId);
|
||||||
|
|
||||||
|
if (group && group.detectors) {
|
||||||
|
for (let detector of group.detectors) {
|
||||||
|
const tag = 'smoke_alarm_' + device.groupId + '_' + detector.sensor.replace(/\./g, '_');
|
||||||
|
await fetch('/api/services/notify/' + serviceName, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'clear_notification',
|
||||||
|
data: { tag: tag }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical Alert löschen (iOS)
|
||||||
|
await fetch('/api/services/notify/' + serviceName, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'clear_notification',
|
||||||
|
data: { tag: 'smoke_critical' }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
showStatus('success', `✅ ${device.deviceName} gestoppt!`);
|
||||||
|
setTimeout(hideStatus, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showStatus('error', 'Fehler: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle stoppen
|
||||||
|
async function stopAll() {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
showStatus('error', 'Kein Token in URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('info', 'Stoppe alle Geräte...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Event an Node-RED senden
|
||||||
|
await fetch('/api/events/smoke_stop_all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'stop_all'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alle Notifications auf allen Geräten löschen
|
||||||
|
if (config && config.groups) {
|
||||||
|
for (let group of config.groups) {
|
||||||
|
for (let device of (group.devices || [])) {
|
||||||
|
const serviceName = 'mobile_app_' + device.device_id;
|
||||||
|
|
||||||
|
for (let detector of (group.detectors || [])) {
|
||||||
|
const tag = 'smoke_alarm_' + group.id + '_' + detector.sensor.replace(/\./g, '_');
|
||||||
|
try {
|
||||||
|
await fetch('/api/services/notify/' + serviceName, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'clear_notification',
|
||||||
|
data: { tag: tag }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical Alert löschen
|
||||||
|
try {
|
||||||
|
await fetch('/api/services/notify/' + serviceName, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'clear_notification',
|
||||||
|
data: { tag: 'smoke_critical' }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('success', '✅ Alle Benachrichtigungen gestoppt!');
|
||||||
|
setTimeout(hideStatus, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showStatus('error', 'Fehler: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status anzeigen
|
||||||
|
function showStatus(type, message) {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
status.className = 'status ' + type;
|
||||||
|
status.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatus() {
|
||||||
|
document.getElementById('status').className = 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
loadConfig();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue