1012 lines
36 KiB
HTML
1012 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Gerät - Home Automation</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.back-button {
|
|
background: none;
|
|
border: none;
|
|
color: #667eea;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
padding: 8px 0;
|
|
margin-bottom: 12px;
|
|
display: inline-block;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.back-button:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.device-icon {
|
|
font-size: 48px;
|
|
text-align: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
font-size: 24px;
|
|
margin-bottom: 8px;
|
|
text-align: center;
|
|
}
|
|
|
|
.device-meta {
|
|
color: #666;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 16px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.control-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.control-group:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.control-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.toggle-button {
|
|
width: 100%;
|
|
padding: 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.toggle-button.on {
|
|
background: #34c759;
|
|
color: white;
|
|
}
|
|
|
|
.toggle-button.off {
|
|
background: #e0e0e0;
|
|
color: #666;
|
|
}
|
|
|
|
.toggle-button:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.slider-container {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.slider {
|
|
width: 100%;
|
|
height: 40px;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background: #e0e0e0;
|
|
outline: none;
|
|
border-radius: 20px;
|
|
padding: 0;
|
|
}
|
|
|
|
.slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 32px;
|
|
height: 32px;
|
|
background: #667eea;
|
|
cursor: pointer;
|
|
border-radius: 50%;
|
|
border: 3px solid white;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.slider::-moz-range-thumb {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: #667eea;
|
|
cursor: pointer;
|
|
border-radius: 50%;
|
|
border: 3px solid white;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.slider-value {
|
|
text-align: center;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.button-group {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.control-button {
|
|
padding: 12px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: #667eea;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.control-button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.control-button:disabled {
|
|
background: #e0e0e0;
|
|
color: #999;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.state-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.state-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.state-value {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.state-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.phase-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 20px;
|
|
}
|
|
|
|
.phase-section h4 {
|
|
color: #333;
|
|
margin-bottom: 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phase-values {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.phase-value {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
background: rgba(102, 126, 234, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.phase-value .value {
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
}
|
|
|
|
.phase-value .unit {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.phase-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.state-badge {
|
|
display: inline-block;
|
|
padding: 8px 20px;
|
|
border-radius: 20px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
}
|
|
|
|
.state-badge.open {
|
|
background: #ff9500;
|
|
color: white;
|
|
}
|
|
|
|
.state-badge.closed {
|
|
background: #34c759;
|
|
color: white;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
color: white;
|
|
font-size: 18px;
|
|
padding: 40px;
|
|
}
|
|
|
|
.error {
|
|
background: rgba(255, 59, 48, 0.9);
|
|
color: white;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.success {
|
|
background: rgba(52, 199, 89, 0.9);
|
|
color: white;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
margin-top: 12px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.color-picker {
|
|
width: 100%;
|
|
height: 60px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<a href="#" id="back-button" class="back-button">← Zurück</a>
|
|
<div class="device-icon" id="device-icon">📱</div>
|
|
<h1 id="device-name">Gerät wird geladen...</h1>
|
|
<div class="device-meta" id="device-room"></div>
|
|
<div class="device-meta" id="device-type"></div>
|
|
</div>
|
|
|
|
<div id="error-container"></div>
|
|
<div id="loading" class="loading">Lade Gerät...</div>
|
|
<div id="controls-container" style="display: none;"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// API configuration from backend
|
|
window.API_BASE = '{{ api_base }}';
|
|
</script>
|
|
|
|
<!-- Load API client AFTER API_BASE is set -->
|
|
<script src="/static/types.js"></script>
|
|
<script src="/static/api-client.js"></script>
|
|
|
|
<script>
|
|
// Get device ID from URL
|
|
const pathParts = window.location.pathname.split('/');
|
|
const deviceId = decodeURIComponent(pathParts[pathParts.length - 1]);
|
|
console.log('Device ID from URL:', deviceId);
|
|
|
|
// Device data
|
|
let deviceData = null;
|
|
let deviceState = {};
|
|
let roomName = '';
|
|
|
|
// Device type icons
|
|
const deviceIcons = {
|
|
'light': '💡',
|
|
'thermostat': '🌡️',
|
|
'contact': '🚪',
|
|
'temp_humidity_sensor': '🌡️',
|
|
'relay': '🔌',
|
|
'outlet': '🔌',
|
|
'cover': '🪟'
|
|
};
|
|
|
|
async function loadDevice() {
|
|
const loading = document.getElementById('loading');
|
|
const controlsContainer = document.getElementById('controls-container');
|
|
const errorContainer = document.getElementById('error-container');
|
|
|
|
try {
|
|
// Load device info using API client
|
|
// NEW: Use new endpoints for device info and layout
|
|
deviceData = await window.apiClient.getDevice(deviceId);
|
|
console.log("Loaded device data:", deviceData);
|
|
deviceState = await window.apiClient.getDeviceState(deviceId);
|
|
console.log("Loaded device state:", deviceState);
|
|
const layoutInfo = await window.apiClient.getDeviceLayout(deviceId);
|
|
console.log("Loaded layout info:", layoutInfo);
|
|
roomName = layoutInfo.room;
|
|
|
|
// Update header
|
|
document.getElementById('device-icon').textContent = deviceIcons[deviceData.type] || '📱';
|
|
document.getElementById('device-name').textContent = deviceData.name;
|
|
document.getElementById('device-room').textContent = roomName || 'Kein Raum';
|
|
document.getElementById('device-type').textContent = getTypeLabel(deviceData.type);
|
|
|
|
// Set back button
|
|
document.getElementById('back-button').href = roomName ? `/room/${encodeURIComponent(roomName)}` : '/rooms';
|
|
|
|
// Render controls
|
|
loading.style.display = 'none';
|
|
controlsContainer.style.display = 'block';
|
|
renderControls(controlsContainer);
|
|
|
|
// Start SSE
|
|
connectRealtime();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading device:', error);
|
|
loading.style.display = 'none';
|
|
errorContainer.innerHTML = `
|
|
<div class="error">
|
|
⚠️ Fehler beim Laden: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function getTypeLabel(type) {
|
|
const labels = {
|
|
'light': 'Licht',
|
|
'thermostat': 'Thermostat',
|
|
'contact': 'Kontaktsensor',
|
|
'temp_humidity_sensor': 'Temperatur & Luftfeuchte',
|
|
'three_phase_powermeter': 'Dreiphasen-Stromzähler',
|
|
'relay': 'Schalter',
|
|
'outlet': 'Steckdose',
|
|
'cover': 'Jalousie'
|
|
};
|
|
return labels[type] || type;
|
|
}
|
|
|
|
function renderControls(container) {
|
|
container.innerHTML = '';
|
|
|
|
switch (deviceData.type) {
|
|
case 'light':
|
|
renderLightControls(container);
|
|
break;
|
|
case 'thermostat':
|
|
renderThermostatControls(container);
|
|
break;
|
|
case 'relay':
|
|
case 'outlet':
|
|
renderOutletControls(container);
|
|
break;
|
|
case 'contact':
|
|
renderContactDisplay(container);
|
|
break;
|
|
case 'temp_humidity_sensor':
|
|
renderTempHumidityDisplay(container);
|
|
break;
|
|
case 'three_phase_powermeter':
|
|
renderThreePhasePowerDisplay(container);
|
|
break;
|
|
case 'cover':
|
|
renderCoverControls(container);
|
|
break;
|
|
default:
|
|
container.innerHTML = '<div class="card">Keine Steuerung verfügbar</div>';
|
|
}
|
|
}
|
|
|
|
function renderLightControls(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Steuerung</div>';
|
|
|
|
// Power toggle
|
|
const powerGroup = document.createElement('div');
|
|
powerGroup.className = 'control-group';
|
|
const powerButton = document.createElement('button');
|
|
powerButton.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
|
|
powerButton.textContent = deviceState.power === 'on' ? '💡 An' : '💡 Aus';
|
|
powerButton.onclick = () => togglePower();
|
|
powerGroup.appendChild(powerButton);
|
|
card.appendChild(powerGroup);
|
|
|
|
// Brightness slider (if supported)
|
|
if (deviceData.features?.brightness) {
|
|
const brightnessGroup = document.createElement('div');
|
|
brightnessGroup.className = 'control-group';
|
|
brightnessGroup.innerHTML = `
|
|
<label class="control-label">Helligkeit</label>
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" id="brightness-slider" min="0" max="100" value="${deviceState.brightness || 0}">
|
|
<div class="slider-value" id="brightness-value">${deviceState.brightness || 0}%</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(brightnessGroup);
|
|
|
|
setTimeout(() => {
|
|
const slider = document.getElementById('brightness-slider');
|
|
const valueDisplay = document.getElementById('brightness-value');
|
|
slider.oninput = (e) => {
|
|
valueDisplay.textContent = e.target.value + '%';
|
|
};
|
|
slider.onchange = (e) => {
|
|
setBrightness(parseInt(e.target.value));
|
|
};
|
|
}, 0);
|
|
}
|
|
|
|
// Color picker (if supported)
|
|
if (deviceData.features?.color_hsb) {
|
|
const colorGroup = document.createElement('div');
|
|
colorGroup.className = 'control-group';
|
|
colorGroup.innerHTML = `
|
|
<label class="control-label">Farbe</label>
|
|
<input type="color" class="color-picker" id="color-picker" value="#ffffff">
|
|
`;
|
|
card.appendChild(colorGroup);
|
|
|
|
setTimeout(() => {
|
|
const picker = document.getElementById('color-picker');
|
|
picker.onchange = (e) => {
|
|
setColor(e.target.value);
|
|
};
|
|
}, 0);
|
|
}
|
|
|
|
container.appendChild(card);
|
|
}
|
|
|
|
function renderThermostatControls(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
|
|
// Current state display
|
|
const stateGrid = document.createElement('div');
|
|
stateGrid.className = 'state-grid';
|
|
stateGrid.innerHTML = `
|
|
<div class="state-item">
|
|
<div class="state-value" id="current-temp">${deviceState.current?.toFixed(1) || '--'}°C</div>
|
|
<div class="state-label">Aktuell</div>
|
|
</div>
|
|
<div class="state-item">
|
|
<div class="state-value" id="target-temp">${deviceState.target?.toFixed(1) || '--'}°C</div>
|
|
<div class="state-label">Ziel</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(stateGrid);
|
|
|
|
// Target temperature slider
|
|
const sliderGroup = document.createElement('div');
|
|
sliderGroup.className = 'control-group';
|
|
sliderGroup.style.marginTop = '20px';
|
|
sliderGroup.innerHTML = `
|
|
<label class="control-label">Zieltemperatur</label>
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" id="temp-slider" min="5" max="30" step="0.5" value="${deviceState.target || 21}">
|
|
<div class="slider-value" id="temp-value">${deviceState.target?.toFixed(1) || '21.0'}°C</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(sliderGroup);
|
|
|
|
container.appendChild(card);
|
|
|
|
setTimeout(() => {
|
|
const slider = document.getElementById('temp-slider');
|
|
const valueDisplay = document.getElementById('temp-value');
|
|
slider.oninput = (e) => {
|
|
valueDisplay.textContent = parseFloat(e.target.value).toFixed(1) + '°C';
|
|
};
|
|
slider.onchange = (e) => {
|
|
setTargetTemp(parseFloat(e.target.value));
|
|
};
|
|
}, 0);
|
|
}
|
|
|
|
function renderOutletControls(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Steuerung</div>';
|
|
|
|
const powerGroup = document.createElement('div');
|
|
powerGroup.className = 'control-group';
|
|
const powerButton = document.createElement('button');
|
|
powerButton.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
|
|
powerButton.textContent = deviceState.power === 'on' ? '🔌 An' : '🔌 Aus';
|
|
powerButton.onclick = () => togglePower();
|
|
powerGroup.appendChild(powerButton);
|
|
card.appendChild(powerGroup);
|
|
|
|
container.appendChild(card);
|
|
}
|
|
|
|
function renderContactDisplay(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Status</div>';
|
|
|
|
const statusDiv = document.createElement('div');
|
|
statusDiv.style.textAlign = 'center';
|
|
const isOpen = deviceState.contact === 'open';
|
|
statusDiv.innerHTML = `
|
|
<div class="state-badge ${isOpen ? 'open' : 'closed'}" id="contact-status">
|
|
${isOpen ? 'Offen' : 'Geschlossen'}
|
|
</div>
|
|
`;
|
|
card.appendChild(statusDiv);
|
|
|
|
container.appendChild(card);
|
|
}
|
|
|
|
function renderTempHumidityDisplay(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Messwerte</div>';
|
|
|
|
const stateGrid = document.createElement('div');
|
|
stateGrid.className = 'state-grid';
|
|
stateGrid.innerHTML = `
|
|
<div class="state-item">
|
|
<div class="state-value" id="temperature">${deviceState.temperature?.toFixed(1) || '--'}°C</div>
|
|
<div class="state-label">Temperatur</div>
|
|
</div>
|
|
<div class="state-item">
|
|
<div class="state-value" id="humidity">${deviceState.humidity || '--'}%</div>
|
|
<div class="state-label">Luftfeuchte</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(stateGrid);
|
|
|
|
container.appendChild(card);
|
|
}
|
|
|
|
function renderThreePhasePowerDisplay(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Leistungsmessung</div>';
|
|
|
|
// Übersicht
|
|
const overviewGrid = document.createElement('div');
|
|
overviewGrid.className = 'state-grid';
|
|
overviewGrid.innerHTML = `
|
|
<div class="state-item">
|
|
<div class="state-value" id="total-power">${deviceState.total_power?.toFixed(0) || '--'} W</div>
|
|
<div class="state-label">Gesamtleistung</div>
|
|
</div>
|
|
<div class="state-item">
|
|
<div class="state-value" id="energy">${deviceState.energy?.toFixed(2) || '--'} kWh</div>
|
|
<div class="state-label">Energie</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(overviewGrid);
|
|
|
|
// Phasen Details
|
|
const phaseCard = document.createElement('div');
|
|
phaseCard.className = 'card';
|
|
phaseCard.innerHTML = '<div class="card-title">Phasen</div>';
|
|
phaseCard.style.marginTop = '20px';
|
|
|
|
const phaseGrid = document.createElement('div');
|
|
phaseGrid.className = 'phase-grid';
|
|
phaseGrid.innerHTML = `
|
|
<div class="phase-section">
|
|
<h4>Phase 1</h4>
|
|
<div class="phase-values">
|
|
<div class="phase-value">
|
|
<span class="value" id="phase1-power">${deviceState.phase1_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase1-voltage">${deviceState.phase1_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase1-current">${deviceState.phase1_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase-section">
|
|
<h4>Phase 2</h4>
|
|
<div class="phase-values">
|
|
<div class="phase-value">
|
|
<span class="value" id="phase2-power">${deviceState.phase2_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase2-voltage">${deviceState.phase2_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase2-current">${deviceState.phase2_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase-section">
|
|
<h4>Phase 3</h4>
|
|
<div class="phase-values">
|
|
<div class="phase-value">
|
|
<span class="value" id="phase3-power">${deviceState.phase3_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase3-voltage">${deviceState.phase3_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase3-current">${deviceState.phase3_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
phaseCard.appendChild(phaseGrid);
|
|
|
|
container.appendChild(card);
|
|
container.appendChild(phaseCard);
|
|
}
|
|
|
|
function renderCoverControls(container) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Position</div>';
|
|
|
|
// Position slider
|
|
const sliderGroup = document.createElement('div');
|
|
sliderGroup.className = 'control-group';
|
|
sliderGroup.innerHTML = `
|
|
<div class="slider-container">
|
|
<input type="range" class="slider" id="position-slider" min="0" max="100" value="${deviceState.position || 0}">
|
|
<div class="slider-value" id="position-value">${deviceState.position || 0}%</div>
|
|
</div>
|
|
`;
|
|
card.appendChild(sliderGroup);
|
|
|
|
// Control buttons
|
|
const buttonGroup = document.createElement('div');
|
|
buttonGroup.className = 'button-group';
|
|
buttonGroup.style.marginTop = '16px';
|
|
buttonGroup.innerHTML = `
|
|
<button class="control-button" onclick="setCoverPosition(0)">Auf</button>
|
|
<button class="control-button" onclick="stopCover()">Stop</button>
|
|
<button class="control-button" onclick="setCoverPosition(100)">Zu</button>
|
|
`;
|
|
card.appendChild(buttonGroup);
|
|
|
|
container.appendChild(card);
|
|
|
|
setTimeout(() => {
|
|
const slider = document.getElementById('position-slider');
|
|
const valueDisplay = document.getElementById('position-value');
|
|
slider.oninput = (e) => {
|
|
valueDisplay.textContent = e.target.value + '%';
|
|
};
|
|
slider.onchange = (e) => {
|
|
setCoverPosition(parseInt(e.target.value));
|
|
};
|
|
}, 0);
|
|
}
|
|
|
|
// Control functions
|
|
async function togglePower() {
|
|
const newState = deviceState.power === 'on' ? 'off' : 'on';
|
|
await sendCommand({
|
|
type: deviceData.type,
|
|
payload: { power: newState }
|
|
});
|
|
}
|
|
|
|
async function setBrightness(value) {
|
|
await sendCommand({
|
|
type: 'light',
|
|
payload: { brightness: value }
|
|
});
|
|
}
|
|
|
|
async function setColor(hexColor) {
|
|
// Convert hex to HSB
|
|
const hsb = hexToHSB(hexColor);
|
|
await sendCommand({
|
|
type: 'light',
|
|
payload: {
|
|
hue: Math.round(hsb.h),
|
|
sat: Math.round(hsb.s)
|
|
}
|
|
});
|
|
}
|
|
|
|
async function setTargetTemp(value) {
|
|
await sendCommand({
|
|
type: 'thermostat',
|
|
payload: { target: value }
|
|
});
|
|
}
|
|
|
|
async function setCoverPosition(value) {
|
|
await sendCommand({
|
|
type: 'cover',
|
|
payload: { position: value }
|
|
});
|
|
}
|
|
|
|
async function stopCover() {
|
|
await sendCommand({
|
|
type: 'cover',
|
|
payload: { action: 'stop' }
|
|
});
|
|
}
|
|
|
|
async function sendCommand(payload) {
|
|
try {
|
|
await window.apiClient.setDeviceState(deviceId, deviceData.type, payload.payload);
|
|
showSuccess('Befehl gesendet');
|
|
} catch (error) {
|
|
console.error('Error sending command:', error);
|
|
showError('Fehler beim Senden: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
const container = document.getElementById('controls-container');
|
|
const successDiv = document.createElement('div');
|
|
successDiv.className = 'success';
|
|
successDiv.textContent = '✓ ' + message;
|
|
container.appendChild(successDiv);
|
|
setTimeout(() => successDiv.remove(), 3000);
|
|
}
|
|
|
|
function showError(message) {
|
|
const container = document.getElementById('error-container');
|
|
container.innerHTML = `<div class="error">⚠️ ${message}</div>`;
|
|
setTimeout(() => container.innerHTML = '', 5000);
|
|
}
|
|
|
|
function hexToHSB(hex) {
|
|
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const diff = max - min;
|
|
|
|
let h = 0;
|
|
if (diff !== 0) {
|
|
if (max === r) h = ((g - b) / diff) % 6;
|
|
else if (max === g) h = (b - r) / diff + 2;
|
|
else h = (r - g) / diff + 4;
|
|
}
|
|
h = Math.round(h * 60);
|
|
if (h < 0) h += 360;
|
|
|
|
const s = max === 0 ? 0 : (diff / max) * 100;
|
|
|
|
return { h, s, b: max * 100 };
|
|
}
|
|
|
|
function connectRealtime() {
|
|
try {
|
|
// Use API client's realtime connection
|
|
window.apiClient.connectRealtime((event) => {
|
|
console.log('SSE event received:', event);
|
|
console.log('Current deviceId:', deviceId);
|
|
console.log('Event device_id:', event.device_id);
|
|
console.log('Device type:', deviceData.type);
|
|
if (event.device_id === deviceId && event.state) {
|
|
console.log('Updating device state for:', deviceId);
|
|
console.log('Old state:', deviceState);
|
|
console.log('New state from event:', event.state);
|
|
deviceState = { ...deviceState, ...event.state };
|
|
console.log('Merged state:', deviceState);
|
|
updateUI();
|
|
} else {
|
|
console.log('SSE event ignored - not for this device or no state');
|
|
}
|
|
}, (error) => {
|
|
console.error('SSE connection error:', error);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to connect to realtime events:', error);
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
// Update based on device type
|
|
switch (deviceData.type) {
|
|
case 'light':
|
|
updateLightUI();
|
|
break;
|
|
case 'thermostat':
|
|
updateThermostatUI();
|
|
break;
|
|
case 'relay':
|
|
case 'outlet':
|
|
updateOutletUI();
|
|
break;
|
|
case 'contact':
|
|
updateContactUI();
|
|
break;
|
|
case 'temp_humidity_sensor':
|
|
updateTempHumidityUI();
|
|
break;
|
|
case 'three_phase_powermeter':
|
|
updateThreePhasePowerUI();
|
|
break;
|
|
case 'cover':
|
|
updateCoverUI();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateLightUI() {
|
|
const button = document.querySelector('.toggle-button');
|
|
if (button) {
|
|
button.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
|
|
button.textContent = deviceState.power === 'on' ? '💡 An' : '💡 Aus';
|
|
}
|
|
|
|
const brightnessSlider = document.getElementById('brightness-slider');
|
|
const brightnessValue = document.getElementById('brightness-value');
|
|
if (brightnessSlider && deviceState.brightness != null) {
|
|
brightnessSlider.value = deviceState.brightness;
|
|
brightnessValue.textContent = deviceState.brightness + '%';
|
|
}
|
|
}
|
|
|
|
function updateThermostatUI() {
|
|
const currentTemp = document.getElementById('current-temp');
|
|
const targetTemp = document.getElementById('target-temp');
|
|
const tempSlider = document.getElementById('temp-slider');
|
|
const tempValue = document.getElementById('temp-value');
|
|
|
|
if (currentTemp && deviceState.current != null) {
|
|
currentTemp.textContent = deviceState.current.toFixed(1) + '°C';
|
|
}
|
|
if (targetTemp && deviceState.target != null) {
|
|
targetTemp.textContent = deviceState.target.toFixed(1) + '°C';
|
|
}
|
|
if (tempSlider && deviceState.target != null) {
|
|
tempSlider.value = deviceState.target;
|
|
tempValue.textContent = deviceState.target.toFixed(1) + '°C';
|
|
}
|
|
}
|
|
|
|
function updateOutletUI() {
|
|
const button = document.querySelector('.toggle-button');
|
|
if (button) {
|
|
button.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
|
|
button.textContent = deviceState.power === 'on' ? '🔌 An' : '🔌 Aus';
|
|
}
|
|
}
|
|
|
|
function updateContactUI() {
|
|
const status = document.getElementById('contact-status');
|
|
if (status && deviceState.contact) {
|
|
const isOpen = deviceState.contact === 'open';
|
|
status.className = 'state-badge ' + (isOpen ? 'open' : 'closed');
|
|
status.textContent = isOpen ? 'Offen' : 'Geschlossen';
|
|
}
|
|
}
|
|
|
|
function updateTempHumidityUI() {
|
|
const temperature = document.getElementById('temperature');
|
|
const humidity = document.getElementById('humidity');
|
|
|
|
if (temperature && deviceState.temperature != null) {
|
|
temperature.textContent = deviceState.temperature.toFixed(1) + '°C';
|
|
}
|
|
if (humidity && deviceState.humidity != null) {
|
|
humidity.textContent = deviceState.humidity + '%';
|
|
}
|
|
}
|
|
|
|
function updateThreePhasePowerUI() {
|
|
console.log('updateThreePhasePowerUI called with deviceState:', deviceState);
|
|
// Update overview
|
|
const totalPower = document.getElementById('total-power');
|
|
const energy = document.getElementById('energy');
|
|
|
|
console.log('Elements found - totalPower:', totalPower, 'energy:', energy);
|
|
|
|
if (totalPower && deviceState.total_power != null) {
|
|
console.log('Updating total power to:', deviceState.total_power);
|
|
totalPower.textContent = deviceState.total_power.toFixed(0) + ' W';
|
|
}
|
|
if (energy && deviceState.energy != null) {
|
|
console.log('Updating energy to:', deviceState.energy);
|
|
energy.textContent = deviceState.energy.toFixed(2) + ' kWh';
|
|
}
|
|
|
|
// Update phases
|
|
const phases = ['phase1', 'phase2', 'phase3'];
|
|
phases.forEach(phase => {
|
|
const power = document.getElementById(`${phase}-power`);
|
|
const voltage = document.getElementById(`${phase}-voltage`);
|
|
const current = document.getElementById(`${phase}-current`);
|
|
|
|
if (power && deviceState[`${phase}_power`] != null) {
|
|
power.textContent = deviceState[`${phase}_power`].toFixed(0);
|
|
}
|
|
if (voltage && deviceState[`${phase}_voltage`] != null) {
|
|
voltage.textContent = deviceState[`${phase}_voltage`].toFixed(1);
|
|
}
|
|
if (current && deviceState[`${phase}_current`] != null) {
|
|
current.textContent = deviceState[`${phase}_current`].toFixed(2);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCoverUI() {
|
|
const slider = document.getElementById('position-slider');
|
|
const value = document.getElementById('position-value');
|
|
|
|
if (slider && deviceState.position != null) {
|
|
slider.value = deviceState.position;
|
|
value.textContent = deviceState.position + '%';
|
|
}
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
window.apiClient.disconnectRealtime();
|
|
});
|
|
|
|
// Load device on page load
|
|
document.addEventListener('DOMContentLoaded', loadDevice);
|
|
</script>
|
|
</body>
|
|
</html>
|