Files
home-automation/apps/ui/templates/garage.html
2025-11-28 08:31:16 +01:00

639 lines
22 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;
}
.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;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 100px;
height: 50px;
background: #e0e0e0;
border-radius: 25px;
cursor: pointer;
transition: background 0.3s ease;
border: none;
outline: none;
}
.toggle-switch.on {
background: #34c759;
}
.toggle-switch::after {
content: '';
position: absolute;
top: 4px;
left: 4px;
width: 42px;
height: 42px;
background: white;
border-radius: 50%;
transition: transform 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.toggle-switch.on::after {
transform: translateX(50px);
}
.toggle-switch:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.toggle-switch:active::after {
width: 50px;
}
.toggle-label {
display: block;
text-align: center;
margin-top: 8px;
font-size: 14px;
font-weight: 500;
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;
}
}
.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;
}
#error-container:empty {
margin-bottom: 0;
}
</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 only the relay device (it will include the powermeter)
const relayDevice = garageDevices.find(d => d.device_id === 'power_relay_caroutlet');
if (relayDevice) {
const deviceSection = createDeviceSection(relayDevice);
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 fragment = document.createDocumentFragment();
// Create separate sections for each component
renderDeviceContent(fragment, device);
return fragment;
}
function renderDeviceContent(container, device) {
// Render all content as separate device sections for Car Outlet
if (device.device_id === 'power_relay_caroutlet') {
// 1. Header section
const headerSection = document.createElement('div');
headerSection.className = 'device-section';
headerSection.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; gap: 12px;">
<div style="font-size: 32px;">⚡</div>
<div style="font-size: 20px; font-weight: 600; color: #333;">Car Outlet</div>
</div>
`;
container.appendChild(headerSection);
// 2. Control section
const controlSection = document.createElement('div');
controlSection.className = 'device-section';
controlSection.dataset.deviceId = device.device_id;
renderOutletControls(controlSection, device);
container.appendChild(controlSection);
// 3. Powermeter section
const powermeterDevice = Object.values(devicesData).find(d => d.device_id === 'powermeter_caroutlet');
if (powermeterDevice) {
const powermeterSection = document.createElement('div');
powermeterSection.className = 'device-section';
renderThreePhasePowerDisplay(powermeterSection, powermeterDevice);
container.appendChild(powermeterSection);
}
}
}
function renderOutletControls(container, device) {
const controlGroup = document.createElement('div');
controlGroup.style.textAlign = 'center';
const state = deviceStates[device.device_id];
const currentPower = state?.power === 'on';
const toggleSwitch = document.createElement('button');
toggleSwitch.className = `toggle-switch ${currentPower ? 'on' : ''}`;
toggleSwitch.onclick = () => {
const currentState = deviceStates[device.device_id]?.power === 'on';
toggleOutlet(device.device_id, currentState ? 'off' : 'on');
};
const label = document.createElement('div');
label.className = 'toggle-label';
label.textContent = currentPower ? 'Ein' : 'Aus';
// Status display
const stateDisplay = document.createElement('div');
stateDisplay.style.marginTop = '16px';
stateDisplay.style.fontSize = '18px';
stateDisplay.style.fontWeight = '600';
stateDisplay.style.color = currentPower ? '#34c759' : '#666';
// stateDisplay.textContent = `Status: ${currentPower ? 'Eingeschaltet' : 'Ausgeschaltet'}`;
controlGroup.appendChild(toggleSwitch);
controlGroup.appendChild(label);
controlGroup.appendChild(stateDisplay);
container.appendChild(controlGroup);
}
function renderThreePhasePowerDisplay(container, device) {
const state = deviceStates[device.device_id] || {};
// Leistungsmessung Title
// const title = document.createElement('h3');
// title.style.margin = '0 0 20px 0';
// title.style.fontSize = '18px';
// title.style.fontWeight = '600';
// title.style.color = '#333';
// title.textContent = 'Leistungsmessung';
// container.appendChild(title);
// Ü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>
`;
container.appendChild(overviewGrid);
// Phasen Title
const phaseTitle = document.createElement('h4');
phaseTitle.style.margin = '20px 0 12px 0';
phaseTitle.style.fontSize = '16px';
phaseTitle.style.fontWeight = '600';
phaseTitle.style.color = '#333';
phaseTitle.textContent = 'Phasen';
container.appendChild(phaseTitle);
// Phasen Details
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>
`;
container.appendChild(phaseGrid);
}
async function toggleOutlet(deviceId, newState) {
try {
const device = devicesData[deviceId];
await sendCommand(deviceId, {
type: device.type,
payload: { 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);
}
}
async function sendCommand(deviceId, payload) {
const device = devicesData[deviceId];
await window.apiClient.setDeviceState(deviceId, device.type, payload.payload);
}
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 toggleSwitch = section.querySelector('.toggle-switch');
const label = section.querySelector('.toggle-label');
if (toggleSwitch && label && state.power) {
const isOn = state.power === 'on';
toggleSwitch.className = `toggle-switch ${isOn ? 'on' : ''}`;
label.textContent = isOn ? 'Ein' : 'Aus';
// Update state display in separate card
const cards = section.querySelectorAll('.card');
if (cards.length >= 3) { // Header, Control, State
const stateCard = cards[2];
stateCard.innerHTML = `
<div style="font-size: 18px; font-weight: 600; color: ${isOn ? '#34c759' : '#666'};">
Status: ${isOn ? 'Eingeschaltet' : 'Ausgeschaltet'}
</div>
`;
}
}
}
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>