650 lines
21 KiB
HTML
650 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Garage - 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: 800px;
|
|
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;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: #333;
|
|
margin: 0;
|
|
}
|
|
|
|
.devices-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.devices-container {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
.device-section {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.device-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.device-icon {
|
|
font-size: 32px;
|
|
}
|
|
|
|
.device-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.device-name {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.device-type {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin: 0;
|
|
}
|
|
|
|
.card {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.card:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.state-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.state-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.state-value {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.state-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.control-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.control-group:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.control-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.control-button {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.control-button.on {
|
|
background: #34c759;
|
|
color: white;
|
|
}
|
|
|
|
.control-button.off {
|
|
background: #e0e0e0;
|
|
color: #666;
|
|
}
|
|
|
|
.control-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.control-button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<a href="/rooms" class="back-button">← Zurück zu Räumen</a>
|
|
<h1 class="page-title">Garage</h1>
|
|
</div>
|
|
|
|
<div id="error-container"></div>
|
|
<div id="loading" class="loading">Lade Geräte...</div>
|
|
<div id="devices-container" class="devices-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>
|
|
// Device IDs for garage devices
|
|
const GARAGE_DEVICES = [
|
|
'power_relay_caroutlet',
|
|
'powermeter_caroutlet'
|
|
];
|
|
|
|
// Device states
|
|
const deviceStates = {};
|
|
let devicesData = {};
|
|
|
|
async function loadGarageDevices() {
|
|
const loading = document.getElementById('loading');
|
|
const container = document.getElementById('devices-container');
|
|
const errorContainer = document.getElementById('error-container');
|
|
|
|
try {
|
|
// Load all devices using API client
|
|
const allDevices = await window.apiClient.getDevices();
|
|
console.log('All devices loaded:', allDevices.length);
|
|
|
|
// Filter garage devices
|
|
const garageDevices = allDevices.filter(device =>
|
|
GARAGE_DEVICES.includes(device.device_id)
|
|
);
|
|
|
|
console.log('Garage devices found:', garageDevices);
|
|
|
|
if (garageDevices.length === 0) {
|
|
throw new Error('Keine Garage-Geräte gefunden');
|
|
}
|
|
|
|
// Create device lookup
|
|
garageDevices.forEach(device => {
|
|
devicesData[device.device_id] = device;
|
|
});
|
|
|
|
// Load device states
|
|
for (const device of garageDevices) {
|
|
try {
|
|
deviceStates[device.device_id] = await window.apiClient.getDeviceState(device.device_id);
|
|
console.log(`State for ${device.device_id}:`, deviceStates[device.device_id]);
|
|
} catch (err) {
|
|
console.warn(`Failed to load state for ${device.device_id}:`, err);
|
|
deviceStates[device.device_id] = null;
|
|
}
|
|
}
|
|
|
|
loading.style.display = 'none';
|
|
container.style.display = 'grid';
|
|
|
|
// Render devices
|
|
garageDevices.forEach(device => {
|
|
const deviceSection = createDeviceSection(device);
|
|
container.appendChild(deviceSection);
|
|
});
|
|
|
|
// Start SSE for live updates
|
|
connectRealtime();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading garage devices:', error);
|
|
loading.style.display = 'none';
|
|
errorContainer.innerHTML = `
|
|
<div class="error">
|
|
⚠️ Fehler beim Laden: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function createDeviceSection(device) {
|
|
const section = document.createElement('div');
|
|
section.className = 'device-section';
|
|
section.dataset.deviceId = device.device_id;
|
|
|
|
// Device header
|
|
const header = document.createElement('div');
|
|
header.className = 'device-header';
|
|
|
|
const icon = document.createElement('div');
|
|
icon.className = 'device-icon';
|
|
icon.textContent = getDeviceIcon(device.type);
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'device-info';
|
|
|
|
const name = document.createElement('h2');
|
|
name.className = 'device-name';
|
|
name.textContent = device.name;
|
|
|
|
const type = document.createElement('p');
|
|
type.className = 'device-type';
|
|
type.textContent = getTypeLabel(device.type);
|
|
|
|
info.appendChild(name);
|
|
info.appendChild(type);
|
|
|
|
header.appendChild(icon);
|
|
header.appendChild(info);
|
|
|
|
section.appendChild(header);
|
|
|
|
// Device content
|
|
const content = document.createElement('div');
|
|
content.className = 'device-content';
|
|
|
|
renderDeviceContent(content, device);
|
|
|
|
section.appendChild(content);
|
|
|
|
return section;
|
|
}
|
|
|
|
function renderDeviceContent(container, device) {
|
|
// Clear existing content
|
|
container.innerHTML = '';
|
|
|
|
switch (device.type) {
|
|
case 'relay':
|
|
case 'outlet':
|
|
renderOutletControls(container, device);
|
|
break;
|
|
case 'three_phase_powermeter':
|
|
renderThreePhasePowerDisplay(container, device);
|
|
break;
|
|
default:
|
|
container.innerHTML = '<div class="card">Keine Steuerung verfügbar</div>';
|
|
}
|
|
}
|
|
|
|
function renderOutletControls(container, device) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.innerHTML = '<div class="card-title">Steuerung</div>';
|
|
|
|
const controlGroup = document.createElement('div');
|
|
controlGroup.className = 'control-group';
|
|
|
|
const label = document.createElement('label');
|
|
label.className = 'control-label';
|
|
label.textContent = 'Status';
|
|
|
|
const buttonGroup = document.createElement('div');
|
|
buttonGroup.className = 'button-group';
|
|
|
|
const state = deviceStates[device.device_id];
|
|
const currentPower = state?.power === 'on';
|
|
|
|
const onButton = document.createElement('button');
|
|
onButton.className = `control-button ${currentPower ? 'on' : 'off'}`;
|
|
onButton.textContent = 'Ein';
|
|
onButton.onclick = () => toggleOutlet(device.device_id, 'on');
|
|
|
|
const offButton = document.createElement('button');
|
|
offButton.className = `control-button ${!currentPower ? 'on' : 'off'}`;
|
|
offButton.textContent = 'Aus';
|
|
offButton.onclick = () => toggleOutlet(device.device_id, 'off');
|
|
|
|
buttonGroup.appendChild(onButton);
|
|
buttonGroup.appendChild(offButton);
|
|
|
|
controlGroup.appendChild(label);
|
|
controlGroup.appendChild(buttonGroup);
|
|
|
|
card.appendChild(controlGroup);
|
|
container.appendChild(card);
|
|
}
|
|
|
|
function renderThreePhasePowerDisplay(container, device) {
|
|
const state = deviceStates[device.device_id] || {};
|
|
|
|
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-${device.device_id}">${state.total_power?.toFixed(0) || '--'} W</div>
|
|
<div class="state-label">Gesamtleistung</div>
|
|
</div>
|
|
<div class="state-item">
|
|
<div class="state-value" id="energy-${device.device_id}">${state.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-${device.device_id}">${state.phase1_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase1-voltage-${device.device_id}">${state.phase1_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase1-current-${device.device_id}">${state.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-${device.device_id}">${state.phase2_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase2-voltage-${device.device_id}">${state.phase2_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase2-current-${device.device_id}">${state.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-${device.device_id}">${state.phase3_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase3-voltage-${device.device_id}">${state.phase3_voltage?.toFixed(1) || '--'}</span>
|
|
<span class="unit">V</span>
|
|
</div>
|
|
<div class="phase-value">
|
|
<span class="value" id="phase3-current-${device.device_id}">${state.phase3_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
phaseCard.appendChild(phaseGrid);
|
|
|
|
container.appendChild(card);
|
|
container.appendChild(phaseCard);
|
|
}
|
|
|
|
async function toggleOutlet(deviceId, newState) {
|
|
try {
|
|
await window.apiClient.setDeviceState(deviceId, { power: newState });
|
|
console.log(`Set ${deviceId} to ${newState}`);
|
|
} catch (error) {
|
|
console.error('Error toggling outlet:', error);
|
|
alert('Fehler beim Schalten des Geräts: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function connectRealtime() {
|
|
try {
|
|
window.apiClient.connectRealtime((event) => {
|
|
console.log('SSE event received:', event);
|
|
if (event.device_id && event.state && GARAGE_DEVICES.includes(event.device_id)) {
|
|
console.log('Updating garage device state for:', event.device_id);
|
|
deviceStates[event.device_id] = { ...deviceStates[event.device_id], ...event.state };
|
|
updateDeviceUI(event.device_id);
|
|
}
|
|
}, (error) => {
|
|
console.error('SSE connection error:', error);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to connect to realtime events:', error);
|
|
}
|
|
}
|
|
|
|
function updateDeviceUI(deviceId) {
|
|
const device = devicesData[deviceId];
|
|
if (!device) return;
|
|
|
|
const state = deviceStates[deviceId];
|
|
console.log(`Updating UI for ${deviceId}:`, state);
|
|
|
|
switch (device.type) {
|
|
case 'relay':
|
|
case 'outlet':
|
|
updateOutletUI(deviceId, state);
|
|
break;
|
|
case 'three_phase_powermeter':
|
|
updateThreePhasePowerUI(deviceId, state);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateOutletUI(deviceId, state) {
|
|
const section = document.querySelector(`[data-device-id="${deviceId}"]`);
|
|
if (!section) return;
|
|
|
|
const onButton = section.querySelector('.control-button:nth-child(1)');
|
|
const offButton = section.querySelector('.control-button:nth-child(2)');
|
|
|
|
if (onButton && offButton && state.power) {
|
|
const isOn = state.power === 'on';
|
|
onButton.className = `control-button ${isOn ? 'on' : 'off'}`;
|
|
offButton.className = `control-button ${!isOn ? 'on' : 'off'}`;
|
|
}
|
|
}
|
|
|
|
function updateThreePhasePowerUI(deviceId, state) {
|
|
// Update overview
|
|
const totalPower = document.getElementById(`total-power-${deviceId}`);
|
|
const energy = document.getElementById(`energy-${deviceId}`);
|
|
|
|
if (totalPower && state.total_power != null) {
|
|
totalPower.textContent = state.total_power.toFixed(0) + ' W';
|
|
}
|
|
if (energy && state.energy != null) {
|
|
energy.textContent = state.energy.toFixed(2) + ' kWh';
|
|
}
|
|
|
|
// Update phases
|
|
const phases = ['phase1', 'phase2', 'phase3'];
|
|
phases.forEach(phase => {
|
|
const power = document.getElementById(`${phase}-power-${deviceId}`);
|
|
const voltage = document.getElementById(`${phase}-voltage-${deviceId}`);
|
|
const current = document.getElementById(`${phase}-current-${deviceId}`);
|
|
|
|
if (power && state[`${phase}_power`] != null) {
|
|
power.textContent = state[`${phase}_power`].toFixed(0);
|
|
}
|
|
if (voltage && state[`${phase}_voltage`] != null) {
|
|
voltage.textContent = state[`${phase}_voltage`].toFixed(1);
|
|
}
|
|
if (current && state[`${phase}_current`] != null) {
|
|
current.textContent = state[`${phase}_current`].toFixed(2);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getDeviceIcon(type) {
|
|
const icons = {
|
|
'relay': '⚡',
|
|
'outlet': '⚡',
|
|
'three_phase_powermeter': '📊'
|
|
};
|
|
return icons[type] || '📱';
|
|
}
|
|
|
|
function getTypeLabel(type) {
|
|
const labels = {
|
|
'relay': 'Relais',
|
|
'outlet': 'Steckdose',
|
|
'three_phase_powermeter': 'Dreiphasen-Stromzähler'
|
|
};
|
|
return labels[type] || 'Unbekannt';
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
window.apiClient.disconnectRealtime();
|
|
});
|
|
|
|
// Load garage devices on page load
|
|
document.addEventListener('DOMContentLoaded', loadGarageDevices);
|
|
</script>
|
|
</body>
|
|
</html> |