Files
home-automation/apps/ui/templates/device.html

829 lines
28 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;
}
.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 = pathParts[pathParts.length - 1];
// 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',
'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 '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 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) => {
if (event.device_id === deviceId && event.state) {
deviceState = { ...deviceState, ...event.state };
updateUI();
}
}, (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 '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 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>