MAX transformation added
This commit is contained in:
@@ -173,7 +173,12 @@ async def handle_abstract_set(
|
||||
# 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)
|
||||
# For MAX! thermostats, vendor_payload is a plain string (integer temperature)
|
||||
# For other devices, it's a dict that needs JSON encoding
|
||||
if device_technology == "max" and device_type == "thermostat":
|
||||
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)
|
||||
@@ -294,11 +299,30 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
topic = str(message.topic)
|
||||
payload_str = message.payload.decode()
|
||||
|
||||
try:
|
||||
payload = json.loads(payload_str)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Invalid JSON on {topic}: {payload_str}")
|
||||
continue
|
||||
# Determine if message is from a MAX! device (requires plain text handling)
|
||||
is_max_device = False
|
||||
max_device_id = None
|
||||
max_device_type = None
|
||||
|
||||
# Check if topic matches any MAX! device state topic
|
||||
for device_id, device in devices.items():
|
||||
if device.get("technology") == "max" and topic == device["topics"]["state"]:
|
||||
is_max_device = True
|
||||
max_device_id = device_id
|
||||
max_device_type = device["type"]
|
||||
break
|
||||
|
||||
# Parse payload based on device technology
|
||||
if is_max_device:
|
||||
# MAX! sends plain integer/string, not JSON
|
||||
payload = payload_str.strip()
|
||||
else:
|
||||
# All other technologies use JSON
|
||||
try:
|
||||
payload = json.loads(payload_str)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Invalid JSON on {topic}: {payload_str}")
|
||||
continue
|
||||
|
||||
# Check if this is an abstract SET message
|
||||
if topic.startswith("home/") and topic.endswith("/set"):
|
||||
@@ -318,15 +342,24 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
|
||||
# Check if this is a vendor STATE message
|
||||
else:
|
||||
# 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"],
|
||||
device_technology, payload, redis_channel
|
||||
)
|
||||
break
|
||||
# For MAX! devices, we already identified them above
|
||||
if is_max_device:
|
||||
device = devices[max_device_id]
|
||||
device_technology = device.get("technology", "unknown")
|
||||
await handle_vendor_state(
|
||||
client, redis_client, max_device_id, max_device_type,
|
||||
device_technology, payload, redis_channel
|
||||
)
|
||||
else:
|
||||
# Find device by vendor state topic for other technologies
|
||||
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"],
|
||||
device_technology, payload, redis_channel
|
||||
)
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("MQTT worker cancelled")
|
||||
|
||||
@@ -124,6 +124,79 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> di
|
||||
return payload
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HANDLER FUNCTIONS: max technology (Homegear MAX!)
|
||||
# ============================================================================
|
||||
|
||||
def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
|
||||
"""Transform abstract thermostat payload to MAX! (Homegear) format.
|
||||
|
||||
MAX! expects only the integer temperature value (no JSON).
|
||||
|
||||
Transformations:
|
||||
- Extract 'target' temperature from payload
|
||||
- Convert float to integer (MAX! only accepts integers)
|
||||
- Return as plain string value
|
||||
|
||||
Example:
|
||||
- Abstract: {'mode': 'heat', 'target': 22.5}
|
||||
- MAX!: "22"
|
||||
|
||||
Note: MAX! ignores mode - it's always in heating mode
|
||||
"""
|
||||
if "target" not in payload:
|
||||
logger.warning(f"MAX! thermostat payload missing 'target': {payload}")
|
||||
return "21" # Default fallback
|
||||
|
||||
target_temp = payload["target"]
|
||||
|
||||
# Convert to integer (MAX! protocol requirement)
|
||||
if isinstance(target_temp, (int, float)):
|
||||
int_temp = int(round(target_temp))
|
||||
return str(int_temp)
|
||||
|
||||
logger.warning(f"MAX! invalid target temperature type: {type(target_temp)}, value: {target_temp}")
|
||||
return "21"
|
||||
|
||||
|
||||
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]:
|
||||
"""Transform MAX! (Homegear) thermostat payload to abstract format.
|
||||
|
||||
MAX! sends only the integer temperature value (no JSON).
|
||||
|
||||
Transformations:
|
||||
- Parse plain string/int value
|
||||
- Convert to float for abstract protocol
|
||||
- Wrap in abstract payload structure with mode='heat'
|
||||
|
||||
Example:
|
||||
- MAX!: "22" or 22
|
||||
- Abstract: {'target': 22.0, 'mode': 'heat'}
|
||||
|
||||
Note: MAX! doesn't send current temperature via SET_TEMPERATURE topic
|
||||
"""
|
||||
try:
|
||||
# Handle both string and numeric input
|
||||
if isinstance(payload, str):
|
||||
target_temp = float(payload.strip())
|
||||
elif isinstance(payload, (int, float)):
|
||||
target_temp = float(payload)
|
||||
else:
|
||||
logger.warning(f"MAX! unexpected payload type: {type(payload)}, value: {payload}")
|
||||
target_temp = 21.0
|
||||
|
||||
return {
|
||||
"target": target_temp,
|
||||
"mode": "heat" # MAX! is always in heating mode
|
||||
}
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"MAX! failed to parse temperature: {payload}, error: {e}")
|
||||
return {
|
||||
"target": 21.0,
|
||||
"mode": "heat"
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# REGISTRY: Maps (device_type, technology, direction) -> handler function
|
||||
# ============================================================================
|
||||
@@ -142,6 +215,8 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
|
||||
("thermostat", "simulator", "to_abstract"): _transform_thermostat_simulator_to_abstract,
|
||||
("thermostat", "zigbee2mqtt", "to_vendor"): _transform_thermostat_zigbee2mqtt_to_vendor,
|
||||
("thermostat", "zigbee2mqtt", "to_abstract"): _transform_thermostat_zigbee2mqtt_to_abstract,
|
||||
("thermostat", "max", "to_vendor"): _transform_thermostat_max_to_vendor,
|
||||
("thermostat", "max", "to_abstract"): _transform_thermostat_max_to_abstract,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user