Compare commits
7 Commits
0.8.3
...
car_outlet
| Author | SHA1 | Date | |
|---|---|---|---|
|
08294ca294
|
|||
|
e5eb368dca
|
|||
|
169d0505cb
|
|||
|
02a2be92d5
|
|||
|
bcfc967460
|
|||
|
bd1f3bc8c9
|
|||
|
f9df70cf68
|
43
apps/abstraction/vendors/hottis_pv_modbus.py
vendored
43
apps/abstraction/vendors/hottis_pv_modbus.py
vendored
@@ -23,17 +23,40 @@ def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
|
|||||||
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
|
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
|
||||||
"""Transform Hottis Modbus relay payload to abstract format.
|
"""Transform Hottis Modbus relay payload to abstract format.
|
||||||
|
|
||||||
Hottis Modbus sends JSON like:
|
Hottis Modbus sends plain text 'on' or 'off'.
|
||||||
{"status": "Ok", "timestamp": "...", "state": false, "cnt": 528}
|
Example:
|
||||||
|
- Hottis PV Modbus: 'on'
|
||||||
|
- Abstract: {'power': 'on'}
|
||||||
|
"""
|
||||||
|
return {"power": payload.strip()}
|
||||||
|
|
||||||
|
def transform_contact_sensor_to_vendor(payload: dict[str, Any]) -> str:
|
||||||
|
"""Transform abstract contact sensor payload to format.
|
||||||
|
|
||||||
|
Contact sensors are read-only.
|
||||||
|
"""
|
||||||
|
logger.warning("Contact sensors are read-only - SET commands should not be used")
|
||||||
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def transform_contact_sensor_to_abstract(payload: str) -> dict[str, Any]:
|
||||||
|
"""Transform contact sensor payload to abstract format.
|
||||||
|
|
||||||
|
MAX! sends "true"/"false" (string or bool) on STATE topic.
|
||||||
|
|
||||||
Transformations:
|
Transformations:
|
||||||
- state: true -> power: 'on'
|
- "true" or True -> "open" (window/door open)
|
||||||
- state: false -> power: 'off'
|
- "false" or False -> "closed" (window/door closed)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- contact sensor: "off"
|
||||||
|
- Abstract: {"contact": "open"}
|
||||||
"""
|
"""
|
||||||
data = json.loads(payload)
|
contact_value = payload.strip().lower() == "off"
|
||||||
state = data.get("state", False)
|
return {
|
||||||
power = "on" if bool(state) else "off"
|
"contact": "open" if contact_value else "closed"
|
||||||
return {"power": power}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def transform_three_phase_powermeter_to_vendor(payload: dict[str, Any]) -> str:
|
def transform_three_phase_powermeter_to_vendor(payload: dict[str, Any]) -> str:
|
||||||
@@ -104,4 +127,8 @@ HANDLERS = {
|
|||||||
("relay", "to_abstract"): transform_relay_to_abstract,
|
("relay", "to_abstract"): transform_relay_to_abstract,
|
||||||
("three_phase_powermeter", "to_vendor"): transform_three_phase_powermeter_to_vendor,
|
("three_phase_powermeter", "to_vendor"): transform_three_phase_powermeter_to_vendor,
|
||||||
("three_phase_powermeter", "to_abstract"): transform_three_phase_powermeter_to_abstract,
|
("three_phase_powermeter", "to_abstract"): transform_three_phase_powermeter_to_abstract,
|
||||||
|
("contact_sensor", "to_vendor"): transform_contact_sensor_to_vendor,
|
||||||
|
("contact_sensor", "to_abstract"): transform_contact_sensor_to_abstract,
|
||||||
|
("contact", "to_vendor"): transform_contact_sensor_to_vendor,
|
||||||
|
("contact", "to_abstract"): transform_contact_sensor_to_abstract,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,8 @@
|
|||||||
// Device IDs for garage devices
|
// Device IDs for garage devices
|
||||||
const GARAGE_DEVICES = [
|
const GARAGE_DEVICES = [
|
||||||
'power_relay_caroutlet',
|
'power_relay_caroutlet',
|
||||||
'powermeter_caroutlet'
|
'powermeter_caroutlet',
|
||||||
|
'sensor_caroutlet'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Device states
|
// Device states
|
||||||
@@ -410,7 +411,17 @@
|
|||||||
renderOutletControls(controlSection, device);
|
renderOutletControls(controlSection, device);
|
||||||
container.appendChild(controlSection);
|
container.appendChild(controlSection);
|
||||||
|
|
||||||
// 3. Powermeter section
|
// 3. Feedback section
|
||||||
|
const feedbackDevice = Object.values(devicesData).find(d => d.device_id === 'sensor_caroutlet');
|
||||||
|
if (feedbackDevice) {
|
||||||
|
const feedbackSection = document.createElement('div');
|
||||||
|
feedbackSection.className = 'device-section';
|
||||||
|
feedbackSection.dataset.deviceId = feedbackDevice.device_id;
|
||||||
|
renderFeedbackDisplay(feedbackSection, feedbackDevice);
|
||||||
|
container.appendChild(feedbackSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Powermeter section
|
||||||
const powermeterDevice = Object.values(devicesData).find(d => d.device_id === 'powermeter_caroutlet');
|
const powermeterDevice = Object.values(devicesData).find(d => d.device_id === 'powermeter_caroutlet');
|
||||||
if (powermeterDevice) {
|
if (powermeterDevice) {
|
||||||
const powermeterSection = document.createElement('div');
|
const powermeterSection = document.createElement('div');
|
||||||
@@ -424,7 +435,6 @@
|
|||||||
function renderOutletControls(container, device) {
|
function renderOutletControls(container, device) {
|
||||||
const controlGroup = document.createElement('div');
|
const controlGroup = document.createElement('div');
|
||||||
controlGroup.style.textAlign = 'center';
|
controlGroup.style.textAlign = 'center';
|
||||||
// controlGroup.style.marginBottom = '8px';
|
|
||||||
|
|
||||||
const state = deviceStates[device.device_id];
|
const state = deviceStates[device.device_id];
|
||||||
const currentPower = state?.power === 'on';
|
const currentPower = state?.power === 'on';
|
||||||
@@ -440,36 +450,36 @@
|
|||||||
label.className = 'toggle-label';
|
label.className = 'toggle-label';
|
||||||
label.textContent = currentPower ? 'Ein' : 'Aus';
|
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(toggleSwitch);
|
||||||
controlGroup.appendChild(label);
|
controlGroup.appendChild(label);
|
||||||
// controlGroup.appendChild(stateDisplay);
|
|
||||||
|
|
||||||
container.appendChild(controlGroup);
|
container.appendChild(controlGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFeedbackDisplay(container, device) {
|
||||||
|
const state = deviceStates[device.device_id] || {};
|
||||||
|
const controlGroup = document.createElement('div');
|
||||||
|
controlGroup.style.textAlign = 'center';
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'toggle-label';
|
||||||
|
|
||||||
|
console.log(`Rendering feedback for ${device.device_id}:`, state);
|
||||||
|
|
||||||
|
if (state.contact === 'closed') {
|
||||||
|
label.textContent = 'Schütz ✅ eingeschaltet';
|
||||||
|
} else {
|
||||||
|
label.textContent = 'Schütz 🅾️ ausgeschaltet';
|
||||||
|
}
|
||||||
|
|
||||||
|
controlGroup.appendChild(label);
|
||||||
|
container.appendChild(controlGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderThreePhasePowerDisplay(container, device) {
|
function renderThreePhasePowerDisplay(container, device) {
|
||||||
const state = deviceStates[device.device_id] || {};
|
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');
|
const overviewGrid = document.createElement('div');
|
||||||
overviewGrid.className = 'state-grid';
|
overviewGrid.className = 'state-grid';
|
||||||
overviewGrid.innerHTML = `
|
overviewGrid.innerHTML = `
|
||||||
@@ -484,16 +494,13 @@
|
|||||||
`;
|
`;
|
||||||
container.appendChild(overviewGrid);
|
container.appendChild(overviewGrid);
|
||||||
|
|
||||||
// Phasen Title
|
|
||||||
const phaseTitle = document.createElement('h4');
|
const phaseTitle = document.createElement('h4');
|
||||||
phaseTitle.style.margin = '20px 0 8px 0';
|
phaseTitle.style.margin = '20px 0 8px 0';
|
||||||
phaseTitle.style.fontSize = '16px';
|
phaseTitle.style.fontSize = '16px';
|
||||||
phaseTitle.style.fontWeight = '600';
|
phaseTitle.style.fontWeight = '600';
|
||||||
phaseTitle.style.color = '#333';
|
phaseTitle.style.color = '#333';
|
||||||
// phaseTitle.textContent = 'Phasen';
|
|
||||||
container.appendChild(phaseTitle);
|
container.appendChild(phaseTitle);
|
||||||
|
|
||||||
// Phasen Details
|
|
||||||
const phaseGrid = document.createElement('div');
|
const phaseGrid = document.createElement('div');
|
||||||
phaseGrid.className = 'phase-grid';
|
phaseGrid.className = 'phase-grid';
|
||||||
phaseGrid.innerHTML = `
|
phaseGrid.innerHTML = `
|
||||||
@@ -601,12 +608,14 @@
|
|||||||
const state = deviceStates[deviceId];
|
const state = deviceStates[deviceId];
|
||||||
console.log(`Updating UI for ${deviceId}:`, state);
|
console.log(`Updating UI for ${deviceId}:`, state);
|
||||||
|
|
||||||
switch (device.type) {
|
switch (deviceId) {
|
||||||
case 'relay':
|
case 'power_relay_caroutlet':
|
||||||
case 'outlet':
|
|
||||||
updateOutletUI(deviceId, state);
|
updateOutletUI(deviceId, state);
|
||||||
break;
|
break;
|
||||||
case 'three_phase_powermeter':
|
case 'sensor_caroutlet':
|
||||||
|
updateFeedbackDisplay(deviceId, state);
|
||||||
|
break;
|
||||||
|
case 'powermeter_caroutlet':
|
||||||
updateThreePhasePowerUI(deviceId, state);
|
updateThreePhasePowerUI(deviceId, state);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -637,6 +646,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFeedbackDisplay(deviceId, state) {
|
||||||
|
const section = document.querySelector(`[data-device-id="${deviceId}"]`);
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
const label = section.querySelector('.toggle-label');
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
const isOn = state.contact === 'closed';
|
||||||
|
label.textContent = isOn ? 'Schütz ✅ eingeschaltet' : 'Schütz 🅾️ ausgeschaltet';
|
||||||
|
|
||||||
|
// 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) {
|
function updateThreePhasePowerUI(deviceId, state) {
|
||||||
// Update overview
|
// Update overview
|
||||||
const totalPower = document.getElementById(`total-power-${deviceId}`);
|
const totalPower = document.getElementById(`total-power-${deviceId}`);
|
||||||
|
|||||||
@@ -860,7 +860,6 @@ devices:
|
|||||||
topics:
|
topics:
|
||||||
set: "IoT/Car/Control"
|
set: "IoT/Car/Control"
|
||||||
state: "IoT/Car/Control/State"
|
state: "IoT/Car/Control/State"
|
||||||
|
|
||||||
- device_id: powermeter_caroutlet
|
- device_id: powermeter_caroutlet
|
||||||
name: Car Outlet
|
name: Car Outlet
|
||||||
type: three_phase_powermeter
|
type: three_phase_powermeter
|
||||||
@@ -868,6 +867,13 @@ devices:
|
|||||||
technology: hottis_pv_modbus
|
technology: hottis_pv_modbus
|
||||||
topics:
|
topics:
|
||||||
state: "IoT/Car/Values"
|
state: "IoT/Car/Values"
|
||||||
|
- device_id: sensor_caroutlet
|
||||||
|
name: Car Outlet
|
||||||
|
type: contact
|
||||||
|
cap_version: contact_sensor@1.0.0
|
||||||
|
technology: hottis_pv_modbus
|
||||||
|
topics:
|
||||||
|
state: IoT/Car/Feedback/State
|
||||||
|
|
||||||
- device_id: schranklicht_flur_vor_kueche
|
- device_id: schranklicht_flur_vor_kueche
|
||||||
name: Schranklicht Flur vor Küche
|
name: Schranklicht Flur vor Küche
|
||||||
|
|||||||
@@ -317,8 +317,12 @@ rooms:
|
|||||||
title: Ladestrom
|
title: Ladestrom
|
||||||
icon: ⚡
|
icon: ⚡
|
||||||
rank: 310
|
rank: 310
|
||||||
|
- device_id: sensor_caroutlet
|
||||||
|
title: Schützzustand
|
||||||
|
icon: 🔌
|
||||||
|
rank: 315
|
||||||
- device_id: powermeter_caroutlet
|
- device_id: powermeter_caroutlet
|
||||||
title: Ladestrom
|
title: Messwerte
|
||||||
icon: 📊
|
icon: 📊
|
||||||
rank: 320
|
rank: 320
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user