Hue Dimmer Switch controller for Zigbee2MQTT via pyscript

I’m not a huge Blueprint fan, so I made this pyscript.

The script allows you to define a key/value list of switch names with lights to control for the given switch. It handles on/off and brightness controls and has some configuration variables for you to tweak as desired.

Feedback welcome. :+1:

# Zigbee2MQTT - Hue Dimmer Switch Control
# - Listens to MQTT messages on zigbee2mqtt/<device>
# - Maps switch device names to target light entities using SWITCH_TO_LIGHTS dictionary
# - Handles on/off presses and brightness up/down (press for small steps, hold for large steps)
# - Only adjusts brightness when lights are already on
# - Uses action field from Z2M payloads: on_press, off_press, up_press, down_press, up_hold, down_hold

# logger.set_level(
#     **{"custom_components.pyscript.file.zigbee2mqtt_hue_switch_control": "debug"}
# )


# Configuration: Map switch device names to target light entities
# Key: Switch name (from MQTT)
# Value: List of light entity IDs that this switch controls.
SWITCH_TO_LIGHTS = {
    "Office Hue Switch": [
        "light.office_nano_bulb_01",
        "light.office_nano_bulb_02",
        "light.office_hue_lightstrip",
    ],
    "Living Room Hue Switch": ["light.magic_areas_light_groups_living_room_all_lights"],
}

# Brightness settings
BRIGHTNESS_MIN = 10  # Minimum brightness to ensure visibility
BRIGHTNESS_MAX = 255
BRIGHTNESS_STEP = 30  # ~12% of 255, for press actions
BRIGHTNESS_HOLD_STEP = 56  # ~22% of 255, for hold actions
DEFAULT_TRANSITION = 0.2  # Transition time in seconds


# Clamp brightness to valid range
def clamp_brightness(value: int) -> int:
    return max(BRIGHTNESS_MIN, min(int(value), BRIGHTNESS_MAX))


# Returns brightness of first lit entity, or None if no lights are on
def get_current_brightness(target_lights: list[str]) -> int | None:
    for entity in target_lights:
        if state.get(entity) == "on":
            brightness = state.get(f"{entity}.brightness")
            if brightness is not None:
                return int(brightness)
    return None


@mqtt_trigger("zigbee2mqtt/+")
def on_z2m_event(topic=None, payload=None, payload_obj=None, **kwargs):
    log.debug(f"Z2M Event - Topic: {topic}")
    if not topic or not topic.startswith("zigbee2mqtt/"):
        return

    device_name = topic.split("/", 1)[1]

    # Look up target lights from configuration mapping
    target_lights = SWITCH_TO_LIGHTS.get(device_name)
    if target_lights is None:
        log.debug(f"No mapping found for switch '{device_name}', skipping")
        return

    log.debug(f"TARGETING: {target_lights}")

    # Check if any light entity exists
    entity_exists = False
    for entity in target_lights:
        if state.get(entity):
            entity_exists = True
            break
    if not entity_exists:
        log.debug(f"No light entities found in {target_lights}, skipping")
        return

    # Parse payload
    action = (payload_obj or {}).get("action")
    if not action:
        return

    log.debug(f"{device_name} --> Action: {action}")

    # Handle on/off press actions
    if action == "on_press":
        log.debug(f"{device_name} --> TURN ON")
        light.turn_on(entity_id=target_lights)
        return
    elif action == "off_press":
        log.debug(f"{device_name} --> TURN OFF")
        light.turn_off(entity_id=target_lights)
        return

    # Handle brightness adjustment actions (press and hold)
    elif action in ["up_press", "down_press", "up_hold", "down_hold"]:
        current_brightness = get_current_brightness(target_lights)
        if current_brightness is None:
            return

        is_hold = action.endswith("_hold")
        direction = "up" if action.startswith("up") else "down"
        step = BRIGHTNESS_HOLD_STEP if is_hold else BRIGHTNESS_STEP

        delta = step if direction == "up" else -step
        new_brightness = clamp_brightness(current_brightness + delta)

        # Skip redundant write if no change (for press actions only)
        if not is_hold and current_brightness == new_brightness:
            return

        action_type = "move" if is_hold else "step"
        log.debug(
            f"{device_name} --> Brightness {direction} {action_type} -> {new_brightness}"
        )
        light.turn_on(
            entity_id=target_lights,
            brightness=new_brightness,
            transition=DEFAULT_TRANSITION,
        )