Compare commits

..

42 Commits
0.3.0 ... 0.7.0

Author SHA1 Message Date
9ba478c34d seems to work
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/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 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 15:37:03 +01:00
15e132b187 messages fix 3 2025-12-08 14:27:50 +01:00
f40887ec37 messages fix 2 2025-12-08 14:27:25 +01:00
507f6f3854 messages fix 2025-12-08 14:25:31 +01:00
f163bb09bf initial 2025-12-08 13:56:48 +01:00
54fdcc12e1 deckenlampe wohnzimmer
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 13:19:00 +01:00
9f725c4c70 homekit names 3
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/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 11:47:39 +01:00
f1dbd9344d homekit names 2
All checks were successful
ci/woodpecker/tag/build/5 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/build/6 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/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 11:36:17 +01:00
5a67d7b330 homekit names
Some checks failed
ci/woodpecker/tag/config unknown status
ci/woodpecker/tag/namespace Pipeline is pending
ci/woodpecker/tag/build/5 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/build/6 Pipeline failed
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/ingress unknown status
2025-12-08 11:20:27 +01:00
cc342245f8 gartenlicht vorne
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 10:48:23 +01:00
50253d536d more lights 6
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 10:28:30 +01:00
e0aa50c9d2 more lights 5
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 09:22:38 +01:00
dc20d9f4b2 more lights 4
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 09:15:32 +01:00
ffb35928b4 more lights 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 22:12:23 +01:00
ac84ff7103 more lights 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 21:49:34 +01:00
c185494da3 more lights
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 21:44:57 +01:00
ec4a37a268 tasmoto and kommode
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 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/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-07 21:32:36 +01:00
d4b1d27b81 accessory name in logging
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/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/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-07 21:19:41 +01:00
ad07bc79e2 kugellampe patty
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 19:14:32 +01:00
ab41e79cb2 car outlet adjusted 8
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:53:06 +01:00
fe92d336b1 car outlet adjusted 7
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:47:54 +01:00
0ca59896ad car outlet adjusted 6
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:46:25 +01:00
7858996d0f car outlet adjusted 5
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 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/2 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 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/ingress Pipeline was successful
2025-12-05 15:42:41 +01:00
a0f7cc7bd9 car outlet adjusted 4
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/build/1 Pipeline was successful
ci/woodpecker/tag/namespace 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/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 15:25:50 +01:00
a98802437c car outlet adjusted 3
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/config 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/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 15:11:18 +01:00
708e287016 car outlet adjusted 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/4 Pipeline was successful
ci/woodpecker/tag/build/1 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/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 13:59:42 +01:00
d11eab8474 car outlet adjusted
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/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 13:49:12 +01:00
eccffbbd55 use image from registry 2025-12-03 22:38:26 +01:00
2b963a33ef build homekit too
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 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/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-03 22:32:42 +01:00
1311f7a59b Putzlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:53:17 +01:00
a226fa9268 Putzlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:26:38 +01:00
3bd8d293a2 fix licht spuele 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 15:46:43 +01:00
be30ad3a3c fix licht spuele 2025-12-01 15:45:12 +01:00
500384b1cd streamline ci 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/5 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/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 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-01 14:56:56 +01:00
6b4c247413 forgotten files
All checks were successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/5 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/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/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-01 14:53:50 +01:00
04a1807306 streamline ci
Some checks failed
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
2025-12-01 14:52:50 +01:00
db5e4589d0 fix error for devices with missing state 2025-12-01 14:48:32 +01:00
5399f044a1 separation of ui and static
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 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/ingress Pipeline was successful
6
2025-12-01 14:24:15 +01:00
16fa5143dd separation of ui and static
All checks were successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/5 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/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
5
2025-12-01 14:15:54 +01:00
cff154c247 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 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/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
4
2025-12-01 14:11:25 +01:00
038664ec94 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 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/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
3
2025-12-01 14:06:54 +01:00
2bbf825cf7 separation of ui and static
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 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/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2
2025-12-01 14:02:37 +01:00
36 changed files with 762 additions and 614 deletions

View File

