from __future__ import annotations
from typing import Any, cast
from pydantic import ConfigDict, Field, computed_field, model_validator
from .._base import NavienBaseModel
from ..enums import (
DHW_OPERATION_SETTING_TEXT,
DhwOperationSetting,
RecirculationMode,
)
from ..unit_system import get_unit_system
from ._converters import reservation_param_to_preferred
[docs]
class ReservationEntry(NavienBaseModel):
"""A single scheduled reservation entry.
Wraps the raw 6-byte protocol fields and provides computed properties
for display-ready values including unit-aware temperature conversion.
The raw protocol fields are:
- enable: 2=enabled, 1=disabled (device boolean)
- week: bitfield of active days (Sun=bit7, Mon=bit6, ..., Sat=bit1)
- hour: 0-23
- min: 0-59
- mode: DHW operation mode ID (1-6)
- param: temperature in half-degrees Celsius
"""
enable: int = 2
week: int = 0
hour: int = 0
min: int = 0
mode: int = 1
param: int = 0
@computed_field # type: ignore[prop-decorator]
@property
def enabled(self) -> bool:
"""Whether this reservation is active (device bool: 2=on, 1=off)."""
return self.enable == 2
@computed_field # type: ignore[prop-decorator]
@property
def days(self) -> list[str]:
"""Weekday names for this reservation."""
from ..encoding import decode_week_bitfield
return decode_week_bitfield(self.week)
@computed_field # type: ignore[prop-decorator]
@property
def time(self) -> str:
"""Formatted time string (HH:MM)."""
return f"{self.hour:02d}:{self.min:02d}"
@computed_field # type: ignore[prop-decorator]
@property
def temperature(self) -> float:
"""Temperature in the user's preferred unit."""
return reservation_param_to_preferred(self.param)
@computed_field # type: ignore[prop-decorator]
@property
def unit(self) -> str:
"""Temperature unit symbol."""
return "°C" if get_unit_system() == "metric" else "°F"
@computed_field # type: ignore[prop-decorator]
@property
def mode_name(self) -> str:
"""Human-readable operation mode name."""
try:
return DHW_OPERATION_SETTING_TEXT.get(
DhwOperationSetting(self.mode), f"Unknown ({self.mode})"
)
except ValueError:
return f"Unknown ({self.mode})"
[docs]
class ReservationSchedule(NavienBaseModel):
"""Complete reservation schedule from the device.
Can be constructed from raw MQTT response data. The ``reservation``
field accepts either a hex string (from GET responses) or a list of
dicts/ReservationEntry objects.
"""
reservation_use: int = Field(default=0, alias="reservationUse")
reservation: list[ReservationEntry] = Field(default_factory=list)
model_config = ConfigDict(
alias_generator=None,
populate_by_name=True,
extra="ignore",
use_enum_values=False,
)
@model_validator(mode="before")
@classmethod
def _decode_hex_reservation(cls, data: Any) -> Any:
"""Decode hex-encoded reservation string into entry list."""
if isinstance(data, dict):
d = cast(dict[str, Any], data).copy()
raw = d.get("reservation", "")
if isinstance(raw, str):
if raw:
from ..encoding import decode_reservation_hex
d["reservation"] = decode_reservation_hex(raw)
else:
d["reservation"] = []
return d
return data
@computed_field # type: ignore[prop-decorator]
@property
def enabled(self) -> bool:
"""Whether the reservation system is globally enabled.
Device bool convention: 2=on, 1=off.
"""
return self.reservation_use == 2
[docs]
class WeeklyReservationEntry(NavienBaseModel):
"""A single entry in a weekly temperature reservation schedule.
Similar to :class:`ReservationEntry` but used with the RESERVATION_WEEKLY
command (33554438), which configures a separate weekly temperature schedule
independent of the timed reservation system.
The raw protocol fields mirror the standard reservation format:
- enable: 2=enabled, 1=disabled (device boolean)
- week: bitfield of active days (Sun=bit7, Mon=bit6, ..., Sat=bit1)
- hour: 0-23
- min: 0-59
- mode: DHW operation mode ID (1-6)
- param: temperature in half-degrees Celsius
"""
enable: int = 2
week: int = 0
hour: int = 0
min: int = 0
mode: int = 1
param: int = 0
@computed_field # type: ignore[prop-decorator]
@property
def enabled(self) -> bool:
"""Whether this entry is active (device bool: 2=on, 1=off)."""
return self.enable == 2
@computed_field # type: ignore[prop-decorator]
@property
def days(self) -> list[str]:
"""Weekday names for this entry."""
from ..encoding import decode_week_bitfield
return decode_week_bitfield(self.week)
@computed_field # type: ignore[prop-decorator]
@property
def time(self) -> str:
"""Formatted time string (HH:MM)."""
return f"{self.hour:02d}:{self.min:02d}"
@computed_field # type: ignore[prop-decorator]
@property
def temperature(self) -> float:
"""Temperature in the user's preferred unit."""
return reservation_param_to_preferred(self.param)
@computed_field # type: ignore[prop-decorator]
@property
def unit(self) -> str:
"""Temperature unit symbol."""
return "°C" if get_unit_system() == "metric" else "°F"
@computed_field # type: ignore[prop-decorator]
@property
def mode_name(self) -> str:
"""Human-readable operation mode name."""
try:
return DHW_OPERATION_SETTING_TEXT.get(
DhwOperationSetting(self.mode), f"Unknown ({self.mode})"
)
except ValueError:
return f"Unknown ({self.mode})"
[docs]
class WeeklyReservationSchedule(NavienBaseModel):
"""Complete weekly reservation schedule (RESERVATION_WEEKLY command).
Used with command code 33554438 to configure a temperature schedule
that repeats weekly. Accepts the same hex-encoded format as the
standard reservation schedule.
"""
reservation_use: int = Field(default=0, alias="reservationUse")
reservation: list[WeeklyReservationEntry] = Field(default_factory=list)
model_config = ConfigDict(
alias_generator=None,
populate_by_name=True,
extra="ignore",
use_enum_values=False,
)
@model_validator(mode="before")
@classmethod
def _decode_hex_reservation(cls, data: Any) -> Any:
"""Decode hex-encoded reservation string into entry list."""
if isinstance(data, dict):
d = cast(dict[str, Any], data).copy()
raw = d.get("reservation", "")
if isinstance(raw, str):
if raw:
from ..encoding import decode_reservation_hex
d["reservation"] = decode_reservation_hex(raw)
else:
d["reservation"] = []
return d
return data
@computed_field # type: ignore[prop-decorator]
@property
def enabled(self) -> bool:
"""Whether the weekly reservation system is globally enabled.
Device bool convention: 2=on, 1=off.
"""
return self.reservation_use == 2
[docs]
class RecirculationScheduleEntry(NavienBaseModel):
"""A single entry in a recirculation pump schedule.
Used with the RECIR_RESERVATION command (33554444) to set timed
recirculation cycles. Each entry defines a time window and pump mode.
Fields:
- enable: 2=enabled, 1=disabled (device boolean)
- week: bitfield of active days (Sun=bit7, Mon=bit6, ..., Sat=bit1)
- start_hour: 0-23
- start_min: 0-59
- end_hour: 0-23
- end_min: 0-59
- mode: recirculation mode
(1=Constant, 2=Timer, 3=Temperature, 4=Sensor)
"""
enable: int = 2
week: int = 0
start_hour: int = Field(default=0, alias="startHour")
start_min: int = Field(default=0, alias="startMin")
end_hour: int = Field(default=0, alias="endHour")
end_min: int = Field(default=0, alias="endMin")
mode: int = 1
model_config = ConfigDict(
alias_generator=None,
populate_by_name=True,
extra="ignore",
use_enum_values=False,
)
@computed_field # type: ignore[prop-decorator]
@property
def enabled(self) -> bool:
"""Whether this entry is active (device bool: 2=on, 1=off)."""
return self.enable == 2
@computed_field # type: ignore[prop-decorator]
@property
def days(self) -> list[str]:
"""Weekday names for this entry."""
from ..encoding import decode_week_bitfield
return decode_week_bitfield(self.week)
@computed_field # type: ignore[prop-decorator]
@property
def start_time(self) -> str:
"""Formatted start time string (HH:MM)."""
return f"{self.start_hour:02d}:{self.start_min:02d}"
@computed_field # type: ignore[prop-decorator]
@property
def end_time(self) -> str:
"""Formatted end time string (HH:MM)."""
return f"{self.end_hour:02d}:{self.end_min:02d}"
@computed_field # type: ignore[prop-decorator]
@property
def mode_name(self) -> str:
"""Human-readable recirculation mode name."""
try:
return RecirculationMode(self.mode).name.replace("_", " ").title()
except ValueError:
return f"Unknown ({self.mode})"
[docs]
class RecirculationSchedule(NavienBaseModel):
"""Complete recirculation pump schedule (RECIR_RESERVATION command).
Used with command code 33554444 to configure timed recirculation
pump operation windows.
"""
schedule: list[RecirculationScheduleEntry] = Field(default_factory=list)
[docs]
class OtaCommitPayload(NavienBaseModel):
"""Payload for committing a firmware component update.
Used with the OTA_COMMIT command (33554442). This command uses a
special ``commitOta`` structure instead of the standard mode/param
format.
Args:
sw_code: Software component code identifying which firmware to commit.
1 = Controller, 2 = Panel, 4 = WiFi/communication module.
sw_version: Version number to commit (as reported by the OTA check).
"""
sw_code: int = Field(alias="swCode")
sw_version: int = Field(alias="swVersion")
model_config = ConfigDict(
alias_generator=None,
populate_by_name=True,
extra="ignore",
)