refactored
This commit is contained in:
@@ -326,46 +326,18 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
last_activity = asyncio.get_event_loop().time()
|
||||
topic = str(message.topic)
|
||||
payload_str = message.payload.decode()
|
||||
logger.debug(f"MQTT message received on {topic}: {payload_str}")
|
||||
|
||||
# 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
|
||||
|
||||
# Check for Shelly relay (also sends plain text)
|
||||
is_shelly_relay = False
|
||||
shelly_device_id = None
|
||||
shelly_device_type = None
|
||||
for device_id, device in devices.items():
|
||||
if device.get("technology") == "shelly" and device.get("type") == "relay":
|
||||
if topic == device["topics"]["state"]:
|
||||
is_shelly_relay = True
|
||||
shelly_device_id = device_id
|
||||
shelly_device_type = device["type"]
|
||||
break
|
||||
|
||||
# Parse payload based on device technology
|
||||
if is_max_device or is_shelly_relay:
|
||||
# MAX! and Shelly send plain text, not JSON
|
||||
payload = payload_str.strip()
|
||||
else:
|
||||
# All other technologies use JSON
|
||||
# Check if this is an abstract SET message
|
||||
if topic.startswith("home/") and topic.endswith("/set"):
|
||||
|
||||
# abstract messages should have json payload
|
||||
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"):
|
||||
|
||||
# Extract device_type and device_id from topic
|
||||
parts = topic.split("/")
|
||||
if len(parts) == 4: # home/<type>/<id>/set
|
||||
@@ -382,32 +354,15 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
|
||||
# Check if this is a vendor STATE message
|
||||
else:
|
||||
# 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
|
||||
)
|
||||
# For Shelly relay devices, we already identified them above
|
||||
elif is_shelly_relay:
|
||||
device = devices[shelly_device_id]
|
||||
device_technology = device.get("technology", "unknown")
|
||||
await handle_vendor_state(
|
||||
client, redis_client, shelly_device_id, shelly_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
|
||||
# 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_str, redis_channel
|
||||
)
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("MQTT worker cancelled")
|
||||
|
||||
@@ -7,6 +7,7 @@ This module implements a registry-pattern for vendor-specific transformations:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,11 +25,14 @@ def _transform_light_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, A
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_light_simulator_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform simulator light payload to abstract format.
|
||||
|
||||
Simulator uses same format as abstract protocol (no transformation needed).
|
||||
"""
|
||||
|
||||
payload = json.loads(payload)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@@ -40,11 +44,14 @@ def _transform_thermostat_simulator_to_vendor(payload: dict[str, Any]) -> dict[s
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_thermostat_simulator_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform simulator thermostat payload to abstract format.
|
||||
|
||||
Simulator uses same format as abstract protocol (no transformation needed).
|
||||
"""
|
||||
|
||||
payload = json.loads(payload)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@@ -80,7 +87,7 @@ def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str,
|
||||
return vendor_payload
|
||||
|
||||
|
||||
def _transform_light_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform zigbee2mqtt light payload to abstract format.
|
||||
|
||||
Transformations:
|
||||
@@ -91,7 +98,7 @@ def _transform_light_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[st
|
||||
- zigbee2mqtt: {'state': 'ON', 'brightness': 254}
|
||||
- Abstract: {'power': 'on', 'brightness': 100}
|
||||
"""
|
||||
abstract_payload = payload.copy()
|
||||
abstract_payload = json.loads(payload)
|
||||
|
||||
# Transform state -> power with lowercase values
|
||||
if "state" in abstract_payload:
|
||||
@@ -128,7 +135,7 @@ def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict
|
||||
return vendor_payload
|
||||
|
||||
|
||||
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform zigbee2mqtt thermostat payload to abstract format.
|
||||
|
||||
Transformations:
|
||||
@@ -140,6 +147,7 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> di
|
||||
- zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'}
|
||||
- Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'}
|
||||
"""
|
||||
payload = json.loads(payload)
|
||||
abstract_payload = {}
|
||||
|
||||
# Extract target temperature
|
||||
@@ -173,7 +181,7 @@ def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) ->
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform zigbee2mqtt contact sensor payload to abstract format.
|
||||
|
||||
Transformations:
|
||||
@@ -188,6 +196,7 @@ def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -
|
||||
- zigbee2mqtt: {"contact": false, "battery": 100, "linkquality": 87}
|
||||
- Abstract: {"contact": "open", "battery": 100, "linkquality": 87}
|
||||
"""
|
||||
payload = json.loads(payload)
|
||||
abstract_payload = {}
|
||||
|
||||
# Transform contact state (inverted logic!)
|
||||
@@ -226,7 +235,7 @@ def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform MAX! (Homegear) contact sensor payload to abstract format.
|
||||
|
||||
MAX! sends "true"/"false" (string or bool) on STATE topic.
|
||||
@@ -240,19 +249,7 @@ def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, An
|
||||
- Abstract: {"contact": "open"}
|
||||
"""
|
||||
try:
|
||||
# Handle string, bool, or dict input
|
||||
if isinstance(payload, dict):
|
||||
# If already a dict, extract contact field
|
||||
contact_value = payload.get("contact", False)
|
||||
elif isinstance(payload, str):
|
||||
# Parse string to bool
|
||||
contact_value = payload.strip().lower() == "true"
|
||||
elif isinstance(payload, bool):
|
||||
# Use bool directly
|
||||
contact_value = payload
|
||||
else:
|
||||
logger.warning(f"MAX! contact sensor unexpected payload type: {type(payload)}, value: {payload}")
|
||||
contact_value = False
|
||||
contact_value = payload.strip().lower() == "true"
|
||||
|
||||
# MAX! semantics: True = OPEN, False = CLOSED
|
||||
return {
|
||||
@@ -278,11 +275,12 @@ def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform zigbee2mqtt temp/humidity sensor payload to abstract format.
|
||||
|
||||
Passthrough - zigbee2mqtt provides temperature, humidity, battery, linkquality directly.
|
||||
"""
|
||||
payload = json.loads(payload)
|
||||
return payload
|
||||
|
||||
|
||||
@@ -290,20 +288,24 @@ def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: dict[str, A
|
||||
# HANDLER FUNCTIONS: temp_humidity_sensor - MAX! technology
|
||||
# ============================================================================
|
||||
|
||||
def _transform_temp_humidity_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
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: dict[str, Any]) -> dict[str, Any]:
|
||||
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
|
||||
|
||||
|
||||
@@ -326,12 +328,13 @@ def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str,
|
||||
return vendor_payload
|
||||
|
||||
|
||||
def _transform_relay_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _transform_relay_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform zigbee2mqtt relay payload to abstract format.
|
||||
|
||||
Relay only has power on/off, same transformation as light.
|
||||
- state: 'ON'/'OFF' -> power: 'on'/'off'
|
||||
"""
|
||||
payload = json.loads(payload)
|
||||
abstract_payload = payload.copy()
|
||||
|
||||
if "state" in abstract_payload:
|
||||
@@ -369,12 +372,7 @@ def _transform_relay_shelly_to_abstract(payload: str) -> dict[str, Any]:
|
||||
- Shelly: 'on'
|
||||
- Abstract: {'power': 'on'}
|
||||
"""
|
||||
# Shelly payload is a plain string, not a dict
|
||||
if isinstance(payload, str):
|
||||
return {"power": payload.strip()}
|
||||
|
||||
# Fallback if it's already a dict (shouldn't happen)
|
||||
return payload
|
||||
return {"power": payload.strip()}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -412,7 +410,7 @@ def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
|
||||
return "21"
|
||||
|
||||
|
||||
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]:
|
||||
def _transform_thermostat_max_to_abstract(payload: str) -> dict[str, Any]:
|
||||
"""Transform MAX! (Homegear) thermostat payload to abstract format.
|
||||
|
||||
MAX! sends only the integer temperature value (no JSON).
|
||||
@@ -428,26 +426,14 @@ def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[st
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
# Handle both string and numeric input
|
||||
target_temp = float(payload.strip())
|
||||
|
||||
return {
|
||||
"target": target_temp,
|
||||
"mode": "heat" # MAX! is always in heating mode
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -564,7 +550,7 @@ def transform_abstract_to_vendor(
|
||||
def transform_vendor_to_abstract(
|
||||
device_type: str,
|
||||
device_technology: str,
|
||||
vendor_payload: dict[str, Any]
|
||||
vendor_payload: str
|
||||
) -> dict[str, Any]:
|
||||
"""Transform vendor-specific payload to abstract format.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user