diff --git a/custom_components/njspc_ha/__init__.py b/custom_components/njspc_ha/__init__.py index 17ab3dc..351832d 100644 --- a/custom_components/njspc_ha/__init__.py +++ b/custom_components/njspc_ha/__init__.py @@ -45,6 +45,7 @@ EVENT_FILTER, EVENT_VIRTUAL_CIRCUIT, EVENT_TEMPS, + EVENT_SCHEDULE, ) @@ -187,6 +188,11 @@ async def handle_virtual_circuit(data): data["event"] = EVENT_VIRTUAL_CIRCUIT self.async_set_updated_data(data) + @self.sio.on("schedule") + async def handle_schedule(data): + data["event"] = EVENT_SCHEDULE + self.async_set_updated_data(data) + @self.sio.event async def connect(): print("I'm connected!") diff --git a/custom_components/njspc_ha/const.py b/custom_components/njspc_ha/const.py index 7b8274b..6c051c2 100644 --- a/custom_components/njspc_ha/const.py +++ b/custom_components/njspc_ha/const.py @@ -27,6 +27,7 @@ API_TEMPERATURE_SETPOINT = "state/body/setPoint" API_SET_HEATMODE = "state/body/heatMode" API_CHEM_CONTROLLER_SETPOINT = "state/chemController" +API_CONFIG_SCHEDULE = "config/schedule" # SOCKETIO EVENTS EVENT_CIRCUIT = "circuit" @@ -42,6 +43,7 @@ EVENT_CHEM_CONTROLLER = "chemController" EVENT_FILTER = "filter" EVENT_VIRTUAL_CIRCUIT = "virtualCircuit" +EVENT_SCHEDULE = "schedule" POOL_SETPOINT = "poolSetpoint" SPA_SETPOINT = "spaSetpoint" diff --git a/custom_components/njspc_ha/entity.py b/custom_components/njspc_ha/entity.py index dca9b44..53e49ae 100644 --- a/custom_components/njspc_ha/entity.py +++ b/custom_components/njspc_ha/entity.py @@ -130,6 +130,11 @@ def format_duration(self, secs: int) -> str: formatted = f"{formatted} {sec}sec" return formatted + + + + + @property def device_info(self) -> DeviceInfo | None: """Device info""" diff --git a/custom_components/njspc_ha/schedules.py b/custom_components/njspc_ha/schedules.py new file mode 100644 index 0000000..77e995f --- /dev/null +++ b/custom_components/njspc_ha/schedules.py @@ -0,0 +1,202 @@ +"""Platform for schedule integration.""" +from __future__ import annotations + +from typing import Any +from collections.abc import Mapping + + +from homeassistant.components.switch import SwitchEntity + + +from .entity import PoolEquipmentEntity +from .__init__ import NjsPCHAdata +from .const import ( + PoolEquipmentClass, + EVENT_SCHEDULE, + EVENT_AVAILABILITY, + API_CONFIG_SCHEDULE, +) + +DAY_ABBREVIATIONS = { + "sun": "U", + "sat": "S", + "fri": "F", + "thu": "H", + "wed": "W", + "tue": "T", + "mon": "M", +} + + +class ScheduleSwitch(PoolEquipmentEntity, SwitchEntity): + """Schedule switch for njsPC-HA""" + + def __init__( + self, + coordinator: NjsPCHAdata, + equipment_class: PoolEquipmentClass, + schedule, + clockMode: int = 12, + body=None, + ) -> None: + """Initialize the switch.""" + data = schedule["circuit"] + if body is not None: + # change it over to body if provided + data = body + super().__init__( + coordinator=coordinator, + equipment_class=equipment_class, + data=data, + ) + + self._available = True + self._value = False + self._clock_mode = clockMode + + self._attr_has_entity_name = False + self.schedule_id = schedule["id"] + self.schedule_name = schedule["circuit"]["name"] + if "disabled" in schedule: + self._value = schedule["disabled"] + + self._state_attributes: dict[str, Any] = dict([]) + if "scheduleDays" in schedule: + self._state_attributes["days"] = self.format_schedule_days( + schedule_days=schedule["scheduleDays"] + ) + if "startTime" in schedule and "startTimeType" in schedule: + self._state_attributes["start_time"] = self.format_start_stop_times( + time=schedule["startTime"], time_type=schedule["startTimeType"]["val"] + ) + if "endTime" in schedule and "endTimeType" in schedule: + self._state_attributes["end_time"] = self.format_start_stop_times( + time=schedule["endTime"], time_type=schedule["endTimeType"]["val"] + ) + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if ( + self.coordinator.data["event"] == EVENT_SCHEDULE + and self.coordinator.data["id"] == self.schedule_id + ): + if "disabled" in self.coordinator.data: + self._value = self.coordinator.data["disabled"] + else: + self._value = False + if ( + "circuit" in self.coordinator.data + and "name" in self.coordinator.data["circuit"] + ): + self.schedule_name = self.coordinator.data["circuit"]["name"] + + if ( + "startTime" in self.coordinator.data + and "startTimeType" in self.coordinator.data + ): + self._state_attributes["start_time"] = self.format_start_stop_times( + time=self.coordinator.data["startTime"], + time_type=self.coordinator.data["startTimeType"]["val"], + ) + if ( + "endTime" in self.coordinator.data + and "endTimeType" in self.coordinator.data + ): + self._state_attributes["end_time"] = self.format_start_stop_times( + time=self.coordinator.data["endTime"], + time_type=self.coordinator.data["endTimeType"]["val"], + ) + if "scheduleDays" in self.coordinator.data: + self._state_attributes["days"] = self.format_schedule_days( + schedule_days=self.coordinator.data["scheduleDays"] + ) + self.async_write_ha_state() + elif self.coordinator.data["event"] == EVENT_AVAILABILITY: + self._available = self.coordinator.data["available"] + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + data = {"id": self.schedule_id, "disabled": False} + await self.coordinator.api.command(url=API_CONFIG_SCHEDULE, data=data) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity on.""" + data = {"id": self.schedule_id, "disabled": True} + await self.coordinator.api.command(url=API_CONFIG_SCHEDULE, data=data) + + def format_start_stop_times(self, time: int, time_type: int) -> str: + """Format the minutes time to human readable""" + formatted = "" + if time_type == 1: + return "Sunrise" + elif time_type == 2: + return "Sunset" + elif time_type == 0: + hrs = time // 60 + mins = time - (hrs * 60) + if self._clock_mode == 24: + formatted = f"{hrs:02}:{mins:02}" + else: + ampm = "am" + if hrs > 12: + hrs = hrs - 12 + ampm = "pm" + formatted = f"{hrs}:{mins:02} {ampm}" + else: + formatted = "Unknown" + return formatted + + def format_schedule_days(self, schedule_days) -> str: + """Format the scheduled days into a string""" + formatted = "" + if schedule_days["val"] == 127: + formatted = "Every Day" + elif schedule_days["val"] == 31: + formatted = "Weekdays" + elif schedule_days["val"] == 96: + formatted = "Weekends" + else: + day_list = [] + idx = 0 + for day in schedule_days["days"]: + if day["name"] == "sun": + # we need to change the insert index if Sunday is in the list so that it stays at the beginning + idx = 1 + day_list.insert(idx, DAY_ABBREVIATIONS[day["name"]]) + formatted = "-".join(day_list) + return formatted + + @property + def should_poll(self) -> bool: + return False + + @property + def available(self) -> bool: + return self._available + + @property + def name(self) -> str: + return f"{self.schedule_name} Schedule" + + @property + def unique_id(self) -> str: + """Set unique device_id""" + return f"{self.coordinator.controller_id}_schedule_{self.schedule_id}_disabled" + + @property + def is_on(self) -> bool: + return not self._value + + @property + def icon(self) -> str: + if self._value: + return "mdi:calendar-remove" + else: + return "mdi:calendar-clock" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + + return self._state_attributes diff --git a/custom_components/njspc_ha/switch.py b/custom_components/njspc_ha/switch.py index abcf6fb..c7051ce 100644 --- a/custom_components/njspc_ha/switch.py +++ b/custom_components/njspc_ha/switch.py @@ -15,6 +15,7 @@ from .features import CircuitSwitch from .chemistry import SuperChlorSwitch from .bodies import BodyCircuitSwitch +from .schedules import ScheduleSwitch async def async_setup_entry( @@ -85,5 +86,41 @@ async def async_setup_entry( SuperChlorSwitch(coordinator=coordinator, chlorinator=chlorinator) ) + for schedule in config["schedules"]: + equipment_class = PoolEquipmentClass.AUX_CIRCUIT + _body = None + match schedule["circuit"]["equipmentType"]: + case "circuit": + try: + if schedule["circuit"]["id"] == 1 or schedule["circuit"]["id"] == 6: + for body in config["temps"]["bodies"]: + if ( + body["circuit"] == schedule["circuit"]["id"] + and "name" in schedule["circuit"] + ): + _body = body + equipment_class = PoolEquipmentClass.BODY + elif schedule["circuit"]["type"]["isLight"]: + equipment_class = PoolEquipmentClass.LIGHT + else: + equipment_class = PoolEquipmentClass.AUX_CIRCUIT + except KeyError: + equipment_class = PoolEquipmentClass.AUX_CIRCUIT + case "circuitGroup": + equipment_class = PoolEquipmentClass.CIRCUIT_GROUP + case "feature": + equipment_class = PoolEquipmentClass.FEATURE + case "lightGroup": + equipment_class = PoolEquipmentClass.LIGHT_GROUP + new_devices.append( + ScheduleSwitch( + coordinator=coordinator, + equipment_class=equipment_class, + schedule=schedule, + body=_body, + clockMode=config["clockMode"]["val"] + ) + ) + if new_devices: async_add_entities(new_devices)