Compare commits

...

17 Commits

Author SHA1 Message Date
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
11 changed files with 185 additions and 138 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,7 @@ matrix:
- abstraction - abstraction
- rules - rules
- static - static
- homekit
steps: steps:
build-${APP}: build-${APP}:
@@ -22,8 +26,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:
@@ -26,9 +30,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,10 @@ 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 - hottis_pv_modbus technology
# ============================================================================ # ============================================================================
def _transform_relay_hottis_modbus_to_vendor(payload: dict[str, Any]) -> str: 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 +369,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 +424,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 +571,20 @@ 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,
# 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

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

@@ -358,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 = {
@@ -380,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;
@@ -517,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);
} }
@@ -552,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(() => {
@@ -580,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);
} }
@@ -598,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

@@ -730,8 +730,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
@@ -777,12 +788,12 @@ devices:
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
@@ -790,6 +801,6 @@ devices:
cap_version: "three_phase_powermeter@1.0.0" cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_modbus technology: hottis_modbus
topics: topics:
state: "caroutlet/powermeter" state: "IoT/Car/Values"

View File

@@ -123,6 +123,10 @@ 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: thermostat_kueche - device_id: thermostat_kueche
title: Kueche title: Kueche
icon: 🌡️ icon: 🌡️