transformation added

This commit is contained in:
2025-11-09 12:59:15 +01:00
parent 8fd0921a08
commit 1eff8a2044
3 changed files with 121 additions and 16 deletions

View File

@@ -16,10 +16,14 @@ from aiomqtt import Client
from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState
from apps.abstraction.transformation import (
transform_abstract_to_vendor,
transform_vendor_to_abstract
)
# Configure logging
logging.basicConfig(
level=logging.INFO,
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
@@ -127,6 +131,7 @@ async def handle_abstract_set(
mqtt_client: Client,
device_id: str,
device_type: str,
device_technology: str,
vendor_topic: str,
payload: dict[str, Any]
) -> None:
@@ -136,21 +141,22 @@ async def handle_abstract_set(
mqtt_client: MQTT client instance
device_id: Device identifier
device_type: Device type (e.g., 'light', 'thermostat')
device_technology: Technology identifier (e.g., 'zigbee2mqtt')
vendor_topic: Vendor-specific SET topic
payload: Message payload
"""
# Extract actual payload (remove type wrapper if present)
vendor_payload = payload.get("payload", payload)
abstract_payload = payload.get("payload", payload)
# Validate payload based on device type
try:
if device_type == "light":
# Validate light SET payload (power and/or brightness)
LightState.model_validate(vendor_payload)
LightState.model_validate(abstract_payload)
elif device_type == "thermostat":
# For thermostat SET: only allow mode and target fields
allowed_set_fields = {"mode", "target"}
invalid_fields = set(vendor_payload.keys()) - allowed_set_fields
invalid_fields = set(abstract_payload.keys()) - allowed_set_fields
if invalid_fields:
logger.warning(
f"Thermostat SET {device_id} contains invalid fields {invalid_fields}, "
@@ -159,11 +165,14 @@ async def handle_abstract_set(
return
# Validate against ThermostatState (current/battery/window_open are optional)
ThermostatState.model_validate(vendor_payload)
ThermostatState.model_validate(abstract_payload)
except ValidationError as e:
logger.error(f"Validation failed for {device_type} SET {device_id}: {e}")
return
# Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
vendor_message = json.dumps(vendor_payload)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_message}")
@@ -175,6 +184,7 @@ async def handle_vendor_state(
redis_client: aioredis.Redis,
device_id: str,
device_type: str,
device_technology: str,
payload: dict[str, Any],
redis_channel: str = "ui:updates"
) -> None:
@@ -185,23 +195,27 @@ async def handle_vendor_state(
redis_client: Redis client instance
device_id: Device identifier
device_type: Device type (e.g., 'light', 'thermostat')
payload: State payload
device_technology: Technology identifier (e.g., 'zigbee2mqtt')
payload: State payload (vendor-specific format)
redis_channel: Redis channel for UI updates
"""
# Transform vendor-specific payload to abstract format
abstract_payload = transform_vendor_to_abstract(device_type, device_technology, payload)
# Validate state payload based on device type
try:
if device_type == "light":
LightState.model_validate(payload)
LightState.model_validate(abstract_payload)
elif device_type == "thermostat":
# Validate thermostat state: mode, target, current (required), battery, window_open
ThermostatState.model_validate(payload)
ThermostatState.model_validate(abstract_payload)
except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return
# Publish to abstract state topic (retained)
abstract_topic = f"home/{device_type}/{device_id}/state"
abstract_message = json.dumps(payload)
abstract_message = json.dumps(abstract_payload)
logger.info(f"← abstract STATE {device_id}: {abstract_topic}{abstract_message}")
await mqtt_client.publish(abstract_topic, abstract_message, qos=1, retain=True)
@@ -210,7 +224,7 @@ async def handle_vendor_state(
ui_update = {
"type": "state",
"device_id": device_id,
"payload": payload,
"payload": abstract_payload,
"ts": datetime.now(timezone.utc).isoformat()
}
redis_message = json.dumps(ui_update)
@@ -297,8 +311,9 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
if device_id in devices:
device = devices[device_id]
vendor_topic = device["topics"]["set"]
device_technology = device.get("technology", "unknown")
await handle_abstract_set(
client, device_id, device_type, vendor_topic, payload
client, device_id, device_type, device_technology, vendor_topic, payload
)
# Check if this is a vendor STATE message
@@ -306,8 +321,10 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
# Find device by vendor state topic
for device_id, device in devices.items():
if topic == device["topics"]["state"]:
device_technology = device.get("technology", "unknown")
await handle_vendor_state(
client, redis_client, device_id, device["type"], payload, redis_channel
client, redis_client, device_id, device["type"],
device_technology, payload, redis_channel
)
break

View File

@@ -0,0 +1,88 @@
"""Payload transformation functions for vendor-specific device communication.
This module provides transformation functions to translate between abstract
home protocol payloads and vendor-specific device payloads.
"""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_abstract_to_vendor(
device_type: str,
device_technology: str,
abstract_payload: dict[str, Any]
) -> dict[str, Any]:
"""Transform abstract payload to vendor-specific payload for SET commands.
This function allows technology-specific transformations when sending commands
to devices. For example, different vendors might use different field names or
value formats for the same abstract concept.
Args:
device_type: Type of device (e.g., 'light', 'thermostat')
device_technology: Technology identifier (e.g., 'zigbee2mqtt', 'tasmota')
abstract_payload: Abstract payload following home protocol
Returns:
Vendor-specific payload for the device
Example:
Input: {'power': 'on', 'brightness': 75}
Output: {'state': 'ON', 'brightness': 75} # hypothetical vendor format
"""
logger.debug(
f"transform_abstract_to_vendor IN: type={device_type}, tech={device_technology}, "
f"payload={abstract_payload}"
)
# TODO: Implement technology-specific transformations here
# Currently pass-through: return payload unchanged
vendor_payload = abstract_payload
logger.debug(
f"transform_abstract_to_vendor OUT: type={device_type}, tech={device_technology}, "
f"payload={vendor_payload}"
)
return vendor_payload
def transform_vendor_to_abstract(
device_type: str,
device_technology: str,
vendor_payload: dict[str, Any]
) -> dict[str, Any]:
"""Transform vendor-specific payload to abstract payload for STATE messages.
This function allows technology-specific transformations when receiving state
updates from devices. For example, different vendors might report state using
different field names or value formats.
Args:
device_type: Type of device (e.g., 'light', 'thermostat')
device_technology: Technology identifier (e.g., 'zigbee2mqtt', 'tasmota')
vendor_payload: Vendor-specific payload from the device
Returns:
Abstract payload following home protocol
Example:
Input: {'state': 'ON', 'brightness': 75} # hypothetical vendor format
Output: {'power': 'on', 'brightness': 75}
"""
logger.debug(
f"transform_vendor_to_abstract IN: type={device_type}, tech={device_technology}, "
f"payload={vendor_payload}"
)
# TODO: Implement technology-specific transformations here
# Currently pass-through: return payload unchanged
abstract_payload = vendor_payload
logger.debug(
f"transform_vendor_to_abstract OUT: type={device_type}, tech={device_technology}, "
f"payload={abstract_payload}"
)
return abstract_payload

View File

@@ -16,7 +16,7 @@ devices:
- device_id: test_lampe_1
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
technology: simulator
features:
power: true
brightness: true
@@ -26,7 +26,7 @@ devices:
- device_id: test_lampe_2
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
technology: simulator
features:
power: true
topics:
@@ -35,7 +35,7 @@ devices:
- device_id: test_lampe_3
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
technology: simulator
features:
power: true
brightness: true
@@ -45,7 +45,7 @@ devices:
- device_id: test_thermo_1
type: thermostat
cap_version: "thermostat@2.0.0"
technology: zigbee2mqtt
technology: simulator
features:
mode: false
target: true