39 Commits

Author SHA1 Message Date
fec97e54c1 garaga page 3 2025-11-27 22:26:39 +01:00
743e84560d garaga page 2 2025-11-27 22:24:29 +01:00
f25ab6a3a1 garaga page 2025-11-27 22:20:50 +01:00
b08a3f2564 hottis modbus relay 7 2025-11-27 19:17:59 +01:00
db43854156 hottis modbus relay 6 2025-11-27 17:34:08 +01:00
3d759bd3ff hottis modbus relay 5 2025-11-27 17:13:56 +01:00
7193c2be7f hottis modbus relay 4 2025-11-27 17:11:30 +01:00
02596f4796 hottis modbus relay 3 2025-11-27 17:07:28 +01:00
e316ec0f58 hottis modbus relay 2 2025-11-27 16:58:02 +01:00
18481d9970 hottis modbus relay 2025-11-27 16:48:19 +01:00
84fe6eea96 initial 2025-11-27 16:40:09 +01:00
84e401778e ci 2025-11-22 11:02:51 +01:00
4ee3c13d3e ci test 2 2025-11-21 16:26:46 +01:00
d685366c09 ci test
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
2025-11-21 16:13:42 +01:00
07b28e2f1f test 3
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
2025-11-21 15:47:17 +01:00
39bfb66098 test 2
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
2025-11-21 15:24:34 +01:00
75860cd1c2 test
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
2025-11-21 15:19:59 +01:00
bcbb58ea36 registry cache 4
Some checks failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
2025-11-21 13:57:43 +01:00
b38ed75261 registry cache 3
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
2025-11-21 13:55:11 +01:00
feb055b2ea registry cache 2
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
2025-11-21 13:53:31 +01:00
cce730b2fa registry cache
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
2025-11-21 13:44:23 +01:00
a26901037d namespace and config 15
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
2025-11-21 12:14:11 +01:00
4889f5ed8b namespace and config 14
Some checks failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
2025-11-21 12:12:19 +01:00
804e9bf742 namespace and config 13
Some checks failed
ci/woodpecker/tag/build/4 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/predeploy Pipeline failed
2025-11-21 12:11:10 +01:00
f60d5d03e9 namespace and config 12
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline failed
2025-11-21 12:06:56 +01:00
eff88e1d2f namespace and config 11 2025-11-21 12:06:28 +01:00
d027163087 namespace and config 10
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
2025-11-21 12:01:20 +01:00
4051ca22a4 namespace and config 9
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline failed
2025-11-21 12:00:35 +01:00
2608e935b8 namespace and config 8
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:55:50 +01:00
51f3b4f227 namespace and config 7
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline is pending
ci/woodpecker/push/woodpecker/5 Pipeline is pending
ci/woodpecker/tag/woodpecker/4 Pipeline is pending
ci/woodpecker/tag/woodpecker/5 Pipeline is pending
ci/woodpecker/tag/woodpecker/1 Pipeline is pending
ci/woodpecker/tag/woodpecker/2 Pipeline is pending
ci/woodpecker/tag/woodpecker/3 Pipeline is pending
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
2025-11-21 11:46:24 +01:00
006359687f namespace and config 6
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
2025-11-21 11:45:41 +01:00
f26d304890 namespace and config 5
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
2025-11-21 11:44:10 +01:00
6feec48ac6 namespace and config 4
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
2025-11-21 11:40:54 +01:00
ed6ed66a37 namespace and config 3
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:37:39 +01:00
09498dd0e5 namespace and config 2
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
2025-11-21 11:33:58 +01:00
41f5e06e30 namespace and config
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:23:49 +01:00
7769c6066a add ci script 5
All checks were successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
2025-11-21 11:10:45 +01:00
5f23e28cc0 add ci script 4
All checks were successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
2025-11-21 11:03:16 +01:00
cc083c1055 add ci script 3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-21 10:57:51 +01:00
14 changed files with 1124 additions and 9 deletions

View File

