stop geräte benachrichtung card hinzugefügt

This commit is contained in:
duffyduck 2025-12-26 13:46:33 +01:00
parent 4c306181e4
commit b131f78b8b
3 changed files with 410 additions and 5 deletions

View File

@ -29,11 +29,11 @@
### 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:
```
http://homeassistant.local:8123/local/settings.html
http://homeassistant.local:8123/local/settings-smoke-notify.html
```
### 2. Node-RED Flow importieren
@ -75,7 +75,7 @@ Wenn alle Nodes konfiguriert sind: **Deploy**
**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!
@ -83,7 +83,7 @@ http://homeassistant.local:8123/local/settings.html?token=DEIN_TOKEN
Dashboard bearbeiten → Karte hinzufügen → Webpage:
```yaml
type: iframe
url: /local/settings.html?token=DEIN_TOKEN
url: /local/settings-smoke-notify.html?token=DEIN_TOKEN
aspect_ratio: 100%
```
@ -205,7 +205,8 @@ Für iOS-Geräte werden **Critical Alerts** verwendet. Diese sind laute Benachri
node-red/
├── flows/
│ └── 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)
```
---

View File

@ -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>