Compare commits
17 Commits
0.3.3-conf
...
0.5.5-conf
| Author | SHA1 | Date | |
|---|---|---|---|
|
0ca59896ad
|
|||
|
7858996d0f
|
|||
|
a0f7cc7bd9
|
|||
|
a98802437c
|
|||
|
708e287016
|
|||
|
d11eab8474
|
|||
|
eccffbbd55
|
|||
|
2b963a33ef
|
|||
|
1311f7a59b
|
|||
|
a226fa9268
|
|||
|
3bd8d293a2
|
|||
|
be30ad3a3c
|
|||
|
500384b1cd
|
|||
|
6b4c247413
|
|||
|
04a1807306
|
|||
|
db5e4589d0
|
|||
|
5399f044a1
|
@@ -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
|
|
||||||
|
|||||||
@@ -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]
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
15
.woodpecker/namespace.yml
Normal 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"
|
||||||
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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/Cat/Values"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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: 🌡️
|
||||||
|
|||||||
Reference in New Issue
Block a user