@@ -1,8 +1,18 @@
matrix:
APP:
- ui
- api
- abstraction
- rules
env:
NAMESPACE: "homea2"
steps: steps:
build_ui: build:
image: plugins/kaniko image: plugins/kaniko
settings: settings:
repo: ${FORGE_NAME}/${CI_REPO}-ui repo: ${FORGE_NAME}/${CI_REPO}/${APP}
registry: registry:
from_secret: container_registry from_secret: container_registry
auto_tag: true auto_tag: true
@@ -10,6 +20,30 @@ steps:
from_secret: container_registry_username from_secret: container_registry_username
password: password:
from_secret: container_registry_password from_secret: container_registry_password
dockerfile: apps/ui/Dockerfile dockerfile: apps/${APP}/Dockerfile
when: when:
- event: [push, tag] event: [push, tag]
ref:
exclude:
- refs/tags/*-configchange
create_namespace:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
commands:
- kubectl create namespace ${NAMESPACE} || echo "Namespace ${NAMESPACE} already exists"
when:
- event: [tag]
configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
commands:
when:
- event: [tag]

43
.woodpecker/build.yml Normal file
View File

@@ -0,0 +1,43 @@
matrix:
APP:
- ui
- api
- abstraction
- rules
steps:
show:
image: quay.io/wollud1969/networktools:latest
environment:
forge_name:
from_secret: forge_name
container_registry:
from_secret: container_registry
container_registry_username:
from_secret: container_registry_username
container_registry_password:
from_secret: container_registry_password
commands:
- echo $${forge_name} | base64
- echo $${container_registry} | base64
- echo $${container_registry_username} | base64
- echo $${container_registry_password} | base64
build:
image: plugins/kaniko
settings:
repo: ${FORGE_NAME}/${CI_REPO}/${APP}
auto_tag: true
dockerfile: apps/${APP}/Dockerfile
username:
from_secret: docker_hub_username
password:
from_secret: docker_hub_password
when:
event: [push, tag]
ref:
exclude:
- refs/tags/*-configchange

37
.woodpecker/predeploy.yml Normal file
View File

@@ -0,0 +1,37 @@
steps:
create_namespace:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
NAMESPACE: "homea2"
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl create namespace $NAMESPACE || echo "Namespace $NAMESPACE already exists"
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
NAMESPACE: "homea2"
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl create configmap home-automation-config
--from-file=devices=config/devices.yaml
--from-file=groups=config/groups.yaml
--from-file=layout=config/layout.yaml
--from-file=rules=config/rules.yaml
--from-file=scenes=config/scenes.yaml
--namespace=$NAMESPACE
--dry-run=client -o yaml | kubectl apply -f -
when:
event: [tag]

View File

@@ -15,7 +15,7 @@ import uuid
from aiomqtt import Client from aiomqtt import Client
from pydantic import ValidationError from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState, ThreePhasePowerState
from apps.abstraction.transformation import ( from apps.abstraction.transformation import (
transform_abstract_to_vendor, transform_abstract_to_vendor,
transform_vendor_to_abstract transform_vendor_to_abstract
@@ -231,6 +231,9 @@ async def handle_vendor_state(
elif device_type in {"temp_humidity", "temp_humidity_sensor"}: elif device_type in {"temp_humidity", "temp_humidity_sensor"}:
# Validate temperature & humidity sensor state # Validate temperature & humidity sensor state
TempHumidityState.model_validate(abstract_payload) TempHumidityState.model_validate(abstract_payload)
elif device_type == "three_phase_powermeter":
# Validate three-phase powermeter state
ThreePhasePowerState.model_validate(abstract_payload)
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}") logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return return

View File

@@ -374,6 +374,103 @@ def _transform_relay_shelly_to_abstract(payload: str) -> dict[str, Any]:
""" """
return {"power": payload.strip()} return {"power": payload.strip()}
# ============================================================================
# HANDLER FUNCTIONS: relay - hottis_modbus technology
# ============================================================================
def _transform_relay_hottis_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Modbus format.
Hottis Modbus expects plain text 'on' or 'off' (not JSON).
- power: 'on'/'off' -> 'on'/'off' (plain string)
Example:
- Abstract: {'power': 'on'}
- Hottis Modbus: 'on'
"""
power = payload.get("power", "off")
return power
def _transform_relay_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format.
Hottis Modbus sends plain text 'on' or 'off' (not JSON).
- 'on'/'off' -> power: 'on'/'off'
Example:
- Hottis Modbus: 'on'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip()}
# ============================================================================
# HANDLER FUNCTIONS: three_phase_powermeter - hottis_modbus technology
# ============================================================================
def _transform_three_phase_powermeter_hottis_modbus_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract three_phase_powermeter payload to hottis_modbus format.
energy: float = Field(..., description="Total energy in kWh")
total_power: float = Field(..., description="Total power in W")
phase1_power: float = Field(..., description="Power for phase 1 in W")
phase2_power: float = Field(..., description="Power for phase 2 in W")
phase3_power: float = Field(..., description="Power for phase 3 in W")
phase1_voltage: float = Field(..., description="Voltage for phase 1 in V")
phase2_voltage: float = Field(..., description="Voltage for phase 2 in V")
phase3_voltage: float = Field(..., description="Voltage for phase 3 in V")
phase1_current: float = Field(..., description="Current for phase 1 in A")
phase2_current: float = Field(..., description="Current for phase 2 in A")
phase3_current: float = Field(..., description="Current for phase 3 in A")
"""
vendor_payload = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
}
return vendor_payload
def _transform_three_phase_powermeter_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_modbus three_phase_powermeter payload to abstract format.
Transformations:
- Direct mapping of all power meter fields
Example:
- hottis_modbus: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
- Abstract: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
"""
payload = json.loads(payload)
abstract_payload = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
}
return abstract_payload
# ============================================================================ # ============================================================================
# HANDLER FUNCTIONS: max technology (Homegear MAX!) # HANDLER FUNCTIONS: max technology (Homegear MAX!)
@@ -482,6 +579,12 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract, ("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract,
("relay", "shelly", "to_vendor"): _transform_relay_shelly_to_vendor, ("relay", "shelly", "to_vendor"): _transform_relay_shelly_to_vendor,
("relay", "shelly", "to_abstract"): _transform_relay_shelly_to_abstract, ("relay", "shelly", "to_abstract"): _transform_relay_shelly_to_abstract,
("relay", "hottis_modbus", "to_vendor"): _transform_relay_hottis_modbus_to_vendor,
("relay", "hottis_modbus", "to_abstract"): _transform_relay_hottis_modbus_to_abstract,
# Three-Phase Powermeter transformations
("three_phase_powermeter", "hottis_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_modbus_to_vendor,
("three_phase_powermeter", "hottis_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_modbus_to_abstract,
} }

View File

@@ -129,6 +129,22 @@ async def device_detail(request: Request, device_id: str) -> HTMLResponse:
}) })
@app.get("/garage", response_class=HTMLResponse)
async def garage(request: Request) -> HTMLResponse:
"""Render the garage page with car outlet devices.
Args:
request: The FastAPI request object
Returns:
HTMLResponse: Rendered garage template
"""
return templates.TemplateResponse("garage.html", {
"request": request,
"api_base": API_BASE
})
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request) -> HTMLResponse: async def dashboard(request: Request) -> HTMLResponse:
"""Render the dashboard with rooms and devices. """Render the dashboard with rooms and devices.

View File

@@ -150,11 +150,15 @@ class HomeAutomationClient {
this.eventSource.close(); this.eventSource.close();
} }
this.eventSource = new EventSource(this.api('/realtime')); const realtimeUrl = this.api('/realtime');
console.log('Connecting to SSE endpoint:', realtimeUrl);
this.eventSource = new EventSource(realtimeUrl);
this.eventSource.onmessage = (event) => { this.eventSource.onmessage = (event) => {
console.log('Raw SSE event received:', event.data);
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('Parsed SSE data:', data);
// Normalize event format: convert API format to unified format // Normalize event format: convert API format to unified format
const normalizedEvent = { const normalizedEvent = {
@@ -163,6 +167,7 @@ class HomeAutomationClient {
state: data.payload || data.state // Support both formats state: data.payload || data.state // Support both formats
}; };
console.log('Normalized SSE event:', normalizedEvent);
onEvent(normalizedEvent); onEvent(normalizedEvent);
// Notify all registered listeners // Notify all registered listeners
@@ -172,12 +177,17 @@ class HomeAutomationClient {
} }
}); });
} catch (error) { } catch (error) {
console.error('Failed to parse SSE event:', error); console.error('Failed to parse SSE event:', error, 'Raw data:', event.data);
} }
}; };
this.eventSource.onopen = (event) => {
console.log('SSE connection opened:', event);
};
this.eventSource.onerror = (error) => { this.eventSource.onerror = (error) => {
console.error('SSE connection error:', error); console.error('SSE connection error:', error);
console.log('EventSource readyState:', this.eventSource.readyState);
if (onError) { if (onError) {
onError(error); onError(error);
} }

View File

@@ -217,6 +217,48 @@
color: #666; 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;
}
}
.state-badge { .state-badge {
display: inline-block; display: inline-block;
padding: 8px 20px; padding: 8px 20px;
@@ -298,7 +340,8 @@
<script> <script>
// Get device ID from URL // Get device ID from URL
const pathParts = window.location.pathname.split('/'); const pathParts = window.location.pathname.split('/');
const deviceId = pathParts[pathParts.length - 1]; const deviceId = decodeURIComponent(pathParts[pathParts.length - 1]);
console.log('Device ID from URL:', deviceId);
// Device data // Device data
let deviceData = null; let deviceData = null;
@@ -366,6 +409,7 @@
'thermostat': 'Thermostat', 'thermostat': 'Thermostat',
'contact': 'Kontaktsensor', 'contact': 'Kontaktsensor',
'temp_humidity_sensor': 'Temperatur & Luftfeuchte', 'temp_humidity_sensor': 'Temperatur & Luftfeuchte',
'three_phase_powermeter': 'Dreiphasen-Stromzähler',
'relay': 'Schalter', 'relay': 'Schalter',
'outlet': 'Steckdose', 'outlet': 'Steckdose',
'cover': 'Jalousie' 'cover': 'Jalousie'
@@ -393,6 +437,9 @@
case 'temp_humidity_sensor': case 'temp_humidity_sensor':
renderTempHumidityDisplay(container); renderTempHumidityDisplay(container);
break; break;
case 'three_phase_powermeter':
renderThreePhasePowerDisplay(container);
break;
case 'cover': case 'cover':
renderCoverControls(container); renderCoverControls(container);
break; break;
@@ -565,6 +612,93 @@
container.appendChild(card); container.appendChild(card);
} }
function renderThreePhasePowerDisplay(container) {
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">${deviceState.total_power?.toFixed(0) || '--'} W</div>
<div class="state-label">Gesamtleistung</div>
</div>
<div class="state-item">
<div class="state-value" id="energy">${deviceState.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">${deviceState.phase1_power?.toFixed(0) || '--'}</span>
<span class="unit">W</span>
</div>
<div class="phase-value">
<span class="value" id="phase1-voltage">${deviceState.phase1_voltage?.toFixed(1) || '--'}</span>
<span class="unit">V</span>
</div>
<div class="phase-value">
<span class="value" id="phase1-current">${deviceState.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">${deviceState.phase2_power?.toFixed(0) || '--'}</span>
<span class="unit">W</span>
</div>
<div class="phase-value">
<span class="value" id="phase2-voltage">${deviceState.phase2_voltage?.toFixed(1) || '--'}</span>
<span class="unit">V</span>
</div>
<div class="phase-value">
<span class="value" id="phase2-current">${deviceState.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">${deviceState.phase3_power?.toFixed(0) || '--'}</span>
<span class="unit">W</span>
</div>
<div class="phase-value">
<span class="value" id="phase3-voltage">${deviceState.phase3_voltage?.toFixed(1) || '--'}</span>
<span class="unit">V</span>
</div>
<div class="phase-value">
<span class="value" id="phase3-current">${deviceState.phase3_current?.toFixed(2) || '--'}</span>
<span class="unit">A</span>
</div>
</div>
</div>
`;
phaseCard.appendChild(phaseGrid);
container.appendChild(card);
container.appendChild(phaseCard);
}
function renderCoverControls(container) { function renderCoverControls(container) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; card.className = 'card';
@@ -707,9 +841,19 @@
try { try {
// Use API client's realtime connection // Use API client's realtime connection
window.apiClient.connectRealtime((event) => { window.apiClient.connectRealtime((event) => {
console.log('SSE event received:', event);
console.log('Current deviceId:', deviceId);
console.log('Event device_id:', event.device_id);
console.log('Device type:', deviceData.type);
if (event.device_id === deviceId && event.state) { if (event.device_id === deviceId && event.state) {
console.log('Updating device state for:', deviceId);
console.log('Old state:', deviceState);
console.log('New state from event:', event.state);
deviceState = { ...deviceState, ...event.state }; deviceState = { ...deviceState, ...event.state };
console.log('Merged state:', deviceState);
updateUI(); updateUI();
} else {
console.log('SSE event ignored - not for this device or no state');
} }
}, (error) => { }, (error) => {
console.error('SSE connection error:', error); console.error('SSE connection error:', error);
@@ -738,6 +882,9 @@
case 'temp_humidity_sensor': case 'temp_humidity_sensor':
updateTempHumidityUI(); updateTempHumidityUI();
break; break;
case 'three_phase_powermeter':
updateThreePhasePowerUI();
break;
case 'cover': case 'cover':
updateCoverUI(); updateCoverUI();
break; break;
@@ -806,6 +953,42 @@
} }
} }
function updateThreePhasePowerUI() {
console.log('updateThreePhasePowerUI called with deviceState:', deviceState);
// Update overview
const totalPower = document.getElementById('total-power');
const energy = document.getElementById('energy');
console.log('Elements found - totalPower:', totalPower, 'energy:', energy);
if (totalPower && deviceState.total_power != null) {
console.log('Updating total power to:', deviceState.total_power);
totalPower.textContent = deviceState.total_power.toFixed(0) + ' W';
}
if (energy && deviceState.energy != null) {
console.log('Updating energy to:', deviceState.energy);
energy.textContent = deviceState.energy.toFixed(2) + ' kWh';
}
// Update phases
const phases = ['phase1', 'phase2', 'phase3'];
phases.forEach(phase => {
const power = document.getElementById(`${phase}-power`);
const voltage = document.getElementById(`${phase}-voltage`);
const current = document.getElementById(`${phase}-current`);
if (power && deviceState[`${phase}_power`] != null) {
power.textContent = deviceState[`${phase}_power`].toFixed(0);
}
if (voltage && deviceState[`${phase}_voltage`] != null) {
voltage.textContent = deviceState[`${phase}_voltage`].toFixed(1);
}
if (current && deviceState[`${phase}_current`] != null) {
current.textContent = deviceState[`${phase}_current`].toFixed(2);
}
});
}
function updateCoverUI() { function updateCoverUI() {
const slider = document.getElementById('position-slider'); const slider = document.getElementById('position-slider');
const value = document.getElementById('position-value'); const value = document.getElementById('position-value');

View File

@@ -0,0 +1,615 @@
<!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>

View File

@@ -231,6 +231,7 @@
'thermostat': '🌡️', 'thermostat': '🌡️',
'contact': '🚪', 'contact': '🚪',
'temp_humidity_sensor': '🌡️', 'temp_humidity_sensor': '🌡️',
'three_phase_powermeter': '📊',
'relay': '💡', 'relay': '💡',
'outlet': '💡', 'outlet': '💡',
'cover': '🪟' 'cover': '🪟'
@@ -403,6 +404,15 @@
} }
break; break;
case 'three_phase_powermeter':
if (state.total_power != null) {
html = `<div class="state-primary">${state.total_power.toFixed(0)} W</div>`;
if (state.energy != null) {
html += `<div class="state-secondary">${state.energy.toFixed(2)} kWh</div>`;
}
}
break;
case 'relay': case 'relay':
case 'outlet': case 'outlet':
if (state.power) { if (state.power) {

View File

@@ -783,6 +783,23 @@ devices:
set: "shellies/lichtterasse/relay/0/command" set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0" state: "shellies/lichtterasse/relay/0"
- device_id: power_relay_caroutlet
name: Car Outlet
type: relay
cap_version: "relay@1.0.0"
technology: hottis_modbus
features:
power: true
topics:
set: "caroutlet/cmd"
state: "caroutlet/state"
- device_id: powermeter_caroutlet
name: Car Outlet
type: three_phase_powermeter
cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_modbus
topics:
state: "caroutlet/powermeter"

View File

@@ -279,4 +279,15 @@ rooms:
title: Licht Terasse title: Licht Terasse
icon: 💡 icon: 💡
rank: 290 rank: 290
- name: Garage
devices:
- device_id: power_relay_caroutlet
title: Ladestrom
icon:
rank: 310
- device_id: powermeter_caroutlet
title: Ladestrom
icon: 📊
rank: 320

View File

@@ -10,6 +10,9 @@ from packages.home_capabilities.temp_humidity_sensor import CAP_VERSION as TEMP_
from packages.home_capabilities.temp_humidity_sensor import TempHumidityState from packages.home_capabilities.temp_humidity_sensor import TempHumidityState
from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION
from packages.home_capabilities.relay import RelayState from packages.home_capabilities.relay import RelayState
from packages.home_capabilities.three_phase_powermeter import CAP_VERSION as THREE_PHASE_POWERMETER_VERSION
from packages.home_capabilities.three_phase_powermeter import ThreePhasePowerState
from packages.home_capabilities.layout import ( from packages.home_capabilities.layout import (
DeviceTile, DeviceTile,
Room, Room,
@@ -56,4 +59,5 @@ __all__ = [
"get_scene_by_id", "get_scene_by_id",
"load_groups", "load_groups",
"load_scenes", "load_scenes",
"ThreePhasePowerState",
] ]

View File

@@ -0,0 +1,29 @@
from pydantic import BaseModel, Field
class ThreePhasePowerState(BaseModel):
"""
State model for a three-phase power meter.
Required fields:
- energy: Total energy in kWh
- total_power: Total power in W
- phase1_power, phase2_power, phase3_power: Power per phase in W
- phase1_voltage, phase2_voltage, phase3_voltage: Voltage per phase in V
- phase1_current, phase2_current, phase3_current: Current per phase in A
"""
energy: float = Field(..., description="Total energy in kWh")
total_power: float = Field(..., description="Total power in W")
phase1_power: float = Field(..., description="Power for phase 1 in W")
phase2_power: float = Field(..., description="Power for phase 2 in W")
phase3_power: float = Field(..., description="Power for phase 3 in W")
phase1_voltage: float = Field(..., description="Voltage for phase 1 in V")
phase2_voltage: float = Field(..., description="Voltage for phase 2 in V")
phase3_voltage: float = Field(..., description="Voltage for phase 3 in V")
phase1_current: float = Field(..., description="Current for phase 1 in A")
phase2_current: float = Field(..., description="Current for phase 2 in A")
phase3_current: float = Field(..., description="Current for phase 3 in A")
# Capability metadata
CAP_VERSION = "three_phase_powermeter@1.0.0"
DISPLAY_NAME = "Three-Phase Power Meter"