"""The Eheim Digital pHcontrol device.""" from __future__ import annotations from datetime import datetime, time, timedelta, timezone from logging import getLogger from typing import TYPE_CHECKING, Any, override from .device import EheimDigitalDevice from .types import ( EheimDigitalDataMissingError, MsgTitle, PHControlErrorCode, PHControlMode, PHDataPacket, UsrDtaPacket, ) if TYPE_CHECKING: from .hub import EheimDigitalHub _LOGGER = getLogger(__package__) class EheimDigitalPHControl(EheimDigitalDevice): """Represent a EHEIM Digital pHcontrol device.""" ph_data: PHDataPacket | None = None def __init__(self, hub: EheimDigitalHub, usrdta: UsrDtaPacket) -> None: """Initialize a pHcontrol device.""" super().__init__(hub, usrdta) @override async def parse_message(self, msg: dict[str, Any]) -> None: """Parse a message.""" if msg["title"] == MsgTitle.PH_DATA: self.ph_data = PHDataPacket(**msg) @override async def update(self) -> None: """Get the new device state.""" await self.hub.send_packet({ "title": MsgTitle.GET_PH_DATA, "to": self.mac_address, "from": "USER", }) async def set_ph_param(self, data: dict[str, Any]) -> None: """Send a SET_PH_PARAM packet, containing new values from data.""" if self.ph_data is None: _LOGGER.error("set_ph_param: No PH_DATA packet received yet.") return await self.hub.send_packet({ "title": self.ph_data["title"], "to": self.mac_address, "sollPH": self.ph_data["sollPH"], "active": self.ph_data["active"], "hystLow": self.ph_data["hystLow"], "hystHigh": self.ph_data["hystHigh"], "offset": self.ph_data["offset"], "acclimatization": self.ph_data["acclimatization"], "mode": self.ph_data["mode"], "expert": self.ph_data["expert"], "kH": self.ph_data["kH"], "schedule": self.ph_data["schedule"], "sync": self.ph_data["sync"], "partnerName": self.ph_data["partnerName"], "nightStartT": self.ph_data["nightStartT"], "dayStartT": self.ph_data["dayStartT"], "nReduce": self.ph_data["nReduce"], "from": "USER", **data, }) @property def is_active(self) -> bool: """Return whether the device is active.""" if self.ph_data is None: raise EheimDigitalDataMissingError return bool(self.ph_data["active"]) async def set_active(self, *, active: bool) -> None: """Set whether the filter should be active or not.""" if self.ph_data is None: return await self.set_ph_param({"active": int(active)}) @property def is_ph(self) -> float: """Return the current pH value.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["isPH"] / 10 @property def soll_ph(self) -> float: """Return the desired pH value.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["sollPH"] / 10 async def set_soll_ph(self, value: float) -> None: """Set a new desired pH value.""" if self.ph_data is None: return await self.set_ph_param({"sollPH": int(value * 10)}) @property def hyst_low(self) -> float: """Return the lower hysteresis threshold.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["hystLow"] / 10 async def set_hyst_low(self, value: float) -> None: """Set the lower hysteresis threshold.""" if self.ph_data is None: return await self.set_ph_param({"hystLow": int(value * 10)}) @property def hyst_high(self) -> float: """Return the higher hysteresis threshold.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["hystHigh"] / 10 async def set_hyst_high(self, value: float) -> None: """Set the higher hysteresis threshold.""" if self.ph_data is None: return await self.set_ph_param({"hystHigh": int(value * 10)}) @property def offset(self) -> float: """Return the offset value.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["offset"] / 10 async def set_offset(self, value: float) -> None: """Set the offset value.""" if self.ph_data is None: return await self.set_ph_param({"offset": int(value * 10)}) @property def acclimatization(self) -> bool: """Return the acclimatization value.""" if self.ph_data is None: raise EheimDigitalDataMissingError return bool(self.ph_data["acclimatization"]) async def set_acclimatization(self, *, value: bool) -> None: """Set the acclimatization value.""" if self.ph_data is None: return await self.set_ph_param({"acclimatization": int(value)}) @property def mode(self) -> PHControlMode: """Return the pHControl mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return PHControlMode(self.ph_data["mode"]) async def set_mode(self, value: PHControlMode) -> None: """Set the pHControl mode.""" if self.ph_data is None: return await self.set_ph_param({"mode": int(value)}) @property def expert(self) -> bool: """Return whether expert mode is enabled for daycycle mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return bool(self.ph_data["expert"]) async def set_expert(self, *, value: bool) -> None: """Set whether expert mode is enabled for daycycle mode.""" if self.ph_data is None: return await self.set_ph_param({"expert": int(value)}) @property def kh(self) -> int: """Return the kH value in °dH.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["kH"] @property def schedule(self) -> list[list[int]]: """Return the schedule for daycyle expert mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["schedule"] async def set_schedule(self, schedule: list[list[int]]) -> None: """Set the schedule for daycyle expert mode.""" if self.ph_data is None: return await self.set_ph_param({"schedule": schedule}) @property def sync(self) -> str: """Return the device MAC address to sync the daycycle mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["sync"] async def set_sync(self, value: str) -> None: """Set the device MAC address to sync the daycycle mode.""" if self.ph_data is None: return await self.set_ph_param({"sync": value}) @property def partner_name(self) -> str: """Return the name of the partner device.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["partnerName"] async def set_partner_name(self, value: str) -> None: """Set the name of the partner device.""" if self.ph_data is None: return await self.set_ph_param({"partnerName": value}) @property def day_start_time(self) -> time: """Return the day start time for Daycycle mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return time( self.ph_data["dayStartT"] // 60, self.ph_data["dayStartT"] % 60, tzinfo=timezone(timedelta(minutes=self.usrdta["timezone"])), ) async def set_day_start_time(self, day_start_time: time) -> None: """Set the day start time for Daycycle mode.""" if self.ph_data is None: return t = ( datetime( 2020, 1, 1, day_start_time.hour, day_start_time.minute, tzinfo=day_start_time.tzinfo, ) .astimezone(timezone(timedelta(minutes=self.usrdta["timezone"]))) .time() ) await self.set_ph_param({"dayStartT": t.hour * 60 + t.minute}) @property def night_start_time(self) -> time: """Return the night start time for Daycycle mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return time( self.ph_data["nightStartT"] // 60, self.ph_data["nightStartT"] % 60, tzinfo=timezone(timedelta(minutes=self.usrdta["timezone"])), ) async def set_night_start_time(self, night_start_time: time) -> None: """Set the night start time for Daycycle mode.""" if self.ph_data is None: return t = ( datetime( 2020, 1, 1, night_start_time.hour, night_start_time.minute, tzinfo=night_start_time.tzinfo, ) .astimezone(timezone(timedelta(minutes=self.usrdta["timezone"]))) .time() ) await self.set_ph_param({"nightStartT": t.hour * 60 + t.minute}) @property def night_temperature_offset(self) -> float: """Return the night temperature offset for Daycycle mode.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["nReduce"] / 10 async def set_night_temperature_offset( self, night_temperature_offset: float ) -> None: """Set the night temperature offset for Daycycle mode.""" if self.ph_data is None: return await self.set_ph_param({"nReduce": int(night_temperature_offset * 10)}) @property def alert_state(self) -> PHControlErrorCode: """Return the alert state.""" if self.ph_data is None: raise EheimDigitalDataMissingError return PHControlErrorCode(self.ph_data["alertState"]) @property def service_time(self) -> int: """Return the remaining days until the electrode has to be calibrated.""" if self.ph_data is None: raise EheimDigitalDataMissingError return self.ph_data["serviceTime"] @property def valve_is_active(self) -> bool: """Return whether the valve is active.""" if self.ph_data is None: raise EheimDigitalDataMissingError return bool(self.ph_data["valveIsActive"]) @override def as_dict(self) -> dict[str, Any]: """Return the device as a dictionary.""" return { "ph_data": self.ph_data, **super().as_dict(), } @property @override def is_missing_data(self) -> bool: return self.ph_data is None