Skip to content

Commit

Permalink
Merge pull request #232 from loopj/more-thermostats
Browse files Browse the repository at this point in the history
Add additional thermostat types to the thermostat controller
  • Loading branch information
loopj authored Feb 14, 2025
2 parents a5254ac + 95e8fad commit a9af9ad
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/aiovantage/_controllers/port_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ class PortDevicesController(BaseController[PortDevice]):
"Somfy.RS-485_SDN_2_x2E_0_PORT",
"Somfy.URTSI_2_PORT",
"Vantage.DmxGateway",
"Vantage.Generic_HVAC_RS485_PORT",
"Vantage.HVAC-IU_PORT",
)
32 changes: 24 additions & 8 deletions src/aiovantage/_controllers/thermostats.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
from aiovantage.objects import Thermostat
from aiovantage.objects import (
Thermostat,
VantageGenericHVACRS485ZoneChild,
VantageGenericHVACRS485ZoneWithoutFanSpeedChild,
VantageHVACIUZoneChild,
VantageVirtualThermostatPort,
)

from .base import BaseController

ThermostatTypes = (
Thermostat
| VantageGenericHVACRS485ZoneChild
| VantageGenericHVACRS485ZoneWithoutFanSpeedChild
| VantageHVACIUZoneChild
| VantageVirtualThermostatPort
)
"""Types managed by the thermostats controller."""

class ThermostatsController(BaseController[Thermostat]):
"""Thermostats controller.

Thermostats have a number of temperature sensors associated with them which
represent the current indoor temperature, outdoor temperature, and the
current cool and heat setpoints.
"""
class ThermostatsController(BaseController[ThermostatTypes]):
"""Thermostats controller."""

vantage_types = ("Thermostat",)
vantage_types = (
"Thermostat",
"Vantage.Generic_HVAC_RS485_Zone_CHILD",
"Vantage.Generic_HVAC_RS485_Zone_without_FanSpeed_CHILD",
"Vantage.HVAC-IU-Zone_CHILD",
"Vantage.VirtualThermostat_PORT",
)
7 changes: 5 additions & 2 deletions src/aiovantage/_object_interfaces/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ class FanSpeed(IntEnum):
Off = 0
Low = 1
Medium = 2
Hight = 3
High = 3
Max = 4
Auto = 5

# Properties
speed: FanSpeed | None = None

# Methods
@method("GetSpeed", "GetSpeedHW")
@method("GetSpeed", "GetSpeedHW", property="speed")
async def get_speed(self, *, hw: bool = False) -> FanSpeed:
"""Get the speed of a fan.
Expand Down
40 changes: 40 additions & 0 deletions src/aiovantage/_object_interfaces/object.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import datetime as dt
from enum import IntEnum
from typing import TypeVar

from .base import Interface, method

IntEnumT = TypeVar("IntEnumT", bound=IntEnum)


class ObjectInterface(Interface):
"""'Object' object interface."""
Expand Down Expand Up @@ -181,6 +185,29 @@ async def set_property_ex(self, property: str, value: str) -> None:
# -> R:INVOKE <id> <rcode> Object.SetPropertyEx <property> <value>
await self.invoke("Object.SetPropertyEx", property, value)

@method("IsEnumeratorSupported")
async def is_enumerator_supported(
self, interface_name: str, enumeration_name: str, enumerator_name: str
) -> bool:
"""Check if an enumerator is supported by an object.
Args:
interface_name: The name of the interface to check.
enumeration_name: The name of the enumeration to check.
enumerator_name: The name of the enumerator to check.
Returns:
True if the enumerator is supported, False otherwise.
"""
# INVOKE <id> Object.IsEnumeratorSupported <interfaceName> <enumerationName> <enumeratorName>
# -> R:INVOKE <id> <supported (0/1)> Object.IsEnumeratorSupported <interfaceName> <enumerationName> <enumeratorName>
return await self.invoke(
"Object.IsEnumeratorSupported",
interface_name,
enumeration_name,
enumerator_name,
)

@method("GetMTime")
async def get_m_time(self) -> dt.datetime:
"""Get the modification time of an object.
Expand Down Expand Up @@ -213,3 +240,16 @@ async def get_area(self) -> int:
# INVOKE <id> Object.GetArea
# -> R:INVOKE <id> <area> Object.GetArea
return await self.invoke("Object.GetArea")

# Convenience functions, not part of the interface
async def get_supported_enum_values(
self, interface: type[Interface], enum: type[IntEnumT]
) -> list[IntEnumT]:
"""Get all supported enum values of an object."""
return [
val
for val in enum
if await self.is_enumerator_supported(
interface.interface_name, enum.__name__, val.name
)
]
10 changes: 9 additions & 1 deletion src/aiovantage/_objects/thermostat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Thermostat object."""

from dataclasses import dataclass
from dataclasses import dataclass, field

from aiovantage.object_interfaces import ThermostatInterface

Expand All @@ -10,3 +10,11 @@
@dataclass(kw_only=True)
class Thermostat(StationObject, ThermostatInterface):
"""Thermostat object."""

day_mode_event: int = field(default=0, metadata={"name": "DayMode"})
fan_mode_event: int = field(default=0, metadata={"name": "FanMode"})
operation_mode_event: int = field(default=0, metadata={"name": "OperationMode"})
external_temperature: int
display_clock: bool = True
pseudo_mode: bool = True
humidistat: bool = False
2 changes: 1 addition & 1 deletion src/aiovantage/_objects/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Parent:
"""Vantage parent type."""

id: int
vid: int
position: int = field(metadata={"type": "Attribute"})


Expand Down
1 change: 0 additions & 1 deletion src/aiovantage/_objects/vantage_ddg_color_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class ColorType(Enum):
HSL = "HSL"
HSIC = "HSIC"
CCT = "CCT"
COLOR_CHANNEL = "Color Channel"

parent: Parent
color_type: ColorType
Expand Down
99 changes: 99 additions & 0 deletions src/aiovantage/_objects/vantage_generic_hvac_rs485.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Vantage Generic HVAC RS485 objects."""

