Compare commits

...

34 Commits
0.4.1 ... 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
24 changed files with 681 additions and 231 deletions

View File

@@ -11,6 +11,8 @@ matrix:
- abstraction
- rules
- static
- pulsegen
- homekit
steps:
build-${APP}:

View File

@@ -1,6 +1,9 @@
when:
event: [tag]
depends_on:
- namespace
steps:
apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4

View File

@@ -4,7 +4,6 @@ when:
exclude:
- refs/tags/*-configchange
depends_on:
- build
- namespace
@@ -17,6 +16,7 @@ matrix:
- abstraction
- rules
- static
- pulsegen
steps:
deploy-${APP}:

View File

@@ -4,7 +4,6 @@ when:
exclude:
- refs/tags/*-configchange
depends_on:
- deploy

View File

@@ -1,8 +1,5 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
steps:
create_namespace:

View File

@@ -181,16 +181,9 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats and Shelly relays, vendor_payload is a plain string
# For other devices, it's a dict that needs JSON encoding
if (device_technology == "max" and device_type == "thermostat") or \
(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)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_payload}")
logger.debug(f"MQTT message published on {vendor_topic}: {vendor_payload}")
await mqtt_client.publish(vendor_topic, vendor_payload, qos=1)
async def handle_vendor_state(

View File

@@ -17,12 +17,12 @@ logger = logging.getLogger(__name__)
# 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.
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]:
@@ -36,12 +36,12 @@ def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
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.
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]:
@@ -59,7 +59,7 @@ def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
# 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.
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)
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]:
@@ -115,7 +115,7 @@ def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
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.
Transformations:
@@ -132,7 +132,7 @@ def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict
# zigbee2mqtt expects current_heating_setpoint as string
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]:
@@ -171,14 +171,14 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any
# 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.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
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]:
@@ -225,14 +225,14 @@ def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str,
# 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.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
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]:
@@ -266,13 +266,13 @@ def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
# 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.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
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]:
@@ -284,36 +284,13 @@ def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dic
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
# ============================================================================
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.
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")
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]:
@@ -375,10 +352,40 @@ def _transform_relay_shelly_to_abstract(payload: str) -> dict[str, Any]:
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.
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
def _transform_relay_hottis_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]:
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()}
# ============================================================================
# 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]:
"""Transform abstract three_phase_powermeter payload to hottis_modbus format.
def _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract three_phase_powermeter payload to hottis_pv_modbus format.
energy: float = Field(..., description="Total energy in kWh")
total_power: float = Field(..., description="Total power in W")
phase1_power: float = Field(..., description="Power for phase 1 in W")
@@ -441,32 +454,53 @@ def _transform_three_phase_powermeter_hottis_modbus_to_vendor(payload: dict[str,
"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]:
"""Transform hottis_modbus three_phase_powermeter payload to abstract format.
def _transform_three_phase_powermeter_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_pv_modbus three_phase_powermeter payload to abstract format.
Transformations:
- Direct mapping of all power meter fields
Example:
- hottis_modbus: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
- Abstract: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
- Map vendor field names to abstract field names
- totalImportEnergy -> energy
- powerL1/powerL2/powerL3 -> phase1_power/phase2_power/phase3_power
- voltageL1/voltageL2/voltageL3 -> phase1_voltage/phase2_voltage/phase3_voltage
- 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 = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
"energy": energy,
"total_power": phase1_power + phase2_power + phase3_power,
"phase1_power": phase1_power,
"phase2_power": phase2_power,
"phase3_power": phase3_power,
"phase1_voltage": phase1_voltage,
"phase2_voltage": phase2_voltage,
"phase3_voltage": phase3_voltage,
"phase1_current": phase1_current,
"phase2_current": phase2_current,
"phase3_current": phase3_current,
}
return abstract_payload
@@ -567,24 +601,22 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
# 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_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_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", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor,
("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract,
("relay", "shelly", "to_vendor"): _transform_relay_shelly_to_vendor,
("relay", "shelly", "to_abstract"): _transform_relay_shelly_to_abstract,
("relay", "hottis_modbus", "to_vendor"): _transform_relay_hottis_modbus_to_vendor,
("relay", "hottis_modbus", "to_abstract"): _transform_relay_hottis_modbus_to_abstract,
("relay", "hottis_pv_modbus", "to_vendor"): _transform_relay_hottis_pv_modbus_to_vendor,
("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", "hottis_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_modbus_to_vendor,
("three_phase_powermeter", "hottis_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_modbus_to_abstract,
("three_phase_powermeter", "hottis_pv_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor,
("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
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.
@@ -22,9 +22,8 @@ class ContactAccessory(Accessory):
driver: HAP driver instance
device: Device object from DeviceRegistry
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)
self.device = device
self.api_client = api_client

View File

@@ -16,7 +16,7 @@ class OnOffLightAccessory(Accessory):
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.
@@ -24,9 +24,8 @@ class OnOffLightAccessory(Accessory):
driver: HAP driver instance
device: Device object from DeviceRegistry
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)
self.device = device
self.api_client = api_client
@@ -57,9 +56,9 @@ class OnOffLightAccessory(Accessory):
class DimmableLightAccessory(OnOffLightAccessory):
"""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
name = display_name or device.friendly_name or device.name
name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
@@ -106,9 +105,9 @@ class DimmableLightAccessory(OnOffLightAccessory):
class ColorLightAccessory(DimmableLightAccessory):
"""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
name = display_name or device.friendly_name or device.name
name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class OutletAccessory(Accessory):
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.
@@ -23,9 +23,8 @@ class OutletAccessory(Accessory):
driver: HAP driver instance
device: Device object from DeviceRegistry
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)
self.device = device
self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class TempHumidityAccessory(Accessory):
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.
@@ -23,9 +23,8 @@ class TempHumidityAccessory(Accessory):
driver: HAP driver instance
device: Device object from DeviceRegistry
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)
self.device = device
self.api_client = api_client

View File

@@ -17,7 +17,7 @@ class ThermostatAccessory(Accessory):
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.
@@ -25,9 +25,8 @@ class ThermostatAccessory(Accessory):
driver: HAP driver instance
device: Device object from DeviceRegistry
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)
self.device = device
self.api_client = api_client

View File

@@ -51,25 +51,6 @@ class ApiClient:
logger.error(f"Failed to get devices: {e}")
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:
"""
Get current state of a specific device.

View File

@@ -18,8 +18,6 @@ class Device:
device_id: str
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
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})
read_only: bool # True for sensors that don't accept commands
@@ -50,23 +48,6 @@ class DeviceRegistry:
"""
# Get devices and layout
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
devices = []
@@ -76,9 +57,6 @@ class DeviceRegistry:
logger.warning(f"Device without device_id: {dev_data}")
continue
# Get layout info
room_name, title = layout_map.get(device_id, (None, ''))
# Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '')
read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke']
@@ -86,9 +64,7 @@ class DeviceRegistry:
device = Device(
device_id=device_id,
type=device_type,
name=dev_data.get('name', device_id),
friendly_name=title or dev_data.get('name', device_id),
room=room_name,
name=device_id,
features=dev_data.get('features', {}),
read_only=read_only
)

View File

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

View File

@@ -71,14 +71,9 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
try:
accessory = create_accessory_for_device(device, api_client, driver)
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)
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:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
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")
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):
"""
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
features = device.features
display_name = get_accessory_name(device)
# Light accessories
if device_type == "light":
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"):
return DimmableLightAccessory(driver, device, api_client, display_name=display_name)
return DimmableLightAccessory(driver, device, api_client)
else:
return OnOffLightAccessory(driver, device, api_client, display_name=display_name)
return OnOffLightAccessory(driver, device, api_client)
# Thermostat
elif device_type == "thermostat":
return ThermostatAccessory(driver, device, api_client, display_name=display_name)
return ThermostatAccessory(driver, device, api_client)
# Contact sensor
elif device_type == "contact":
return ContactAccessory(driver, device, api_client, display_name=display_name)
return ContactAccessory(driver, device, api_client)
# Temperature/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
elif device_type == "relay":
return OutletAccessory(driver, device, api_client, display_name=display_name)
return OutletAccessory(driver, device, api_client)
# Cover/Blinds (optional)
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

@@ -241,22 +241,6 @@ devices:
ieee_address: "0x0017880108a03e45"
model: "929002241201"
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
name: Deckenlampe oben
type: light
@@ -730,8 +714,19 @@ devices:
features:
power: true
topics:
set: "shellies/LightKitchenSink/relay/0/command"
state: "shellies/LightKitchenSink/relay/0"
set: "shellies/shellyplug-s-DED4E4/relay/0/command"
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
name: Schrank
type: relay
@@ -772,24 +767,128 @@ devices:
topics:
set: "shellies/lichtterasse/relay/0/command"
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
name: Car Outlet
type: relay
cap_version: "relay@1.0.0"
technology: hottis_modbus
technology: hottis_pv_modbus
features:
power: true
topics:
set: "caroutlet/cmd"
state: "caroutlet/state"
set: "IoT/Car/Control"
state: "IoT/Car/Control/State"
- device_id: powermeter_caroutlet
name: Car Outlet
type: three_phase_powermeter
cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_modbus
technology: hottis_pv_modbus
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
icon: 💡
rank: 40
- device_id: licht_kommode_schlafzimmer
title: Kommode Schlafzimmer
icon: 💡
rank: 42
- device_id: thermostat_schlafzimmer
title: Thermostat Schlafzimmer
icon: 🌡️
@@ -39,10 +43,10 @@ rooms:
title: Leselampe Esszimmer
icon: 💡
rank: 60
# - device_id: standlampe_esszimmer
# title: Standlampe Esszimmer
# icon: 💡
# rank: 70
- device_id: licht_fensterbank_esszimmer
title: Fensterbank Esszimmer
icon: 💡
rank: 70
- device_id: kleine_lampe_links_esszimmer
title: kleine Lampe links Esszimmer
icon: 💡
@@ -97,6 +101,10 @@ rooms:
title: Regallicht Wohnzimmer
icon: 💡
rank: 132
- device_id: deckenlampe_wohnzimmer
title: Deckenlampe Wohnzimmer
icon: 💡
rank: 133
- device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer
icon: 🌡️
@@ -123,6 +131,14 @@ rooms:
title: Küche Spüle
icon: 💡
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
title: Kueche
icon: 🌡️
@@ -161,6 +177,14 @@ rooms:
title: Schranklicht vorne Patty
icon: 💡
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
title: Thermostat Patty
icon: 🌡️
@@ -205,18 +229,22 @@ rooms:
title: Deckenlampe Flur oben
icon: 💡
rank: 210
- device_id: haustuer
title: Haustür
icon: 💡
rank: 220
- device_id: licht_flur_schrank
title: Schranklicht Flur
- device_id: kugeln_regal_flur
title: Kugeln Regal
icon: 💡
rank: 222
- device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel
title: Licht oben am Spiegel
icon: 💡
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
title: Temperatur & Luftfeuchte
icon: 🌡️
@@ -279,6 +307,10 @@ rooms:
title: Licht Terasse
icon: 💡
rank: 290
- device_id: gartenlicht_vorne
title: Gartenlicht vorne
icon: 💡
rank: 291
- name: Garage
devices:
- device_id: power_relay_caroutlet

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