659 lines
23 KiB
HTML
659 lines
23 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-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.phase-value.full-width {
|
|
flex: 1;
|
|
}
|
|
|
|
.phase-value.half-width {
|
|
flex: 1;
|
|
}
|
|
|
|
.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';
|
|
// controlGroup.style.marginBottom = '8px';
|
|
|
|
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 = '30px 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 full-width">
|
|
<span class="value" id="phase1-power-${device.device_id}">${state.phase1_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-row">
|
|
<div class="phase-value half-width">
|
|
<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 half-width">
|
|
<span class="value" id="phase1-current-${device.device_id}">${state.phase1_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase-section">
|
|
<h4>Phase 2</h4>
|
|
<div class="phase-values">
|
|
<div class="phase-value full-width">
|
|
<span class="value" id="phase2-power-${device.device_id}">${state.phase2_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-row">
|
|
<div class="phase-value half-width">
|
|
<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 half-width">
|
|
<span class="value" id="phase2-current-${device.device_id}">${state.phase2_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase-section">
|
|
<h4>Phase 3</h4>
|
|
<div class="phase-values">
|
|
<div class="phase-value full-width">
|
|
<span class="value" id="phase3-power-${device.device_id}">${state.phase3_power?.toFixed(0) || '--'}</span>
|
|
<span class="unit">W</span>
|
|
</div>
|
|
<div class="phase-row">
|
|
<div class="phase-value half-width">
|
|
<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 half-width">
|
|
<span class="value" id="phase3-current-${device.device_id}">${state.phase3_current?.toFixed(2) || '--'}</span>
|
|
<span class="unit">A</span>
|
|
</div>
|
|
</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> |