from dataclasses import dataclass, field

from aiovantage.object_interfaces import FanInterface, ThermostatInterface

from .child_device import ChildDevice
from .port_device import PortDevice


@dataclass(kw_only=True)
class VantageGenericHVACRS485Port(PortDevice):
"""Vantage Generic HVAC RS485 port device."""

class Meta:
name = "Vantage.Generic_HVAC_RS485_PORT"

@dataclass(kw_only=True)
class FanSpeedSettings:
auto_fan: bool = True
high_fan: bool = True
low_fan: bool = True
max_fan: bool = True
med_fan: bool = True
off_fan: bool = True

@dataclass(kw_only=True)
class SensorSettings:
no_device_sensors: bool = False
outdoor_sensor: int = field(metadata={"name": "outdoorSensor"})
outdoor_temp_offset: str = field(
default="0", metadata={"name": "outdoorTempOffset"}
)
track_sensors: bool = False

@dataclass(kw_only=True)
class SetpointSettings:
bind_setpoints: bool = False
max_temp: int = 25
min_temp: int = 15

fan_boost_option: bool = False
fan_speed_settings: FanSpeedSettings
fan_individual_control: bool = False
receive_port: int
sensor_settings: SensorSettings
setpoint_settings: SetpointSettings


@dataclass(kw_only=True)
class VantageGenericHVACRS485TechContactsChild(ChildDevice):
"""Vantage Generic HVAC RS485 tech contacts child device."""

class Meta:
name = "Vantage.Generic_HVAC_RS485_TechContacts_CHILD"


@dataclass(kw_only=True)
class VantageGenericHVACRS485CompoundChild(ChildDevice, ThermostatInterface):
"""Vantage Generic HVAC RS485 compound child device."""

class Meta:
name = "Vantage.Generic_HVAC_RS485_Compound_CHILD"

adress_number: int = 1 # NOTE: Intentional typo to match the underlying object


@dataclass(kw_only=True)
class VantageGenericHVACRS485ZoneChild(ChildDevice, ThermostatInterface, FanInterface):
"""Vantage Generic HVAC RS485 zone child device."""

class Meta:
name = "Vantage.Generic_HVAC_RS485_Zone_CHILD"

@dataclass(kw_only=True)
class IndoorSettings:
indoor_sensor: int = field(metadata={"name": "indoorSensor"})
indoor_temp_offset: str = "0"

indoor_settings: IndoorSettings
position_number: int = 1


@dataclass(kw_only=True)
class VantageGenericHVACRS485ZoneWithoutFanSpeedChild(
ChildDevice, ThermostatInterface, FanInterface
):
"""Vantage Generic HVAC RS485 zone child device without fan speed."""

class Meta:
name = "Vantage.Generic_HVAC_RS485_Zone_without_FanSpeed_CHILD"

@dataclass(kw_only=True)
class IndoorSettings:
indoor_sensor: int = field(metadata={"name": "indoorSensor"})
indoor_temp_offset: str = "0"

indoor_settings: IndoorSettings
position_number: int = 1
77 changes: 77 additions & 0 deletions src/aiovantage/_objects/vantage_hvac_iu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Vantage HVAC-IU objects."""

from dataclasses import dataclass, field

from aiovantage.object_interfaces import FanInterface, ThermostatInterface

from .child_device import ChildDevice
from .port_device import PortDevice


@dataclass(kw_only=True)
class VantageHVACIUPort(PortDevice):
"""Vantage HVAC-IU port device."""

class Meta:
name = "Vantage.HVAC-IU_PORT"

temperature_format: str = field(
default="Celcius", # NOTE: Intentional typo to match the underlying object
metadata={"name": "aTemperatureFormat"},
)
outdoor_sensor: int
pauze_time: int = 1 # NOTE: Intentional typo to match the underlying object
serial_number: str = "0"


@dataclass(kw_only=True)
class VantageHVACIULineChild(ChildDevice):
"""Vantage HVAC-IU line child device."""

class Meta:
name = "Vantage.HVAC-IU-Line_CHILD"

@dataclass(kw_only=True)
class OperationModes:
auto: bool = True
cool: bool = True
heat: bool = True

@dataclass(kw_only=True)
class FanSpeeds:
auto: bool = True
high: bool = True
low: bool = True
max: bool = True
med: bool = True

device_type: str = "Daikin"
line_number: int = 1
operation_modes: OperationModes
fan_speeds: FanSpeeds = field(metadata={"name": "xFanSpeeds"})


@dataclass(kw_only=True)
class VantageHVACIUZoneChild(ChildDevice, ThermostatInterface, FanInterface):
"""Vantage HVAC-IU zone child device."""

class Meta:
name = "Vantage.HVAC-IU-Zone_CHILD"

@dataclass(kw_only=True)
class IndoorSensor:
indoor_sensor: int
indoor_temp_offset: str = "0"

main_zone: str = field(
metadata={"name": "ZoneNumber", "wrapper": "aMainZonePlaceHolder"}
)

grouped_zones: list[str] = field(
default_factory=list,
metadata={"name": "ZoneNumberChild", "wrapper": "bGroupZonePlaceHolder"},
)

indoor_sensors: list[IndoorSensor] = field(
default_factory=list, metadata={"name": "cIndoorSensors"}
)
Loading

0 comments on commit a9af9ad

Please sign in to comment.