from typing import Any, Dict, Optional
from .abstract_zone import AbstractZone, ZoneType
[docs]
class RGB:
"""RGB color representation"""
[docs]
def __init__(self, r: int, g: int, b: int, i: Optional[int] = None):
"""Initialize RGB color with validation."""
if not (0 <= r <= 255):
raise ValueError(f"Red component {r} must be between 0 and 255")
if not (0 <= g <= 255):
raise ValueError(f"Green component {g} must be between 0 and 255")
if not (0 <= b <= 255):
raise ValueError(f"Blue component {b} must be between 0 and 255")
if i is not None and not (0 <= i <= 255):
raise ValueError(f"Intensity component {i} must be between 0 and 255")
self.r = r
self.g = g
self.b = b
self.i = i
[docs]
def model_dump(self) -> Dict[str, Any]:
"""Convert to dictionary (replaces Pydantic's model_dump)."""
result = {"r": self.r, "g": self.g, "b": self.b}
if self.i is not None:
result["i"] = self.i
return result
[docs]
@AbstractZone.register_zone_type(ZoneType.LIGHTING_ZONE)
class LightingZone(AbstractZone):
"""State representation for lighting zone v1 with validation"""
[docs]
def __init__(self, zone_id: str, config: Dict[str, Any]):
"""Initialize LightingZone with zone_id and config."""
# Set default name if not provided
if "name" not in config or config["name"] is None:
config["name"] = f"Light {zone_id}"
super().__init__(
id=zone_id,
zone_type=ZoneType.LIGHTING_ZONE,
name=config.get("name"),
config=config,
)
# Initialize lighting zone specific attributes
self.active: Optional[bool] = getattr(self, "active", None)
self.rgbi: Optional[RGB] = getattr(self, "rgbi", None)
self.effect: Optional[str] = getattr(self, "effect", None)
# Validate effect length if present
if self.effect is not None and self._is_valid_effect_name(self.effect):
self._validate_effect_name(self.effect)
def _validate_effect_name(self, effect: str) -> None:
"""Validate effect name length."""
if len(effect) < 1 or len(effect) > 50:
raise ValueError(
f"Effect name '{effect}' must be between 1 and 50 characters"
)
def _is_valid_effect_name(self, effect: str) -> bool:
"""Check if effect name should be validated."""
return isinstance(effect, str)
[docs]
def get_lighting_state(self) -> Dict[str, Any]:
"""Get the current lighting state as a simple dictionary."""
return {
"active": self.active,
"color": self.rgbi.model_dump() if self.rgbi else None,
"effect": self.effect,
}
[docs]
def set_color(self, r: int, g: int, b: int, i: Optional[int] = None) -> None:
"""Set lighting color."""
rgb_color = RGB(r=r, g=g, b=b, i=i)
self.rgbi = rgb_color
self.active = True
self._publish_desired_state({"rgbi": rgb_color, "active": True})
def _get_runtime_state_fields(self) -> set:
"""Runtime state fields for lighting zones."""
return {"active", "rgbi", "effect"}
def _get_field_mappings(self) -> Dict[str, str]:
"""
Lighting zone specific field mappings.
Returns:
Dictionary mapping external field names to internal field names
"""
return {
"isActive": "active",
"color": "rgbi",
"rgb": "rgbi",
"lightEffect": "effect",
"lightingEffect": "effect",
"mode": "effect",
"running": "active",
"enabled": "active",
"on": "active",
}
[docs]
def update_from_state(self, state: Dict[str, Any]) -> None:
"""Update lighting zone from runtime state with special handling for RGBI."""
# Handle RGBI conversion if it's a list
if "rgbi" in state:
rgbi_value = state["rgbi"]
if isinstance(rgbi_value, list) and len(rgbi_value) >= 3:
# Convert list [r, g, b, i] to RGB object
r, g, b = rgbi_value[0], rgbi_value[1], rgbi_value[2]
i = rgbi_value[3] if len(rgbi_value) > 3 else None
state = state.copy() # Don't modify original
state["rgbi"] = RGB(r=r, g=g, b=b, i=i)
# Call parent update method
super().update_from_state(state)
[docs]
def set_effect(self, effect_name: str) -> None:
"""Set lighting effect with validation."""
self._validate_effect_name(effect_name)
self.effect = effect_name
self.active = True
self._publish_desired_state({"effect": effect_name, "active": True})
[docs]
def activate(self) -> None:
"""Activate this zone."""
self._publish_desired_state({"active": True})
[docs]
def deactivate(self) -> None:
"""Deactivate this zone."""
self._publish_desired_state({"active": False})