Compare commits

...

9 Commits

Author SHA1 Message Date
08294ca294 started 4
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 13:34:41 +01:00
e5eb368dca started 3 2025-12-09 13:00:47 +01:00
169d0505cb started 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:53:53 +01:00
02a2be92d5 started
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:34:05 +01:00
bcfc967460 Hottis PV Modbus sensor 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:01:47 +01:00
bd1f3bc8c9 Hottis PV Modbus sensor
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 11:57:49 +01:00
f9df70cf68 Hottis PV Modbus transformation
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 11:17:02 +01:00
5364b855aa add vendor hottis wago modbus 3
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 16:57:57 +01:00
3a1841a8a9 add vendor hottis wago modbus
Some checks failed
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/namespace Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/7 Pipeline failed
ci/woodpecker/tag/config unknown status
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/6 unknown status
ci/woodpecker/tag/ingress unknown status
2025-12-08 16:57:18 +01:00
6 changed files with 168 additions and 39 deletions

View File

@@ -18,6 +18,7 @@ from apps.abstraction.vendors import (
shelly, shelly,
tasmota, tasmota,
hottis_pv_modbus, hottis_pv_modbus,
hottis_wago_modbus,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,6 +41,7 @@ for vendor_name, vendor_module in [
("shelly", shelly), ("shelly", shelly),
("tasmota", tasmota), ("tasmota", tasmota),
("hottis_pv_modbus", hottis_pv_modbus), ("hottis_pv_modbus", hottis_pv_modbus),
("hottis_wago_modbus", hottis_wago_modbus),
]: ]:
for (device_type, direction), handler in vendor_module.HANDLERS.items(): for (device_type, direction), handler in vendor_module.HANDLERS.items():
key = (device_type, vendor_name, direction) key = (device_type, vendor_name, direction)

View File

@@ -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,
} }

View File

@@ -0,0 +1,58 @@
"""Hottis Wago Modbus vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Wago Modbus format.
Hottis Wago Modbus expects plain text 'true' or 'false' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Hottis Wago Modbus: 'true' or 'false'
"""
power = payload.get("power", "off")
# Map abstract "on"/"off" to vendor "true"/"false"
if isinstance(power, str):
power_lower = power.lower()
if power_lower in {"on", "true", "1"}:
return "true"
if power_lower in {"off", "false", "0"}:
return "false"
# Fallback: any truthy value -> "true", else "false"
return "true" if power else "false"
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Wago Modbus relay payload to abstract format.
Hottis Wago Modbus sends plain text 'true' or 'false'.
Example:
- Hottis Wago Modbus: 'true'
- Abstract: {'power': 'on'}
"""
value = payload.strip().lower()
if value == "true":
power = "on"
elif value == "false":
power = "off"
else:
# Fallback for unexpected values: keep as-is
logger.warning("Unexpected relay payload from Hottis Wago Modbus: %r", payload)
power = value
return {"power": power}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

View File

@@ -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}`);

View File

@@ -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

View File

@@ -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