"""The EHEIM classicLEDcontrol light controller.""" from __future__ import annotations import json from logging import getLogger from typing import TYPE_CHECKING, Any, override from eheimdigital.device import EheimDigitalDevice from eheimdigital.types import ( AcclimatePacket, CCVPacket, ClockPacket, CloudPacket, EheimDigitalDataMissingError, LightMode, MoonPacket, MsgTitle, UsrDtaPacket, ) if TYPE_CHECKING: from eheimdigital.hub import EheimDigitalHub _LOGGER = getLogger(__package__) class EheimDigitalClassicLEDControl(EheimDigitalDevice): """Represent a EHEIM classicLEDcontrol light controller.""" ccv: CCVPacket | None = None clock: ClockPacket | None = None cloud: CloudPacket | None = None moon: MoonPacket | None = None acclimate: AcclimatePacket | None = None tankconfig: list[list[str]] power: list[list[int]] def __init__(self, hub: EheimDigitalHub, usrdta: UsrDtaPacket) -> None: """Initialize a classicLEDcontrol light controller.""" super().__init__(hub, usrdta) self.tankconfig = json.loads(usrdta["tankconfig"]) self.power = json.loads(usrdta["power"]) @override async def parse_message(self, msg: dict[str, Any]) -> None: """Parse a message.""" match msg["title"]: case MsgTitle.CCV: self.ccv = CCVPacket(**msg) case MsgTitle.CLOUD: self.cloud = CloudPacket(**msg) case MsgTitle.MOON: self.moon = MoonPacket(**msg) case MsgTitle.CLOCK: self.clock = ClockPacket(**msg) case MsgTitle.ACCLIMATE: self.acclimate = AcclimatePacket(**msg) case _: pass @override async def update(self) -> None: """Get the new light state.""" await self.hub.send_packet({ "title": "REQ_CCV", "to": self.mac_address, "from": "USER", }) await self.hub.send_packet({ "title": "GET_CLOCK", "to": self.mac_address, "from": "USER", }) if "moon" not in self.__dict__: await self.hub.send_packet({ "title": "GET_MOON", "to": self.mac_address, "from": "USER", }) if "cloud" not in self.__dict__: await self.hub.send_packet({ "title": "GET_CLOUD", "to": self.mac_address, "from": "USER", }) if "acclimate" not in self.__dict__: await self.hub.send_packet({ "title": "GET_ACCL", "to": self.mac_address, "from": "USER", }) async def set_cloud(self, data: dict[str, Any]) -> None: """Set the cloud data.""" if self.cloud is None: _LOGGER.error("set_cloud: No CLOUD packet received yet.") return await self.hub.send_packet({ "title": MsgTitle.CLOUD, "to": self.mac_address, "from": "USER", "probability": self.cloud["probability"], "maxAmount": self.cloud["maxAmount"], "minIntensity": self.cloud["minIntensity"], "maxIntensity": self.cloud["maxIntensity"], "minDuration": self.cloud["minDuration"], "maxDuration": self.cloud["maxDuration"], "cloudActive": self.cloud["cloudActive"], "mode": self.cloud["mode"], **data, }) async def set_moon(self, data: dict[str, Any]) -> None: """Set the moon data.""" if self.moon is None: _LOGGER.error("set_moon: No MOON packet received yet.") return await self.hub.send_packet({ "title": MsgTitle.MOON, "to": self.mac_address, "from": "USER", "maxmoonlight": self.moon["maxmoonlight"], "minmoonlight": self.moon["minmoonlight"], "moonlightActive": self.moon["moonlightActive"], "moonlightCycle": self.moon["moonlightCycle"], **data, }) async def set_acclimate(self, data: dict[str, Any]) -> None: """Set the acclimate data.""" if self.acclimate is None: _LOGGER.error("set_acclimate: No ACCLIMATE packet received yet.") return await self.hub.send_packet({ "title": MsgTitle.ACCLIMATE, "to": self.mac_address, "from": "USER", "duration": self.acclimate["duration"], "intensityReduction": self.acclimate["intensityReduction"], "currentAcclDay": self.acclimate["currentAcclDay"], "acclActive": self.acclimate["acclActive"], "pause": self.acclimate["pause"], **data, }) @property def light_level(self) -> tuple[int | None, int | None]: """Return the current light level of the channels.""" if self.ccv is None: raise EheimDigitalDataMissingError return ( self.ccv["currentValues"][0] if len(self.tankconfig[0]) > 0 else None, self.ccv["currentValues"][1] if len(self.tankconfig[1]) > 0 else None, ) @property def power_consumption(self) -> tuple[float | None, float | None]: """Return the power consumption of the channels.""" if self.ccv is None: raise EheimDigitalDataMissingError return ( sum(self.power[0]) * self.ccv["currentValues"][0] if len(self.tankconfig[0]) > 0 else None, sum(self.power[1]) * self.ccv["currentValues"][1] if len(self.tankconfig[1]) > 0 else None, ) @property def light_mode(self) -> LightMode: """Return the current light operation mode.""" if self.clock is None or "mode" not in self.clock: raise EheimDigitalDataMissingError return LightMode(self.clock["mode"]) async def set_light_mode(self, mode: LightMode) -> None: """Set the light operation mode.""" await self.hub.send_packet({ "title": str(mode), "to": self.mac_address, "from": "USER", }) async def turn_on(self, value: int, channel: int) -> None: """Set a new brightness value for a channel.""" if self.light_mode == LightMode.DAYCL_MODE: await self.set_light_mode(LightMode.MAN_MODE) if self.ccv is None: return currentvalues = self.ccv["currentValues"] currentvalues[channel] = value await self.hub.send_packet({ "title": "CCV-SL", "currentValues": currentvalues, "to": self.mac_address, "from": "USER", }) async def turn_off(self, channel: int) -> None: """Turn off a channel.""" if self.light_mode == LightMode.DAYCL_MODE: await self.set_light_mode(LightMode.MAN_MODE) if self.ccv is None: return currentvalues = self.ccv["currentValues"] currentvalues[channel] = 0 await self.hub.send_packet({ "title": "CCV-SL", "currentValues": currentvalues, "to": self.mac_address, "from": "USER", }) @property def cloud_probability(self) -> int: """Return the cloud probability.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["probability"] async def set_cloud_probability(self, probability: int) -> None: """Set the cloud probability.""" if self.cloud is None: return await self.set_cloud({"probability": probability}) @property def cloud_max_amount(self) -> int: """Return the maximum amount of clouds.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["maxAmount"] async def set_cloud_max_amount(self, max_amount: int) -> None: """Set the maximum amount of clouds.""" if self.cloud is None: return await self.set_cloud({"maxAmount": max_amount}) @property def cloud_min_intensity(self) -> int: """Return the minimum intensity of clouds.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["minIntensity"] async def set_cloud_min_intensity(self, min_intensity: int) -> None: """Set the minimum intensity of clouds.""" if self.cloud is None: return await self.set_cloud({"minIntensity": min_intensity}) @property def cloud_max_intensity(self) -> int: """Return the maximum intensity of clouds.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["maxIntensity"] async def set_cloud_max_intensity(self, max_intensity: int) -> None: """Set the maximum intensity of clouds.""" if self.cloud is None: return await self.set_cloud({"maxIntensity": max_intensity}) @property def cloud_min_duration(self) -> int: """Return the minimum cloud duration.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["minDuration"] async def set_cloud_min_duration(self, min_duration: int) -> None: """Set the minimum cloud duration.""" if self.cloud is None: return await self.set_cloud({"minDuration": min_duration}) @property def cloud_max_duration(self) -> int: """Return the maximum cloud duration.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["maxDuration"] async def set_cloud_max_duration(self, max_duration: int) -> None: """Set the maximum cloud duration.""" if self.cloud is None: return await self.set_cloud({"maxDuration": max_duration}) @property def cloud_active(self) -> bool: """Return whether the cloud effect is active.""" if self.cloud is None: raise EheimDigitalDataMissingError return bool(self.cloud["cloudActive"]) async def set_cloud_active(self, *, active: bool) -> None: """Set whether the cloud effect is active.""" if self.cloud is None: return await self.set_cloud({"cloudActive": int(active)}) @property def cloud_mode(self) -> int: """Return the cloud mode.""" if self.cloud is None: raise EheimDigitalDataMissingError return self.cloud["mode"] async def set_cloud_mode(self, mode: int) -> None: """Set the cloud mode.""" if self.cloud is None: return await self.set_cloud({"mode": mode}) @property def moon_max_light(self) -> int: """Return the maximum moonlight intensity.""" if self.moon is None: raise EheimDigitalDataMissingError return self.moon["maxmoonlight"] async def set_moon_max_light(self, max_light: int) -> None: """Set the maximum moonlight intensity.""" if self.moon is None: return await self.set_moon({"maxmoonlight": max_light}) @property def moon_min_light(self) -> int: """Return the minimum moonlight intensity.""" if self.moon is None: raise EheimDigitalDataMissingError return self.moon["minmoonlight"] async def set_moon_min_light(self, min_light: int) -> None: """Set the minimum moonlight intensity.""" if self.moon is None: return await self.set_moon({"minmoonlight": min_light}) @property def moon_light_active(self) -> bool: """Return whether the moonlight effect is active.""" if self.moon is None: raise EheimDigitalDataMissingError return bool(self.moon["moonlightActive"]) async def set_moon_light_active(self, *, active: bool) -> None: """Set whether the moonlight effect is active.""" if self.moon is None: return await self.set_moon({"moonlightActive": int(active)}) @property def moon_light_cycle(self) -> bool: """Return whether the moonlight cycle is active.""" if self.moon is None: raise EheimDigitalDataMissingError return bool(self.moon["moonlightCycle"]) async def set_moon_light_cycle(self, *, cycle: bool) -> None: """Set whether the moonlight cycle is active.""" if self.moon is None: return await self.set_moon({"moonlightCycle": int(cycle)}) @property def acclimate_duration(self) -> int: """Return the acclimate duration.""" if self.acclimate is None: raise EheimDigitalDataMissingError return self.acclimate["duration"] async def set_acclimate_duration(self, duration: int) -> None: """Set the acclimate duration.""" if self.acclimate is None: return await self.set_acclimate({"duration": duration}) @property def acclimate_intensity_reduction(self) -> int: """Return the acclimate intensity reduction.""" if self.acclimate is None: raise EheimDigitalDataMissingError return self.acclimate["intensityReduction"] async def set_acclimate_intensity_reduction(self, reduction: int) -> None: """Set the acclimate intensity reduction.""" if self.acclimate is None: return await self.set_acclimate({"intensityReduction": reduction}) @property def acclimate_current_day(self) -> int: """Return the current acclimate day.""" if self.acclimate is None: raise EheimDigitalDataMissingError return self.acclimate["currentAcclDay"] async def set_acclimate_current_day(self, day: int) -> None: """Set the current acclimate day.""" if self.acclimate is None: return await self.set_acclimate({"currentAcclDay": day}) @property def acclimate_active(self) -> bool: """Return whether the acclimate effect is active.""" if self.acclimate is None: raise EheimDigitalDataMissingError return bool(self.acclimate["acclActive"]) async def set_acclimate_active(self, *, active: bool) -> None: """Set whether the acclimate effect is active.""" if self.acclimate is None: return await self.set_acclimate({"acclActive": int(active)}) @property def acclimate_pause(self) -> bool: """Return whether the acclimate effect is paused.""" if self.acclimate is None: raise EheimDigitalDataMissingError return bool(self.acclimate["pause"]) async def set_acclimate_pause(self, *, pause: bool) -> None: """Set whether the acclimate effect is paused.""" if self.acclimate is None: return await self.set_acclimate({"pause": int(pause)}) @override def as_dict(self) -> dict[str, Any]: """Return the device as a dictionary.""" return { "ccv": self.ccv, "clock": self.clock, "cloud": self.cloud, "moon": self.moon, "acclimate": self.acclimate, **super().as_dict(), } @property @override def is_missing_data(self) -> bool: return ( self.ccv is None or self.clock is None or self.cloud is None or self.moon is None or self.acclimate is None )