transformation added
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
88
apps/abstraction/transformation.py
Normal file
88
apps/abstraction/transformation.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user