From cda2c1c8dd5dc45680846207013e066b58b5e3ff Mon Sep 17 00:00:00 2001 From: Francesco Dipietromaria Date: Fri, 8 May 2026 11:09:34 +0000 Subject: [PATCH] feat: add holiday support for Velis Slp (Nuos) devices The official Ariston NET app exposes a dedicated holiday endpoint for Slp devices at `POST /api/v2/velis/slpPlantData/{gw}/holiday`, but the library only exposes a holiday setter on `AristonGalevoDevice` that targets the Galevo path `/remote/plantData/{gw}/holiday`. There was no way to schedule, clear, or read the holiday state on a Nuos heat pump from Python. This change adds: * `AristonAPI.set_velis_slp_holiday` (sync + async) that POSTs to the Slp-specific endpoint with `{"new": holiday_end_date}` (or `{"new": null}` to clear). * `AristonNuosSplitDevice.set_holiday(holiday_end)` (sync + async) that accepts a `datetime.date` (or `None`) and forwards it to the API after formatting the ISO string the cloud expects, mirroring the existing Galevo `set_holiday` shape. * `NuosSplitProperties.HOLIDAY_UNTIL = "holidayUntil"` and two new read-only properties on `AristonNuosSplitDevice`: - `holiday_end_date` -> the ISO date string the cloud has on file (or `None` when no holiday is scheduled) - `holiday_active` -> `True` when `holidayUntil` is non-null The Slp payload does not expose a dedicated boolean: an active holiday is signalled by `holidayUntil` being a non-null string. * The setters now optimistically write `holidayUntil` back into `device.data`, so the new properties reflect the requested change immediately without waiting for an `update_state()` round-trip (mirrors the cache-update pattern used by the temperature and operation-mode setters). The endpoint, the field name `holidayUntil`, and the fact that no dedicated boolean exists were verified against the official APK (`com.remotethermo.aristonnet` v6.0.7773.40338): the endpoint string is present in `classes*.dex`, and the `SlpPlantDataDto` Kotlin model declares `holidayUntil: String?` with the matching `@SerialName`. --- ariston/ariston_api.py | 34 +++++++++++++++++++++++ ariston/const.py | 1 + ariston/nuos_split_device.py | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/ariston/ariston_api.py b/ariston/ariston_api.py index 465e078..5fe446f 100644 --- a/ariston/ariston_api.py +++ b/ariston/ariston_api.py @@ -452,6 +452,23 @@ def set_holiday( }, ) + def set_velis_slp_holiday( + self, + gw_id: str, + holiday_end_date: Optional[str], + ) -> None: + """Set holiday on a Velis Slp (Nuos) device. + + Pass an ISO-formatted end date string to schedule the holiday, or + ``None`` to clear it. + """ + self._post( + f"{self.__api_url}{ARISTON_VELIS}/{PlantData.Slp.value}/{gw_id}/holiday", + { + "new": holiday_end_date, + }, + ) + def get_bus_errors(self, gw_id: str) -> list[Any]: """Get bus errors""" bus_errors = self._get( @@ -914,6 +931,23 @@ async def async_set_holiday( }, ) + async def async_set_velis_slp_holiday( + self, + gw_id: str, + holiday_end_date: Optional[str], + ) -> None: + """Async set holiday on a Velis Slp (Nuos) device. + + Pass an ISO-formatted end date string to schedule the holiday, or + ``None`` to clear it. + """ + await self._async_post( + f"{self.__api_url}{ARISTON_VELIS}/{PlantData.Slp.value}/{gw_id}/holiday", + { + "new": holiday_end_date, + }, + ) + async def async_get_bus_errors(self, gw_id: str) -> list[Any]: """Async get bus errors""" bus_errors = await self._async_get( diff --git a/ariston/const.py b/ariston/const.py index 13f9b09..20bd24e 100644 --- a/ariston/const.py +++ b/ariston/const.py @@ -427,6 +427,7 @@ class NuosSplitProperties(VelisDeviceProperties): OP_MODE: Final[str] = "opMode" BOOST_ON: Final[str] = "boostOn" HP_STATE: Final[str] = "hpState" + HOLIDAY_UNTIL: Final[str] = "holidayUntil" class EvoLydosDeviceProperties(VelisDeviceProperties): diff --git a/ariston/nuos_split_device.py b/ariston/nuos_split_device.py index bfc6fe7..f7efc79 100644 --- a/ariston/nuos_split_device.py +++ b/ariston/nuos_split_device.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from datetime import date from typing import Optional from .velis_device import AristonVelisDevice @@ -112,6 +113,26 @@ def water_heater_mode_value(self) -> Optional[int]: """Get water heater mode value""" return self.data.get(NuosSplitProperties.OP_MODE, None) + @property + def holiday_end_date(self) -> Optional[str]: + """Get the holiday end date. + + Returns the ISO-formatted end date the cloud has on file + (e.g. ``"2026-08-15T00:00:00"``) when a holiday is scheduled, or + ``None`` when no holiday is active. + """ + return self.data.get(NuosSplitProperties.HOLIDAY_UNTIL, None) + + @property + def holiday_active(self) -> bool: + """Whether a holiday is currently scheduled. + + The Slp payload does not expose a dedicated boolean: an active + holiday is signalled by `holidayUntil` being a non-null string, + and an inactive holiday by `holidayUntil` being ``null``. + """ + return self.data.get(NuosSplitProperties.HOLIDAY_UNTIL) is not None + def set_water_heater_boost(self, boost: bool): """Set water heater boost""" self.api.set_nous_boost(self.gw, boost) @@ -257,3 +278,36 @@ async def async_set_heating_rate(self, heating_rate: float): self.plant_settings[SlpDeviceSettings.SLP_HEATING_RATE], ) self.plant_settings[SlpDeviceSettings.SLP_HEATING_RATE] = heating_rate + + @staticmethod + def _create_holiday_end_date(holiday_end: Optional[date]) -> Optional[str]: + """Format a holiday end date for the cloud payload. + + Returns ``None`` when ``holiday_end`` is ``None``, which the cloud + interprets as "clear the holiday". + """ + return ( + None + if holiday_end is None + else holiday_end.strftime("%Y-%m-%dT00:00:00") + ) + + def set_holiday(self, holiday_end: Optional[date]) -> None: + """Set or clear the holiday on this Nuos device. + + Pass a ``datetime.date`` to schedule the holiday end, or ``None`` to + clear an active holiday. + """ + holiday_end_date = self._create_holiday_end_date(holiday_end) + self.api.set_velis_slp_holiday(self.gw, holiday_end_date) + self.data[NuosSplitProperties.HOLIDAY_UNTIL] = holiday_end_date + + async def async_set_holiday(self, holiday_end: Optional[date]) -> None: + """Async set or clear the holiday on this Nuos device. + + Pass a ``datetime.date`` to schedule the holiday end, or ``None`` to + clear an active holiday. + """ + holiday_end_date = self._create_holiday_end_date(holiday_end) + await self.api.async_set_velis_slp_holiday(self.gw, holiday_end_date) + self.data[NuosSplitProperties.HOLIDAY_UNTIL] = holiday_end_date