Files
home-automation/apps/ui/templates/garage.html
2025-11-27 22:26:39 +01:00

615 lines
20 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;
padding-top: 20px;
}
.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 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>