@@ -1,5 +1,8 @@
when: when:
event: [tag] event: [tag]
ref:
exclude:
- refs/tags/*-configchange
matrix: matrix:
APP: APP:
@@ -8,6 +11,8 @@ matrix:
- abstraction - abstraction
- rules - rules
- static - static
- pulsegen
- homekit
steps: steps:
build-${APP}: build-${APP}:
@@ -22,8 +27,3 @@ steps:
repo: ${FORGE_NAME}/${CI_REPO}/${APP} repo: ${FORGE_NAME}/${CI_REPO}/${APP}
auto_tag: true auto_tag: true
dockerfile: apps/${APP}/Dockerfile dockerfile: apps/${APP}/Dockerfile
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

View File

@@ -1,23 +1,10 @@
when: when:
event: [tag] event: [tag]
steps: depends_on:
create_namespace: - 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
steps:
apply_configuration: apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4 image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment: environment:
@@ -36,6 +23,4 @@ steps:
--namespace=$NAMESPACE --namespace=$NAMESPACE
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
- kubectl apply -f deployment/configmap.yaml -n $NAMESPACE - kubectl apply -f deployment/configmap.yaml -n $NAMESPACE
when:
event: [tag]

View File

@@ -1,9 +1,13 @@
when: when:
event: [tag] event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on: depends_on:
- build - build
- predeploy - namespace
- config
matrix: matrix:
APP: APP:
@@ -12,6 +16,7 @@ matrix:
- abstraction - abstraction
- rules - rules
- static - static
- pulsegen
steps: steps:
deploy-${APP}: deploy-${APP}:
@@ -26,9 +31,5 @@ steps:
- export KUBECONFIG=/tmp/kubeconfig - export KUBECONFIG=/tmp/kubeconfig
- echo "Deploying application ${APP} ($IMAGE) to namespace $NAMESPACE" - echo "Deploying application ${APP} ($IMAGE) to namespace $NAMESPACE"
- cat deployment/${APP}-deployment.yaml | sed "s,%IMAGE%,$IMAGE,g" | kubectl apply -n $NAMESPACE -f - - cat deployment/${APP}-deployment.yaml | sed "s,%IMAGE%,$IMAGE,g" | kubectl apply -n $NAMESPACE -f -
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

View File

@@ -1,5 +1,8 @@
when: when:
event: [tag] event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on: depends_on:
- deploy - deploy
@@ -15,9 +18,4 @@ steps:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig - printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig - export KUBECONFIG=/tmp/kubeconfig
- kubectl apply -f deployment/ingress.yaml -n $NAMESPACE - kubectl apply -f deployment/ingress.yaml -n $NAMESPACE
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

15
.woodpecker/namespace.yml Normal file
View File

@@ -0,0 +1,15 @@
when:
event: [tag]
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"

View File

@@ -180,17 +180,10 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format # Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload) vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats and Shelly relays, vendor_payload is a plain string logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_payload}")
# For other devices, it's a dict that needs JSON encoding logger.debug(f"MQTT message published on {vendor_topic}: {vendor_payload}")
if (device_technology == "max" and device_type == "thermostat") or \ await mqtt_client.publish(vendor_topic, vendor_payload, qos=1)
(device_technology == "shelly" and device_type == "relay"):
vendor_message = vendor_payload # Already a string
else:
vendor_message = json.dumps(vendor_payload)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_message}")
await mqtt_client.publish(vendor_topic, vendor_message, qos=1)
async def handle_vendor_state( async def handle_vendor_state(

View File

@@ -17,12 +17,12 @@ logger = logging.getLogger(__name__)
# HANDLER FUNCTIONS: simulator technology # HANDLER FUNCTIONS: simulator technology
# ============================================================================ # ============================================================================
def _transform_light_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_light_simulator_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract light payload to simulator format. """Transform abstract light payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed). Simulator uses same format as abstract protocol (no transformation needed).
""" """
return payload return json.dumps(payload)
def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]: def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
@@ -36,12 +36,12 @@ def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
return payload return payload
def _transform_thermostat_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_simulator_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to simulator format. """Transform abstract thermostat payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed). Simulator uses same format as abstract protocol (no transformation needed).
""" """
return payload return json.dumps(payload)
def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]: def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
@@ -59,7 +59,7 @@ def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
# HANDLER FUNCTIONS: zigbee2mqtt technology # HANDLER FUNCTIONS: zigbee2mqtt technology
# ============================================================================ # ============================================================================
def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract light payload to zigbee2mqtt format. """Transform abstract light payload to zigbee2mqtt format.
Transformations: Transformations:
@@ -84,7 +84,7 @@ def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str,
# Convert percentage (0-100) to zigbee2mqtt range (0-254) # Convert percentage (0-100) to zigbee2mqtt range (0-254)
vendor_payload["brightness"] = round(abstract_brightness * 254 / 100) vendor_payload["brightness"] = round(abstract_brightness * 254 / 100)
return vendor_payload return json.dumps(vendor_payload)
def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]: def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
@@ -115,7 +115,7 @@ def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
return abstract_payload return abstract_payload
def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to zigbee2mqtt format. """Transform abstract thermostat payload to zigbee2mqtt format.
Transformations: Transformations:
@@ -132,7 +132,7 @@ def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict
# zigbee2mqtt expects current_heating_setpoint as string # zigbee2mqtt expects current_heating_setpoint as string
vendor_payload["current_heating_setpoint"] = str(payload["target"]) vendor_payload["current_heating_setpoint"] = str(payload["target"])
return vendor_payload return json.dumps(vendor_payload)
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]: def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
@@ -171,14 +171,14 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any
# HANDLER FUNCTIONS: contact_sensor - zigbee2mqtt technology # HANDLER FUNCTIONS: contact_sensor - zigbee2mqtt technology
# ============================================================================ # ============================================================================
def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to zigbee2mqtt format. """Transform abstract contact sensor payload to zigbee2mqtt format.
Contact sensors are read-only, so this should not be called for SET commands. Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility. Returns payload as-is for compatibility.
""" """
logger.warning("Contact sensors are read-only - SET commands should not be used") logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload return json.dumps(payload)
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]: def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
@@ -225,14 +225,14 @@ def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str,
# HANDLER FUNCTIONS: contact_sensor - max technology (Homegear MAX!) # HANDLER FUNCTIONS: contact_sensor - max technology (Homegear MAX!)
# ============================================================================ # ============================================================================
def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to MAX! format. """Transform abstract contact sensor payload to MAX! format.
Contact sensors are read-only, so this should not be called for SET commands. Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility. Returns payload as-is for compatibility.
""" """
logger.warning("Contact sensors are read-only - SET commands should not be used") logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload return json.dumps(payload)
def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]: def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
@@ -266,13 +266,13 @@ def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
# HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology # HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology
# ============================================================================ # ============================================================================
def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract temp/humidity sensor payload to zigbee2mqtt format. """Transform abstract temp/humidity sensor payload to zigbee2mqtt format.
Temp/humidity sensors are read-only, so this should not be called for SET commands. Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility. Returns payload as-is for compatibility.
""" """
return payload return json.dumps(payload)
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]: def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
@@ -284,36 +284,13 @@ def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dic
return payload return payload
# ============================================================================
# HANDLER FUNCTIONS: temp_humidity_sensor - MAX! technology
# ============================================================================
def _transform_temp_humidity_sensor_max_to_vendor(payload: str) -> dict[str, Any]:
"""Transform abstract temp/humidity sensor payload to MAX! format.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
payload = json.loads(payload)
return payload
def _transform_temp_humidity_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! temp/humidity sensor payload to abstract format.
Passthrough - MAX! provides temperature, humidity, battery directly.
"""
payload = json.loads(payload)
return payload
# ============================================================================ # ============================================================================
# HANDLER FUNCTIONS: relay - zigbee2mqtt technology # HANDLER FUNCTIONS: relay - zigbee2mqtt technology
# ============================================================================ # ============================================================================
def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to zigbee2mqtt format. """Transform abstract relay payload to zigbee2mqtt format.
Relay only has power on/off, same transformation as light. Relay only has power on/off, same transformation as light.
@@ -325,7 +302,7 @@ def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str,
power_value = vendor_payload.pop("power") power_value = vendor_payload.pop("power")
vendor_payload["state"] = power_value.upper() if isinstance(power_value, str) else power_value vendor_payload["state"] = power_value.upper() if isinstance(power_value, str) else power_value
return vendor_payload return json.dumps(vendor_payload)
def _transform_relay_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]: def _transform_relay_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
@@ -375,10 +352,40 @@ 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 # HANDLER FUNCTIONS: relay - tasmota technology
# ============================================================================ # ============================================================================
def _transform_relay_hottis_modbus_to_vendor(payload: dict[str, Any]) -> str: def _transform_relay_tasmota_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Tasmota format.
Tasmota expects plain text 'on' or 'off' (not JSON).
- power: 'on'/'off' -> 'on'/'off' (plain string)
Example:
- Abstract: {'power': 'on'}
- Tasmota: 'on'
"""
power = payload.get("power", "off")
return power
def _transform_relay_tasmota_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Tasmota relay payload to abstract format.
Tasmota sends plain text 'on' or 'off' (not JSON).
- 'on'/'off' -> power: 'on'/'off'
Example:
- Tasmota: 'ON'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip().lower()}
# ============================================================================
# HANDLER FUNCTIONS: relay - hottis_pv_modbus technology
# ============================================================================
def _transform_relay_hottis_pv_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Modbus format. """Transform abstract relay payload to Hottis Modbus format.
Hottis Modbus expects plain text 'on' or 'off' (not JSON). Hottis Modbus expects plain text 'on' or 'off' (not JSON).
@@ -392,26 +399,32 @@ def _transform_relay_hottis_modbus_to_vendor(payload: dict[str, Any]) -> str:
return power return power
def _transform_relay_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]: def _transform_relay_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format. def _transform_relay_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format.
Hottis Modbus sends JSON like:
{"status": "Ok", "timestamp": "...", "state": false, "cnt": 528}
We only care about the 'state' field:
- state: true -> power: 'on'
- state: false -> power: 'off'
"""
data = json.loads(payload)
state = data.get("state", False)
power = "on" if bool(state) else "off"
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()} return {"power": payload.strip()}
# ============================================================================ # ============================================================================
# HANDLER FUNCTIONS: three_phase_powermeter - hottis_modbus technology # HANDLER FUNCTIONS: three_phase_powermeter - hottis_pv_modbus technology
# ============================================================================ # ============================================================================
def _transform_three_phase_powermeter_hottis_modbus_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract three_phase_powermeter payload to hottis_modbus format. """Transform abstract three_phase_powermeter payload to hottis_pv_modbus format.
energy: float = Field(..., description="Total energy in kWh") energy: float = Field(..., description="Total energy in kWh")
total_power: float = Field(..., description="Total power in W") total_power: float = Field(..., description="Total power in W")
phase1_power: float = Field(..., description="Power for phase 1 in W") phase1_power: float = Field(..., description="Power for phase 1 in W")
@@ -441,34 +454,55 @@ def _transform_three_phase_powermeter_hottis_modbus_to_vendor(payload: dict[str,
"phase3_current": payload.get("phase3_current", 0.0), "phase3_current": payload.get("phase3_current", 0.0),
} }
return vendor_payload return json.dumps(vendor_payload)
def _transform_three_phase_powermeter_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]: def _transform_three_phase_powermeter_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_modbus three_phase_powermeter payload to abstract format. """Transform hottis_pv_modbus three_phase_powermeter payload to abstract format.
Transformations: Transformations:
- Direct mapping of all power meter fields - Map vendor field names to abstract field names
- totalImportEnergy -> energy
Example: - powerL1/powerL2/powerL3 -> phase1_power/phase2_power/phase3_power
- hottis_modbus: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...} - voltageL1/voltageL2/voltageL3 -> phase1_voltage/phase2_voltage/phase3_voltage
- Abstract: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...} - currentL1/currentL2/currentL3 -> phase1_current/phase2_current/phase3_current
- Sum of powerL1..3 -> total_power
""" """
payload = json.loads(payload) data = json.loads(payload)
# Helper to read numeric values uniformly as float
def _get_float(key: str, default: float = 0.0) -> float:
return float(data.get(key, default))
# Read all numeric values via helper for consistent error handling
phase1_power = _get_float("powerL1")
phase2_power = _get_float("powerL2")
phase3_power = _get_float("powerL3")
phase1_voltage = _get_float("voltageL1")
phase2_voltage = _get_float("voltageL2")
phase3_voltage = _get_float("voltageL3")
phase1_current = _get_float("currentL1")
phase2_current = _get_float("currentL2")
phase3_current = _get_float("currentL3")
energy = _get_float("totalImportEnergy")
abstract_payload = { abstract_payload = {
"energy": payload.get("energy", 0.0), "energy": energy,
"total_power": payload.get("total_power", 0.0), "total_power": phase1_power + phase2_power + phase3_power,
"phase1_power": payload.get("phase1_power", 0.0), "phase1_power": phase1_power,
"phase2_power": payload.get("phase2_power", 0.0), "phase2_power": phase2_power,
"phase3_power": payload.get("phase3_power", 0.0), "phase3_power": phase3_power,
"phase1_voltage": payload.get("phase1_voltage", 0.0), "phase1_voltage": phase1_voltage,
"phase2_voltage": payload.get("phase2_voltage", 0.0), "phase2_voltage": phase2_voltage,
"phase3_voltage": payload.get("phase3_voltage", 0.0), "phase3_voltage": phase3_voltage,
"phase1_current": payload.get("phase1_current", 0.0), "phase1_current": phase1_current,
"phase2_current": payload.get("phase2_current", 0.0), "phase2_current": phase2_current,
"phase3_current": payload.get("phase3_current", 0.0), "phase3_current": phase3_current,
} }
return abstract_payload return abstract_payload
@@ -567,24 +601,22 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
# Temperature & humidity sensor transformations (support both type aliases) # Temperature & humidity sensor transformations (support both type aliases)
("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor, ("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract, ("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor, ("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract, ("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
# Relay transformations # Relay transformations
("relay", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor, ("relay", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor,
("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_pv_modbus", "to_vendor"): _transform_relay_hottis_pv_modbus_to_vendor,
("relay", "hottis_modbus", "to_abstract"): _transform_relay_hottis_modbus_to_abstract, ("relay", "hottis_pv_modbus", "to_abstract"): _transform_relay_hottis_pv_modbus_to_abstract,
("relay", "tasmota", "to_vendor"): _transform_relay_tasmota_to_vendor,
("relay", "tasmota", "to_abstract"): _transform_relay_tasmota_to_abstract,
# Three-Phase Powermeter transformations # Three-Phase Powermeter transformations
("three_phase_powermeter", "hottis_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_modbus_to_vendor, ("three_phase_powermeter", "hottis_pv_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor,
("three_phase_powermeter", "hottis_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_modbus_to_abstract, ("three_phase_powermeter", "hottis_pv_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_pv_modbus_to_abstract,
} }

View File

@@ -14,7 +14,7 @@ class ContactAccessory(Accessory):
category = CATEGORY_SENSOR category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
""" """
Initialize the contact sensor accessory. Initialize the contact sensor accessory.
@@ -22,9 +22,8 @@ class ContactAccessory(Accessory):
driver: HAP driver instance driver: HAP driver instance
device: Device object from DeviceRegistry device: Device object from DeviceRegistry
api_client: ApiClient for sending commands api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
""" """
name = display_name or device.friendly_name or device.name name = device.name
super().__init__(driver, name, *args, **kwargs) super().__init__(driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client

View File

@@ -16,7 +16,7 @@ class OnOffLightAccessory(Accessory):
category = CATEGORY_LIGHTBULB category = CATEGORY_LIGHTBULB
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
""" """
Initialize the light accessory. Initialize the light accessory.
@@ -24,9 +24,8 @@ class OnOffLightAccessory(Accessory):
driver: HAP driver instance driver: HAP driver instance
device: Device object from DeviceRegistry device: Device object from DeviceRegistry
api_client: ApiClient for sending commands api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
""" """
name = display_name or device.friendly_name or device.name name = device.name
super().__init__(driver, name, *args, **kwargs) super().__init__(driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client
@@ -57,9 +56,9 @@ class OnOffLightAccessory(Accessory):
class DimmableLightAccessory(OnOffLightAccessory): class DimmableLightAccessory(OnOffLightAccessory):
"""Dimmable Light with brightness control.""" """Dimmable Light with brightness control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
# Don't call super().__init__() yet - we need to set up service first # Don't call super().__init__() yet - we need to set up service first
name = display_name or device.friendly_name or device.name name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs) Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client
@@ -106,9 +105,9 @@ class DimmableLightAccessory(OnOffLightAccessory):
class ColorLightAccessory(DimmableLightAccessory): class ColorLightAccessory(DimmableLightAccessory):
"""RGB Light with full color control.""" """RGB Light with full color control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
# Don't call super().__init__() - build everything from scratch # Don't call super().__init__() - build everything from scratch
name = display_name or device.friendly_name or device.name name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs) Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class OutletAccessory(Accessory):
category = CATEGORY_OUTLET category = CATEGORY_OUTLET
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
""" """
Initialize the outlet accessory. Initialize the outlet accessory.
@@ -23,9 +23,8 @@ class OutletAccessory(Accessory):
driver: HAP driver instance driver: HAP driver instance
device: Device object from DeviceRegistry device: Device object from DeviceRegistry
api_client: ApiClient for sending commands api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
""" """
name = display_name or device.friendly_name or device.name name = device.name
super().__init__(driver, name, *args, **kwargs) super().__init__(driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class TempHumidityAccessory(Accessory):
category = CATEGORY_SENSOR category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
""" """
Initialize the temp/humidity sensor accessory. Initialize the temp/humidity sensor accessory.
@@ -23,9 +23,8 @@ class TempHumidityAccessory(Accessory):
driver: HAP driver instance driver: HAP driver instance
device: Device object from DeviceRegistry device: Device object from DeviceRegistry
api_client: ApiClient for sending commands api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
""" """
name = display_name or device.friendly_name or device.name name = device.name
super().__init__(driver, name, *args, **kwargs) super().__init__(driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client

View File

@@ -17,7 +17,7 @@ class ThermostatAccessory(Accessory):
category = CATEGORY_THERMOSTAT category = CATEGORY_THERMOSTAT
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): def __init__(self, driver, device, api_client, *args, **kwargs):
""" """
Initialize the thermostat accessory. Initialize the thermostat accessory.
@@ -25,9 +25,8 @@ class ThermostatAccessory(Accessory):
driver: HAP driver instance driver: HAP driver instance
device: Device object from DeviceRegistry device: Device object from DeviceRegistry
api_client: ApiClient for sending commands api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
""" """
name = display_name or device.friendly_name or device.name name = device.name
super().__init__(driver, name, *args, **kwargs) super().__init__(driver, name, *args, **kwargs)
self.device = device self.device = device
self.api_client = api_client self.api_client = api_client

View File

@@ -50,26 +50,7 @@ class ApiClient:
except Exception as e: except Exception as e:
logger.error(f"Failed to get devices: {e}") logger.error(f"Failed to get devices: {e}")
raise raise
def get_layout(self) -> Dict:
"""
Get layout information (rooms and device assignments).
Returns:
Layout dictionary with room structure
"""
try:
response = httpx.get(
f'{self.base_url}/layout',
headers=self.headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get layout: {e}")
raise
def get_device_state(self, device_id: str) -> Dict: def get_device_state(self, device_id: str) -> Dict:
""" """
Get current state of a specific device. Get current state of a specific device.

View File

@@ -18,8 +18,6 @@ class Device:
device_id: str device_id: str
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover" type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
name: str # Short name from /devices name: str # Short name from /devices
friendly_name: str # Display title from /layout (fallback to name)
room: Optional[str] # Room name from layout
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true}) features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
read_only: bool # True for sensors that don't accept commands read_only: bool # True for sensors that don't accept commands
@@ -50,24 +48,7 @@ class DeviceRegistry:
""" """
# Get devices and layout # Get devices and layout
devices_data = api_client.get_devices() devices_data = api_client.get_devices()
layout_data = api_client.get_layout()
# Build lookup: device_id -> (room_name, title)
layout_map = {}
if isinstance(layout_data, dict) and 'rooms' in layout_data:
rooms_list = layout_data['rooms']
if isinstance(rooms_list, list):
for room in rooms_list:
if isinstance(room, dict):
room_name = room.get('name', 'Unknown')
devices_in_room = room.get('devices', [])
for device_info in devices_in_room:
if isinstance(device_info, dict):
device_id = device_info.get('device_id')
title = device_info.get('title', '')
if device_id:
layout_map[device_id] = (room_name, title)
# Create Device objects # Create Device objects
devices = [] devices = []
for dev_data in devices_data: for dev_data in devices_data:
@@ -76,9 +57,6 @@ class DeviceRegistry:
logger.warning(f"Device without device_id: {dev_data}") logger.warning(f"Device without device_id: {dev_data}")
continue continue
# Get layout info
room_name, title = layout_map.get(device_id, (None, ''))
# Determine if read-only (sensors don't accept set commands) # Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '') device_type = dev_data.get('type', '')
read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke'] read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke']
@@ -86,9 +64,7 @@ class DeviceRegistry:
device = Device( device = Device(
device_id=device_id, device_id=device_id,
type=device_type, type=device_type,
name=dev_data.get('name', device_id), name=device_id,
friendly_name=title or dev_data.get('name', device_id),
room=room_name,
features=dev_data.get('features', {}), features=dev_data.get('features', {}),
read_only=read_only read_only=read_only
) )

View File

@@ -1,8 +1,6 @@
services: services:
homekit-bridge: homekit-bridge:
build: image: gitea.hottis.de/wn/home-automation/homekit:0.5.0
context: ../..
dockerfile: apps/homekit/Dockerfile
container_name: homekit-bridge container_name: homekit-bridge
# Required for mDNS/Bonjour to work properly # Required for mDNS/Bonjour to work properly

View File

@@ -71,14 +71,9 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
try: try:
accessory = create_accessory_for_device(device, api_client, driver) accessory = create_accessory_for_device(device, api_client, driver)
if accessory: if accessory:
# Set room information in the accessory (HomeKit will use this for suggestions)
if device.room:
# Store room info for potential future use
accessory._room_name = device.room
bridge.add_accessory(accessory) bridge.add_accessory(accessory)
accessory_map[device.device_id] = accessory accessory_map[device.device_id] = accessory
logger.info(f"Added accessory: {device.friendly_name} ({device.type}) in room: {device.room or 'Unknown'}") logger.info(f"Added accessory: {device.name} ({device.type}, {accessory.__class__.__name__})")
else: else:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})") logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
except Exception as e: except Exception as e:
@@ -90,23 +85,6 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
logger.info(f"Bridge built with {len(accessory_map)} accessories") logger.info(f"Bridge built with {len(accessory_map)} accessories")
return bridge return bridge
def get_accessory_name(device) -> str:
"""
Build accessory name including room information.
Args:
device: Device object from DeviceRegistry
Returns:
Name string like "Device Name (Room)" or just "Device Name" if no room
"""
base_name = device.friendly_name or device.name
if device.room:
return f"{base_name} ({device.room})"
return base_name
def create_accessory_for_device(device, api_client: ApiClient, driver: AccessoryDriver): def create_accessory_for_device(device, api_client: ApiClient, driver: AccessoryDriver):
""" """
Create appropriate HomeKit accessory based on device type and features. Create appropriate HomeKit accessory based on device type and features.
@@ -115,32 +93,30 @@ def create_accessory_for_device(device, api_client: ApiClient, driver: Accessory
""" """
device_type = device.type device_type = device.type
features = device.features features = device.features
display_name = get_accessory_name(device)
# Light accessories # Light accessories
if device_type == "light": if device_type == "light":
if features.get("color_hsb"): if features.get("color_hsb"):
return ColorLightAccessory(driver, device, api_client, display_name=display_name) return ColorLightAccessory(driver, device, api_client)
elif features.get("brightness"): elif features.get("brightness"):
return DimmableLightAccessory(driver, device, api_client, display_name=display_name) return DimmableLightAccessory(driver, device, api_client)
else: else:
return OnOffLightAccessory(driver, device, api_client, display_name=display_name) return OnOffLightAccessory(driver, device, api_client)
# Thermostat # Thermostat
elif device_type == "thermostat": elif device_type == "thermostat":
return ThermostatAccessory(driver, device, api_client, display_name=display_name) return ThermostatAccessory(driver, device, api_client)
# Contact sensor # Contact sensor
elif device_type == "contact": elif device_type == "contact":
return ContactAccessory(driver, device, api_client, display_name=display_name) return ContactAccessory(driver, device, api_client)
# Temperature/Humidity sensor # Temperature/Humidity sensor
elif device_type == "temp_humidity_sensor": elif device_type == "temp_humidity_sensor":
return TempHumidityAccessory(driver, device, api_client, display_name=display_name) return TempHumidityAccessory(driver, device, api_client)
# Relay/Outlet # Relay/Outlet
elif device_type == "relay": elif device_type == "relay":
return OutletAccessory(driver, device, api_client, display_name=display_name) return OutletAccessory(driver, device, api_client)
# Cover/Blinds (optional) # Cover/Blinds (optional)
elif device_type == "cover": elif device_type == "cover":

35
apps/pulsegen/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# Pulsegen Dockerfile
# MQTT Pulse Generator Worker
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install Python dependencies
COPY apps/pulsegen/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/pulsegen/ /app/apps/pulsegen/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Run application
CMD ["python", "-m", "apps.pulsegen.main"]

53
apps/pulsegen/README.md Normal file
View File

@@ -0,0 +1,53 @@
# Pulsegen
MQTT-basierte Pulse-Generator Applikation für Home Automation.
## Funktionen
- MQTT-Kommunikation über `aiomqtt`
- Automatische Reconnect-Logik
- Graceful shutdown (SIGTERM/SIGINT)
- JSON message parsing
- Konfigurierbar über Umgebungsvariablen
## Umgebungsvariablen
- `MQTT_BROKER`: MQTT Broker Hostname (default: `localhost`)
- `MQTT_PORT`: MQTT Broker Port (default: `1883`)
## Entwicklung
Lokal starten:
```bash
cd apps/pulsegen
python -m venv venv
source venv/bin/activate # oder venv\Scripts\activate auf Windows
pip install -r requirements.txt
python main.py
```
## Docker
Build:
```bash
docker build -f apps/pulsegen/Dockerfile -t pulsegen .
```
Run:
```bash
docker run -e MQTT_BROKER=172.16.2.16 -e MQTT_PORT=1883 pulsegen
```
## MQTT Topics
### Subscribed
- `pulsegen/command/#` - Kommandos für pulsegen
- `home/+/+/state` - Device state updates
### Published
- `pulsegen/status` - Status-Updates der Applikation

View File

@@ -0,0 +1 @@
"""Pulsegen - MQTT pulse generator application."""

226
apps/pulsegen/main.py Normal file
View File

@@ -0,0 +1,226 @@
"""Pulsegen - MQTT pulse generator application."""
import asyncio
import json
import logging
import os
import signal
from typing import Any
from aiomqtt import Client, Message
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
COIL_STATUS_PREFIX = "dt1/di"
COIL_STATUS_TOPIC = f"{COIL_STATUS_PREFIX}/+"
PULSEGEN_COMMAND_PREFIX = "pulsegen/command"
PULSEGEN_COMMAND_TOPIC = f"{PULSEGEN_COMMAND_PREFIX}/+/+"
COIL_COMMAND_PREFIX = "dt1/coil"
PULSEGEN_STATUS_PREFIX = "pulsegen/status"
COIL_STATUS_CACHE: dict[int, bool] = {}
def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment variables.
Returns:
tuple: (broker_host, broker_port)
"""
broker = os.getenv("MQTT_BROKER", "localhost")
port = int(os.getenv("MQTT_PORT", "1883"))
logger.info(f"MQTT settings: broker={broker}, port={port}")
return broker, port
async def handle_message(message: Message, client: Client) -> None:
"""Handle incoming MQTT message.
Args:
message: MQTT message object
client: MQTT client instance
"""
try:
payload = message.payload.decode()
logger.info(f"Received message on {message.topic}: {payload}")
try:
topic = str(message.topic)
match topic.split("/"):
case [prefix, di, coil_id] if f"{prefix}/{di}" == COIL_STATUS_PREFIX:
try:
coil_num = int(coil_id)
except ValueError:
logger.debug(f"Invalid coil id in topic: {topic}")
return
state = payload.lower() in ("1", "true", "on")
COIL_STATUS_CACHE[coil_num] = state
logger.info(f"Updated coil {coil_num} status to {state}")
logger.info(f"Publishing pulsegen status for coil {coil_num}: {state}")
await client.publish(
topic=f"{PULSEGEN_STATUS_PREFIX}/{coil_num}",
payload="on" if state else "off",
qos=1,
retain=True,
)
case [prefix, command, coil_in_id, coil_out_id] if f"{prefix}/{command}" == PULSEGEN_COMMAND_PREFIX:
try:
coil_in_id = int(coil_in_id)
coil_out_id = int(coil_out_id)
except ValueError:
logger.debug(f"Invalid coil id in topic: {topic}")
return
try:
coil_state = COIL_STATUS_CACHE[coil_in_id]
except KeyError:
logger.debug(f"Coil {coil_in_id} status unknown, cannot process command")
return
cmd = payload.lower() in ("1", "true", "on")
if cmd == coil_state:
logger.info(f"Coil {coil_in_id} already in desired state {cmd}, ignoring command")
return
logger.info(f"Received pulsegen command on {topic}: {coil_in_id=}, {coil_out_id=}, {cmd=}")
coil_cmd_topic = f"{COIL_COMMAND_PREFIX}/{coil_out_id}"
logger.info(f"Sending raising edge command: topic={coil_cmd_topic}")
await client.publish(
topic=coil_cmd_topic,
payload="1",
qos=1,
retain=False,
)
await asyncio.sleep(0.2)
logger.info(f"Sending falling edge command: topic={coil_cmd_topic}")
await client.publish(
topic=coil_cmd_topic,
payload="0",
qos=1,
retain=False,
)
case _:
logger.debug(f"Ignoring message on unrelated topic: {topic}")
except Exception as e:
logger.debug(f"Exception when handling payload: {e}")
except Exception as e:
logger.error(f"Error handling message: {e}", exc_info=True)
async def publish_example(client: Client) -> None:
"""Example function to publish MQTT messages.
Args:
client: MQTT client instance
"""
topic = "pulsegen/status"
payload = {
"status": "running",
"timestamp": asyncio.get_event_loop().time()
}
await client.publish(
topic=topic,
payload=json.dumps(payload),
qos=1
)
logger.info(f"Published to {topic}: {payload}")
async def mqtt_worker(shutdown_event: asyncio.Event) -> None:
"""Main MQTT worker loop.
Connects to MQTT broker, subscribes to topics, and processes messages.
Args:
shutdown_event: Event to signal shutdown
"""
broker, port = get_mqtt_settings()
reconnect_interval = 5 # seconds
while not shutdown_event.is_set():
try:
logger.info(f"Connecting to MQTT broker {broker}:{port}...")
async with Client(
hostname=broker,
port=port,
identifier="pulsegen"
) as client:
logger.info("Connected to MQTT broker")
# Subscribe to topics
for topic in [PULSEGEN_COMMAND_TOPIC, COIL_STATUS_TOPIC]:
await client.subscribe(topic)
logger.info(f"Subscribed to {topic}")
# Publish startup message
await publish_example(client)
# Message loop
async for message in client.messages:
if shutdown_event.is_set():
break
try:
await handle_message(message, client)
except Exception as e:
logger.error(f"Error in message handler: {e}", exc_info=True)
except asyncio.CancelledError:
logger.info("MQTT worker cancelled")
break
except Exception as e:
logger.error(f"MQTT error: {e}", exc_info=True)
if not shutdown_event.is_set():
logger.info(f"Reconnecting in {reconnect_interval} seconds...")
await asyncio.sleep(reconnect_interval)
async def main() -> None:
"""Main application entry point."""
logger.info("Starting pulsegen application...")
# Shutdown event for graceful shutdown
shutdown_event = asyncio.Event()
# Setup signal handlers
def signal_handler(sig: int) -> None:
logger.info(f"Received signal {sig}, initiating shutdown...")
shutdown_event.set()
loop = asyncio.get_event_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda s=sig: signal_handler(s))
# Start MQTT worker
worker_task = asyncio.create_task(mqtt_worker(shutdown_event))
# Wait for shutdown signal
await shutdown_event.wait()
# Wait for worker to finish
await worker_task
logger.info("Pulsegen application stopped")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1 @@
aiomqtt==2.3.0

View File

@@ -8,7 +8,7 @@ WORKDIR /usr/share/nginx/html
RUN rm -rf ./* RUN rm -rf ./*
# Copy only static assets from the UI project # Copy only static assets from the UI project
COPY apps/ui/static/ ./ COPY apps/static/static/ ./
EXPOSE 80 EXPOSE 80

View File

@@ -1,301 +0,0 @@
# Home Automation API Client
Wiederverwendbare JavaScript-API-Client-Bibliothek für das Home Automation UI.
## Installation
Füge die folgenden Script-Tags in deine HTML-Seiten ein:
```html
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
```
## Konfiguration
Der API-Client nutzt `window.API_BASE`, das vom Backend gesetzt wird:
```javascript
window.API_BASE = '{{ api_base }}'; // Jinja2 template
```
## Verwendung
### Globale Instanz
Der API-Client erstellt automatisch eine globale Instanz `window.apiClient`:
```javascript
// Layout abrufen
const layout = await window.apiClient.getLayout();
// Geräte abrufen
const devices = await window.apiClient.getDevices();
// Gerätestatus abrufen
const state = await window.apiClient.getDeviceState('kitchen_light');
// Gerätesteuerung
await window.apiClient.setDeviceState('kitchen_light', 'light', {
power: true,
brightness: 80
});
```
### Verfügbare Methoden
#### `getLayout(): Promise<Layout>`
Lädt die Layout-Daten (Räume und ihre Geräte).
```javascript
const layout = await window.apiClient.getLayout();
// { rooms: [{name: "Küche", devices: ["kitchen_light", ...]}, ...] }
```
#### `getDevices(): Promise<Device[]>`
Lädt alle Geräte mit ihren Features.
```javascript
const devices = await window.apiClient.getDevices();
// [{device_id: "...", name: "...", type: "light", features: {...}}, ...]
```
#### `getDeviceState(deviceId): Promise<DeviceState>`
Lädt den aktuellen Status eines Geräts.
```javascript
const state = await window.apiClient.getDeviceState('kitchen_light');
// {power: true, brightness: 80, ...}
```
#### `getAllStates(): Promise<Object>`
Lädt alle Gerätestatus auf einmal.
```javascript
const states = await window.apiClient.getAllStates();
// {"kitchen_light": {power: true, ...}, "thermostat_1": {...}, ...}
```
#### `setDeviceState(deviceId, type, payload): Promise<void>`
Sendet einen Befehl an ein Gerät.
```javascript
// Licht einschalten
await window.apiClient.setDeviceState('kitchen_light', 'light', {
power: true,
brightness: 80
});
// Thermostat einstellen
await window.apiClient.setDeviceState('thermostat_1', 'thermostat', {
target_temp: 22.5
});
// Rollladen steuern
await window.apiClient.setDeviceState('cover_1', 'cover', {
position: 50
});
```
#### `getDeviceRoom(deviceId): Promise<{room: string}>`
Ermittelt den Raum eines Geräts.
```javascript
const { room } = await window.apiClient.getDeviceRoom('kitchen_light');
// {room: "Küche"}
```
#### `getScenes(): Promise<Scene[]>`
Lädt alle verfügbaren Szenen.
```javascript
const scenes = await window.apiClient.getScenes();
```
#### `activateScene(sceneId): Promise<void>`
Aktiviert eine Szene.
```javascript
await window.apiClient.activateScene('evening');
```
### Realtime-Updates (SSE)
#### `connectRealtime(onEvent, onError): EventSource`
Verbindet sich mit dem SSE-Stream für Live-Updates.
```javascript
window.apiClient.connectRealtime(
(event) => {
console.log('Update:', event.device_id, event.state);
// event = {device_id: "...", type: "state", state: {...}}
},
(error) => {
console.error('Connection error:', error);
}
);
```
#### `onDeviceUpdate(deviceId, callback): Function`
Registriert einen Listener für spezifische Geräte-Updates.
```javascript
// Für ein bestimmtes Gerät
const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
console.log('Kitchen light changed:', event.state);
updateUI(event.state);
});
// Für alle Geräte
const unsubscribeAll = window.apiClient.onDeviceUpdate(null, (event) => {
console.log('Any device changed:', event.device_id, event.state);
});
// Später: Listener entfernen
unsubscribe();
```
#### `disconnectRealtime(): void`
Trennt die SSE-Verbindung und entfernt alle Listener.
```javascript
window.apiClient.disconnectRealtime();
```
### Helper-Methoden
#### `findDevice(devices, deviceId): Device|null`
Findet ein Gerät in einem Array.
```javascript
const devices = await window.apiClient.getDevices();
const device = window.apiClient.findDevice(devices, 'kitchen_light');
```
#### `findRoom(layout, roomName): Room|null`
Findet einen Raum im Layout.
```javascript
const layout = await window.apiClient.getLayout();
const room = window.apiClient.findRoom(layout, 'Küche');
```
#### `getDevicesForRoom(layout, devices, roomName): Device[]`
Gibt alle Geräte eines Raums zurück.
```javascript
const layout = await window.apiClient.getLayout();
const devices = await window.apiClient.getDevices();
const kitchenDevices = window.apiClient.getDevicesForRoom(layout, devices, 'Küche');
```
#### `api(path): string`
Konstruiert eine vollständige API-URL.
```javascript
const url = window.apiClient.api('/devices');
// "http://172.19.1.11:8001/devices"
```
### Backward Compatibility
Die globale `api()` Funktion ist weiterhin verfügbar:
```javascript
function api(url) {
return window.apiClient.api(url);
}
```
## Typen (JSDoc)
Die Datei `types.js` enthält JSDoc-Definitionen für alle API-Typen:
- `Room` - Raum mit Geräten
- `Layout` - Layout-Struktur
- `Device` - Gerätedaten
- `DeviceFeatures` - Geräte-Features
- `DeviceState` - Gerätestatus (Light, Thermostat, Contact, etc.)
- `RealtimeEvent` - SSE-Event-Format
- `Scene` - Szenen-Definition
- `*Payload` - Command-Payloads für verschiedene Gerätetypen
Diese ermöglichen IDE-Autocomplete und Type-Checking in modernen Editoren (VS Code, WebStorm).
## Beispiel: Vollständige Seite
```html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>My Page</title>
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
</head>
<body>
<div id="status"></div>
<button id="toggle">Toggle Light</button>
<script>
window.API_BASE = 'http://172.19.1.11:8001';
const deviceId = 'kitchen_light';
async function init() {
// Load initial state
const state = await window.apiClient.getDeviceState(deviceId);
updateUI(state);
// Listen for updates
window.apiClient.onDeviceUpdate(deviceId, (event) => {
updateUI(event.state);
});
// Connect to realtime
window.apiClient.connectRealtime((event) => {
console.log('Event:', event);
});
// Handle button clicks
document.getElementById('toggle').onclick = async () => {
const currentState = await window.apiClient.getDeviceState(deviceId);
await window.apiClient.setDeviceState(deviceId, 'light', {
power: !currentState.power
});
};
}
function updateUI(state) {
document.getElementById('status').textContent =
state.power ? 'ON' : 'OFF';
}
init();
</script>
</body>
</html>
```
## Error Handling
Alle API-Methoden werfen Exceptions bei Fehlern:
```javascript
try {
const state = await window.apiClient.getDeviceState('invalid_id');
} catch (error) {
console.error('API error:', error);
showErrorMessage(error.message);
}
```
## Auto-Reconnect
Der SSE-Client versucht automatisch, nach 5 Sekunden wieder zu verbinden, wenn die Verbindung abbricht.
## Verwendete Technologien
- **Fetch API** - Für HTTP-Requests
- **EventSource** - Für Server-Sent Events
- **JSDoc** - Für Type Definitions
- **ES6+** - Modern JavaScript (Class, async/await, etc.)

View File

@@ -0,0 +1 @@
empty

View File

@@ -1,43 +0,0 @@
{
"name": "Home Automation",
"short_name": "Home",
"description": "Smart Home Automation System",
"start_url": "/",
"display": "standalone",
"background_color": "#667eea",
"theme_color": "#667eea",
"orientation": "portrait",
"icons": [
{
"src": "/static/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/static/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/static/apple-touch-icon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
]
}

View File

@@ -16,7 +16,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Dashboard"> <meta name="apple-mobile-web-app-title" content="Dashboard">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;

View File

@@ -15,7 +15,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Gerät"> <meta name="apple-mobile-web-app-title" content="Gerät">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -359,6 +358,7 @@
let deviceData = null; let deviceData = null;
let deviceState = {}; let deviceState = {};
let roomName = ''; let roomName = '';
let deviceStateUnknown = false;
// Device type icons // Device type icons
const deviceIcons = { const deviceIcons = {
@@ -381,8 +381,19 @@
// NEW: Use new endpoints for device info and layout // NEW: Use new endpoints for device info and layout
deviceData = await window.apiClient.getDevice(deviceId); deviceData = await window.apiClient.getDevice(deviceId);
console.log("Loaded device data:", deviceData); console.log("Loaded device data:", deviceData);
deviceState = await window.apiClient.getDeviceState(deviceId);
console.log("Loaded device state:", deviceState); try {
deviceState = await window.apiClient.getDeviceState(deviceId);
console.log("Loaded device state:", deviceState);
if (!deviceState || Object.keys(deviceState).length === 0) {
deviceStateUnknown = true;
deviceState = {};
}
} catch (stateError) {
console.warn('No state for device, using unknown state:', stateError);
deviceStateUnknown = true;
deviceState = {};
}
const layoutInfo = await window.apiClient.getDeviceLayout(deviceId); const layoutInfo = await window.apiClient.getDeviceLayout(deviceId);
console.log("Loaded layout info:", layoutInfo); console.log("Loaded layout info:", layoutInfo);
roomName = layoutInfo.room; roomName = layoutInfo.room;
@@ -518,6 +529,14 @@
}, 0); }, 0);
} }
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card); container.appendChild(card);
} }
@@ -553,6 +572,14 @@
`; `;
card.appendChild(sliderGroup); card.appendChild(sliderGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card); container.appendChild(card);
setTimeout(() => { setTimeout(() => {
@@ -581,6 +608,14 @@
powerGroup.appendChild(powerButton); powerGroup.appendChild(powerButton);
card.appendChild(powerGroup); card.appendChild(powerGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card); container.appendChild(card);
} }
@@ -599,6 +634,14 @@
`; `;
card.appendChild(statusDiv); card.appendChild(statusDiv);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card); container.appendChild(card);
} }

View File

@@ -16,7 +16,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Garage"> <meta name="apple-mobile-web-app-title" content="Garage">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;

View File

@@ -16,7 +16,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Home Automation"> <meta name="apple-mobile-web-app-title" content="Home Automation">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;

View File

@@ -15,7 +15,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="{{ room_name }}"> <meta name="apple-mobile-web-app-title" content="{{ room_name }}">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;

View File

@@ -16,7 +16,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Räume"> <meta name="apple-mobile-web-app-title" content="Räume">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<link rel="manifest" href="{{ STATIC_BASE }}/manifest.json">
<style> <style>
* { * {
margin: 0; margin: 0;

View File

@@ -241,22 +241,6 @@ devices:
ieee_address: "0x0017880108a03e45" ieee_address: "0x0017880108a03e45"
model: "929002241201" model: "929002241201"
vendor: "Philips" vendor: "Philips"
- device_id: haustuer
name: Haustür-Lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xec1bbdfffea6a3da"
set: "zigbee2mqtt/0xec1bbdfffea6a3da/set"
metadata:
friendly_name: "Haustür"
ieee_address: "0xec1bbdfffea6a3da"
model: "LED1842G3"
vendor: "IKEA"
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
name: Deckenlampe oben name: Deckenlampe oben
type: light type: light
@@ -730,8 +714,19 @@ devices:
features: features:
power: true power: true
topics: topics:
set: "shellies/LightKitchenSink/relay/0/command" set: "shellies/shellyplug-s-DED4E4/relay/0/command"
state: "shellies/LightKitchenSink/relay/0" state: "shellies/shellyplug-s-DED4E4/relay/0"
- device_id: putzlicht_kueche
name: Putzlicht
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xa4c138563834406c"
set: "zigbee2mqtt/0xa4c138563834406c/set"
- device_id: licht_schrank_esszimmer - device_id: licht_schrank_esszimmer
name: Schrank name: Schrank
type: relay type: relay
@@ -772,24 +767,128 @@ devices:
topics: topics:
set: "shellies/lichtterasse/relay/0/command" set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0" state: "shellies/lichtterasse/relay/0"
- device_id: kugellampe_patty
name: Kugellampe Patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xbc33acfffe21f547"
set: "zigbee2mqtt/0xbc33acfffe21f547/set"
- device_id: kueche_fensterbank_licht
name: Fensterbank Küche
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b8000017515d"
set: "zigbee2mqtt/0xf0d1b8000017515d/set"
- device_id: licht_kommode_schlafzimmer
name: Kommode Schlafzimmer
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/04/POWER"
state: "stat/tasmota/04/POWER"
- device_id: licht_fensterbank_esszimmer
name: Fensterbank Esszimmer
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/02/POWER"
state: "stat/tasmota/02/POWER"
- device_id: licht_schreibtisch_patty
name: Schreibtisch Patty
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/03/POWER"
state: "stat/tasmota/03/POWER"
- device_id: kugeln_regal_flur
name: Kugeln Regal Flur
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/01/POWER"
state: "stat/tasmota/01/POWER"
- device_id: schrank_flur_haustür
name: Schrank Flur Haustür
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/05/POWER"
state: "stat/tasmota/05/POWER"
- device_id: gartenlicht_vorne
name: Gartenlicht vorne
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/06/POWER"
state: "stat/tasmota/06/POWER"
- device_id: power_relay_caroutlet - device_id: power_relay_caroutlet
name: Car Outlet name: Car Outlet
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
technology: hottis_modbus technology: hottis_pv_modbus
features: features:
power: true power: true
topics: topics:
set: "caroutlet/cmd" set: "IoT/Car/Control"
state: "caroutlet/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
cap_version: "three_phase_powermeter@1.0.0" cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_modbus technology: hottis_pv_modbus
topics: topics:
state: "caroutlet/powermeter" state: "IoT/Car/Values"
- device_id: schranklicht_flur_vor_kueche
name: Schranklicht Flur vor Küche
type: light
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
topics:
state: "zigbee2mqtt/0xf0d1b80000155a1f"
set: "zigbee2mqtt/0xf0d1b80000155a1f/set"
- device_id: deckenlampe_wohnzimmer
name: Deckenlampe Wohnzimmer
type: light
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x842e14fffea72027"
set: "zigbee2mqtt/0x842e14fffea72027/set"

View File

@@ -17,6 +17,10 @@ rooms:
title: Medusa-Lampe Schlafzimmer title: Medusa-Lampe Schlafzimmer
icon: 💡 icon: 💡
rank: 40 rank: 40
- device_id: licht_kommode_schlafzimmer
title: Kommode Schlafzimmer
icon: 💡
rank: 42
- device_id: thermostat_schlafzimmer - device_id: thermostat_schlafzimmer
title: Thermostat Schlafzimmer title: Thermostat Schlafzimmer
icon: 🌡️ icon: 🌡️
@@ -39,10 +43,10 @@ rooms:
title: Leselampe Esszimmer title: Leselampe Esszimmer
icon: 💡 icon: 💡
rank: 60 rank: 60
# - device_id: standlampe_esszimmer - device_id: licht_fensterbank_esszimmer
# title: Standlampe Esszimmer title: Fensterbank Esszimmer
# icon: 💡 icon: 💡
# rank: 70 rank: 70
- device_id: kleine_lampe_links_esszimmer - device_id: kleine_lampe_links_esszimmer
title: kleine Lampe links Esszimmer title: kleine Lampe links Esszimmer
icon: 💡 icon: 💡
@@ -97,6 +101,10 @@ rooms:
title: Regallicht Wohnzimmer title: Regallicht Wohnzimmer
icon: 💡 icon: 💡
rank: 132 rank: 132
- device_id: deckenlampe_wohnzimmer
title: Deckenlampe Wohnzimmer
icon: 💡
rank: 133
- device_id: thermostat_wohnzimmer - device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer title: Thermostat Wohnzimmer
icon: 🌡️ icon: 🌡️
@@ -123,6 +131,14 @@ rooms:
title: Küche Spüle title: Küche Spüle
icon: 💡 icon: 💡
rank: 142 rank: 142
- device_id: putzlicht_kueche
title: Küche Putzlicht
icon: 💡
rank: 143
- device_id: kueche_fensterbank_licht
title: Küche Fensterbank
icon: 💡
rank: 144
- device_id: thermostat_kueche - device_id: thermostat_kueche
title: Kueche title: Kueche
icon: 🌡️ icon: 🌡️
@@ -161,6 +177,14 @@ rooms:
title: Schranklicht vorne Patty title: Schranklicht vorne Patty
icon: 💡 icon: 💡
rank: 180 rank: 180
- device_id: kugellampe_patty
title: Kugellampe Patty
icon: 💡
rank: 181
- device_id: licht_schreibtisch_patty
title: Licht Schreibtisch Patty
icon: 💡
rank: 182
- device_id: thermostat_patty - device_id: thermostat_patty
title: Thermostat Patty title: Thermostat Patty
icon: 🌡️ icon: 🌡️
@@ -205,18 +229,22 @@ rooms:
title: Deckenlampe Flur oben title: Deckenlampe Flur oben
icon: 💡 icon: 💡
rank: 210 rank: 210
- device_id: haustuer - device_id: kugeln_regal_flur
title: Haustür title: Kugeln Regal
icon: 💡
rank: 220
- device_id: licht_flur_schrank
title: Schranklicht Flur
icon: 💡 icon: 💡
rank: 222 rank: 222
- device_id: licht_flur_oben_am_spiegel - device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel title: Licht oben am Spiegel
icon: 💡 icon: 💡
rank: 230 rank: 230
- device_id: schrank_flur_haustür
title: Schranklicht an der Haustür
icon: 💡
rank: 231
- device_id: schranklicht_flur_vor_kueche
title: Schranklicht vor Küche
icon: 💡
rank: 232
- device_id: sensor_flur - device_id: sensor_flur
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
@@ -279,6 +307,10 @@ rooms:
title: Licht Terasse title: Licht Terasse
icon: 💡 icon: 💡
rank: 290 rank: 290
- device_id: gartenlicht_vorne
title: Gartenlicht vorne
icon: 💡
rank: 291
- name: Garage - name: Garage
devices: devices:
- device_id: power_relay_caroutlet - device_id: power_relay_caroutlet

View File

@@ -14,7 +14,7 @@ data:
# UI specific environment variables # UI specific environment variables
UI_UI_PORT: "8002" UI_UI_PORT: "8002"
UI_API_BASE: "https://homea2-api.hottis.de" UI_API_BASE: "https://homea2-api.hottis.de"
UI_STATIC_BASE: "http://homea2-static.hottis.de" UI_STATIC_BASE: "https://homea2-static.hottis.de"
UI_BASE_PATH: "/" UI_BASE_PATH: "/"

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pulsegen
namespace: homea2
labels:
app: pulsegen
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: pulsegen
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment"
labels:
app: pulsegen
component: home-automation
spec:
containers:
- name: pulsegen
image: %IMAGE%
env:
- name: MQTT_BROKER
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_BROKER
- name: MQTT_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_PORT
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 256Mi
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "ps aux | grep -v grep | grep python"
initialDelaySeconds: 30
periodSeconds: 10

View File

@@ -37,6 +37,11 @@ spec:
configMapKeyRef: configMapKeyRef:
name: home-automation-environment name: home-automation-environment
key: UI_API_BASE key: UI_API_BASE
- name: STATIC_BASE
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_STATIC_BASE
- name: BASE_PATH - name: BASE_PATH
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef: