467 lines
15 KiB
HTML
467 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ room_name }} - 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: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
font-size: 28px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
|
|
|
|
.room-info {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
|
|
|
|
.devices-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
@media (min-width: 600px) {
|
|
.devices-grid {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 900px) {
|
|
.devices-grid {
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
}
|
|
|
|
.device-card {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
min-height: 110px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
width: 100%;
|
|
}
|
|
|
|
.device-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.device-card:active {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.device-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.device-icon {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.device-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
flex: 1;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.device-state {
|
|
margin-top: auto;
|
|
}
|
|
|
|
.state-primary {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.state-secondary {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.state-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.state-badge.on {
|
|
background: #34c759;
|
|
color: white;
|
|
}
|
|
|
|
.state-badge.off {
|
|
background: #e0e0e0;
|
|
color: #666;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.no-devices {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
color: #666;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<a href="/rooms" class="back-button">← Zurück zu Räumen</a>
|
|
<h1 id="room-name">Raum wird geladen...</h1>
|
|
<div class="room-info" id="room-info"></div>
|
|
</div>
|
|
|
|
<div id="error-container"></div>
|
|
<div id="loading" class="loading">Lade Geräte...</div>
|
|
<div id="devices-grid" class="devices-grid" style="display: none;"></div>
|
|
<div id="no-devices" class="no-devices" style="display: none;">
|
|
Keine Geräte in diesem Raum gefunden.
|
|
</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 room name from URL
|
|
const pathParts = window.location.pathname.split('/');
|
|
const roomName = decodeURIComponent(pathParts[pathParts.length - 1]);
|
|
|
|
// Device type to icon mapping
|
|
const deviceIcons = {
|
|
'light': '💡',
|
|
'thermostat': '🌡️',
|
|
'contact': '🚪',
|
|
'temp_humidity_sensor': '🌡️',
|
|
'relay': '💡',
|
|
'outlet': '💡',
|
|
'cover': '🪟'
|
|
};
|
|
|
|
// Device states
|
|
const deviceStates = {};
|
|
|
|
async function loadRoom() {
|
|
const loading = document.getElementById('loading');
|
|
const grid = document.getElementById('devices-grid');
|
|
const noDevices = document.getElementById('no-devices');
|
|
const errorContainer = document.getElementById('error-container');
|
|
const roomNameEl = document.getElementById('room-name');
|
|
const roomInfoEl = document.getElementById('room-info');
|
|
|
|
try {
|
|
// Load layout and devices using API client
|
|
// NEW: Use device layout endpoint for each device in this room
|
|
const layoutData = await window.apiClient.getLayout();
|
|
const devicesData = await window.apiClient.getDevices();
|
|
// Example: For each device in room.devices, you could fetch layout info via
|
|
// await window.apiClient.fetch(window.apiClient.api(`/devices/${device_id}/layout`));
|
|
|
|
console.log('Room name from URL:', roomName);
|
|
console.log('Available rooms:', layoutData.rooms.map(r => r.name));
|
|
console.log('Total devices:', devicesData.length);
|
|
|
|
// Find the room using API client helper
|
|
const room = window.apiClient.findRoom(layoutData, roomName);
|
|
if (!room) {
|
|
console.error('Room not found:', roomName);
|
|
throw new Error(`Raum "${roomName}" nicht gefunden`);
|
|
}
|
|
|
|
console.log('Found room:', room);
|
|
console.log('Room devices:', room.devices);
|
|
|
|
// Update header
|
|
roomNameEl.textContent = room.name;
|
|
roomInfoEl.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
|
|
|
|
// Create device lookup
|
|
const deviceMap = {};
|
|
devicesData.forEach(device => {
|
|
deviceMap[device.device_id] = device;
|
|
});
|
|
|
|
// Extract device IDs from room devices (they are objects now)
|
|
const deviceIds = room.devices.map(d => d.device_id);
|
|
console.log('Device IDs from room:', deviceIds);
|
|
|
|
// Filter devices for this room
|
|
const roomDevices = deviceIds
|
|
.map(deviceId => deviceMap[deviceId])
|
|
.filter(device => device != null);
|
|
|
|
console.log('Filtered room devices:', roomDevices);
|
|
|
|
loading.style.display = 'none';
|
|
|
|
if (roomDevices.length === 0) {
|
|
noDevices.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Lade nur die States für die Geräte im aktuellen Raum
|
|
for (const device of roomDevices) {
|
|
try {
|
|
deviceStates[device.device_id] = await window.apiClient.getDeviceState(device.device_id);
|
|
} catch (err) {
|
|
deviceStates[device.device_id] = null;
|
|
}
|
|
}
|
|
console.log('Device states:', deviceStates);
|
|
|
|
// Render devices
|
|
grid.style.display = 'grid';
|
|
roomDevices.forEach(device => {
|
|
const card = createDeviceCard(device);
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
// Start SSE for live updates
|
|
connectRealtime();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading room:', error);
|
|
loading.style.display = 'none';
|
|
errorContainer.innerHTML = `
|
|
<div class="error">
|
|
⚠️ Fehler beim Laden: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function createDeviceCard(device) {
|
|
const card = document.createElement('a');
|
|
card.className = 'device-card';
|
|
card.href = `/device/${device.device_id}`;
|
|
card.dataset.deviceId = device.device_id;
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'device-header';
|
|
|
|
const icon = document.createElement('div');
|
|
icon.className = 'device-icon';
|
|
icon.textContent = deviceIcons[device.type] || '📱';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'device-title';
|
|
title.textContent = device.name;
|
|
|
|
header.appendChild(icon);
|
|
header.appendChild(title);
|
|
|
|
const stateDiv = document.createElement('div');
|
|
stateDiv.className = 'device-state';
|
|
updateDeviceCardState(stateDiv, device);
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(stateDiv);
|
|
|
|
return card;
|
|
}
|
|
|
|
function updateDeviceCardState(stateDiv, device) {
|
|
const state = deviceStates[device.device_id];
|
|
if (!state) {
|
|
stateDiv.innerHTML = '<div class="state-secondary">Status unbekannt</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
switch (device.type) {
|
|
case 'light':
|
|
if (state.power) {
|
|
const powerState = state.power === 'on';
|
|
html = `<span class="state-badge ${powerState ? 'on' : 'off'}">${powerState ? 'An' : 'Aus'}</span>`;
|
|
if (powerState && state.brightness != null) {
|
|
html += `<div class="state-secondary">${state.brightness}% Helligkeit</div>`;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'thermostat':
|
|
if (state.target != null) {
|
|
html = `<div class="state-primary">${state.target.toFixed(1)}°C</div>`;
|
|
if (state.current != null) {
|
|
html += `<div class="state-secondary">Ist: ${state.current}°C</div>`;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'contact':
|
|
if (state.contact != null) {
|
|
const isOpen = state.contact === 'open';
|
|
html = `<span class="state-badge ${isOpen ? 'open' : 'closed'}">${isOpen ? 'Offen' : 'Geschlossen'}</span>`;
|
|
}
|
|
break;
|
|
|
|
case 'temp_humidity_sensor':
|
|
if (state.temperature != null) {
|
|
html = `<div class="state-primary">${state.temperature.toFixed(1)}°C</div>`;
|
|
if (state.humidity != null) {
|
|
html += `<div class="state-secondary">${state.humidity}% Luftfeuchte</div>`;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'relay':
|
|
case 'outlet':
|
|
if (state.power) {
|
|
const powerState = state.power === 'on';
|
|
html = `<span class="state-badge ${powerState ? 'on' : 'off'}">${powerState ? 'An' : 'Aus'}</span>`;
|
|
}
|
|
break;
|
|
|
|
case 'cover':
|
|
if (state.position != null) {
|
|
html = `<div class="state-primary">${state.position}%</div>`;
|
|
html += `<div class="state-secondary">Position</div>`;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
html = '<div class="state-secondary">Kein Status</div>';
|
|
}
|
|
|
|
stateDiv.innerHTML = html;
|
|
}
|
|
|
|
function connectRealtime() {
|
|
try {
|
|
// Use API client's connectRealtime method
|
|
window.apiClient.connectRealtime((event) => {
|
|
// Update device state
|
|
if (event.device_id && event.state) {
|
|
deviceStates[event.device_id] = event.state;
|
|
|
|
// Update card if visible
|
|
const card = document.querySelector(`[data-device-id="${event.device_id}"]`);
|
|
if (card) {
|
|
const stateDiv = card.querySelector('.device-state');
|
|
window.apiClient.getDevices().then(devices => {
|
|
const device = window.apiClient.findDevice(devices, event.device_id);
|
|
if (device) {
|
|
updateDeviceCardState(stateDiv, device);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}, (error) => {
|
|
console.error('SSE connection error:', error);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Failed to connect to realtime events:', error);
|
|
}
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
window.apiClient.disconnectRealtime();
|
|
});
|
|
|
|
// Load room on page load
|
|
document.addEventListener('DOMContentLoaded', loadRoom);
|
|
</script>
|
|
</body>
|
|
</html>
|