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