diff --git a/apps/abstraction/main.py b/apps/abstraction/main.py index 54616df..b79aefc 100644 --- a/apps/abstraction/main.py +++ b/apps/abstraction/main.py @@ -15,7 +15,7 @@ import uuid from aiomqtt import Client from pydantic import ValidationError -from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState +from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState from apps.abstraction.transformation import ( transform_abstract_to_vendor, transform_vendor_to_abstract @@ -154,6 +154,9 @@ async def handle_abstract_set( if device_type == "light": # Validate light SET payload (power and/or brightness) LightState.model_validate(abstract_payload) + elif device_type == "relay": + # Validate relay SET payload (power only) + RelayState.model_validate(abstract_payload) elif device_type == "thermostat": # For thermostat SET: only allow mode and target fields allowed_set_fields = {"mode", "target"} @@ -216,6 +219,8 @@ async def handle_vendor_state( try: if device_type == "light": LightState.model_validate(abstract_payload) + elif device_type == "relay": + RelayState.model_validate(abstract_payload) elif device_type == "thermostat": # Validate thermostat state: mode, target, current (required), battery, window_open ThermostatState.model_validate(abstract_payload) diff --git a/apps/abstraction/transformation.py b/apps/abstraction/transformation.py index 3c56f73..260d8ea 100644 --- a/apps/abstraction/transformation.py +++ b/apps/abstraction/transformation.py @@ -307,6 +307,40 @@ def _transform_temp_humidity_sensor_max_to_abstract(payload: dict[str, Any]) -> return payload +# ============================================================================ +# HANDLER FUNCTIONS: relay - zigbee2mqtt technology +# ============================================================================ + +def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: + """Transform abstract relay payload to zigbee2mqtt format. + + Relay only has power on/off, same transformation as light. + - power: 'on'/'off' -> state: 'ON'/'OFF' + """ + vendor_payload = payload.copy() + + if "power" in vendor_payload: + power_value = vendor_payload.pop("power") + vendor_payload["state"] = power_value.upper() if isinstance(power_value, str) else power_value + + return vendor_payload + + +def _transform_relay_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> 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' + """ + abstract_payload = payload.copy() + + if "state" in abstract_payload: + state_value = abstract_payload.pop("state") + abstract_payload["power"] = state_value.lower() if isinstance(state_value, str) else state_value + + return abstract_payload + + # ============================================================================ # HANDLER FUNCTIONS: max technology (Homegear MAX!) # ============================================================================ @@ -420,6 +454,10 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = { ("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", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor, + ("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract, } diff --git a/apps/api/main.py b/apps/api/main.py index 461c8a7..87c159a 100644 --- a/apps/api/main.py +++ b/apps/api/main.py @@ -20,10 +20,12 @@ from packages.home_capabilities import ( THERMOSTAT_VERSION, CONTACT_SENSOR_VERSION, TEMP_HUMIDITY_SENSOR_VERSION, + RELAY_VERSION, LightState, ThermostatState, ContactState, - TempHumidityState + TempHumidityState, + RelayState ) logger = logging.getLogger(__name__) @@ -148,7 +150,8 @@ async def spec() -> dict[str, dict[str, str]]: "light": LIGHT_VERSION, "thermostat": THERMOSTAT_VERSION, "contact": CONTACT_SENSOR_VERSION, - "temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION + "temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION, + "relay": RELAY_VERSION } } @@ -358,6 +361,14 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid payload for light: {e}" ) + elif request.type == "relay": + try: + RelayState(**request.payload) + except ValidationError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Invalid payload for relay: {e}" + ) elif request.type == "thermostat": try: # For thermostat SET: only allow mode and target diff --git a/apps/ui/templates/dashboard.html b/apps/ui/templates/dashboard.html index a9f4c1c..0a76065 100644 --- a/apps/ui/templates/dashboard.html +++ b/apps/ui/templates/dashboard.html @@ -571,6 +571,8 @@ {% if device.type == "light" %} Light {% if device.features.brightness %}• Dimmbar{% endif %} + {% elif device.type == "relay" %} + Relay {% elif device.type == "thermostat" %} Thermostat {% elif device.type == "contact" or device.type == "contact_sensor" %} @@ -621,6 +623,21 @@ {% endif %} + {% elif device.type == "relay" %} +