diff --git a/src/aiovantage/_config_client/client.py b/src/aiovantage/_config_client/client.py index 7a20dff..71e3fdd 100644 --- a/src/aiovantage/_config_client/client.py +++ b/src/aiovantage/_config_client/client.py @@ -131,7 +131,7 @@ async def raw_request(self, request: str, separator: str) -> str: return response - async def rpc_call( + async def rpc( self, interface_cls: type[Interface], method_cls: type[Method[Call, Return]], diff --git a/src/aiovantage/_config_client/interfaces/configuration.py b/src/aiovantage/_config_client/interfaces/configuration.py index 203326e..d662b05 100644 --- a/src/aiovantage/_config_client/interfaces/configuration.py +++ b/src/aiovantage/_config_client/interfaces/configuration.py @@ -1,5 +1,3 @@ -"""IConfiguration interfaces.""" - from collections.abc import AsyncIterator from contextlib import suppress from dataclasses import dataclass, field @@ -13,13 +11,16 @@ @dataclass -class OpenFilter: - """IConfiguration.OpenFilter method definition.""" +class WrappedObject: + # Wildcard type that can be used to represent any object. + vid: int = field(metadata={"name": "VID", "type": "Attribute"}) + obj: object = field(metadata={"type": "Wildcard"}) + +@dataclass +class OpenFilter: @dataclass class Params: - """Method parameters.""" - object_types: list[str] | None = field( default=None, metadata={"wrapper": "Objects", "name": "ObjectType", "type": "Element"}, @@ -32,25 +33,14 @@ class Params: @dataclass class GetFilterResults: - """IConfiguration.GetFilterResults method definition.""" - @dataclass class Params: - """Method parameters.""" - h_filter: int = field(metadata={"name": "hFilter"}) count: int = 50 whole_object: bool = True - @dataclass - class Object: - """Wildcard type that can be used to represent any object.""" - - vid: int = field(metadata={"name": "VID", "type": "Attribute"}) - obj: object = field(metadata={"type": "Wildcard"}) - call: Params | None = field(default=None, metadata={"name": "call"}) - result: list[Object] | None = field( + result: list[WrappedObject] | None = field( default_factory=list, metadata={"wrapper": "return", "name": "Object", "type": "Element"}, ) @@ -58,29 +48,18 @@ class Object: @dataclass class CloseFilter: - """IConfiguration.CloseFilter method definition.""" - call: int | None = field(default=None, metadata={"name": "call"}) result: bool | None = field(default=None, metadata={"name": "return"}) @dataclass class GetObject: - """IConfiguration.GetObject method definition.""" - - @dataclass - class Object: - """Wildcard type that can be used to represent any object.""" - - vid: int = field(metadata={"name": "VID", "type": "Attribute"}) - obj: object = field(metadata={"type": "Wildcard"}) - call: list[int] | None = field( default_factory=list, metadata={"wrapper": "call", "name": "VID", "type": "Element"}, ) - result: list[Object] | None = field( + result: list[WrappedObject] | None = field( default_factory=list, metadata={"wrapper": "return", "name": "Object", "type": "Element"}, ) @@ -88,8 +67,6 @@ class Object: @dataclass(kw_only=True) class IConfiguration: - """IConfiguration interface.""" - open_filter: OpenFilter | None = None get_filter_results: GetFilterResults | None = None close_filter: CloseFilter | None = None @@ -113,7 +90,7 @@ async def open_filter( Returns: The handle of the opened filter """ - return await client.rpc_call( + return await client.rpc( IConfiguration, OpenFilter, OpenFilter.Params(object_types=list(object_types), xpath=xpath), @@ -122,7 +99,7 @@ async def open_filter( @staticmethod async def get_filter_results( client: ConfigClient, h_filter: int, count: int = 50, whole_object: bool = True - ) -> list[GetFilterResults.Object]: + ) -> list[WrappedObject]: """Get results from a filter handle previously opened with open_filter. Args: @@ -134,7 +111,7 @@ async def get_filter_results( Returns: A list of Vantage objects """ - return await client.rpc_call( + return await client.rpc( IConfiguration, GetFilterResults, GetFilterResults.Params(h_filter) ) @@ -149,10 +126,10 @@ async def close_filter(client: ConfigClient, h_filter: int) -> bool: Returns: True if the filter was closed successfully, False otherwise """ - return await client.rpc_call(IConfiguration, CloseFilter, h_filter) + return await client.rpc(IConfiguration, CloseFilter, h_filter) @staticmethod - async def get_object(client: ConfigClient, *vids: int) -> list[GetObject.Object]: + async def get_object(client: ConfigClient, *vids: int) -> list[WrappedObject]: """Get one or more Vantage objects by their VIDs. Args: @@ -162,7 +139,7 @@ async def get_object(client: ConfigClient, *vids: int) -> list[GetObject.Object] Returns: A list of Vantage objects """ - return await client.rpc_call(IConfiguration, GetObject, list(vids)) + return await client.rpc(IConfiguration, GetObject, list(vids)) # Convenience functions, not part of the interface @overload diff --git a/src/aiovantage/_config_client/interfaces/introspection.py b/src/aiovantage/_config_client/interfaces/introspection.py index 9148adf..a0fb347 100644 --- a/src/aiovantage/_config_client/interfaces/introspection.py +++ b/src/aiovantage/_config_client/interfaces/introspection.py @@ -1,5 +1,3 @@ -"""IIntrospection interface.""" - from dataclasses import dataclass, field from ..client import ConfigClient @@ -7,12 +5,8 @@ @dataclass class GetInterfaces: - """IIntrospection.GetInterfaces method definition.""" - @dataclass class Interface: - """Object interface definition.""" - name: str version: str iid: int = field(metadata={"name": "IID"}) @@ -26,12 +20,8 @@ class Interface: @dataclass class GetSysInfo: - """IIntrospection.GetSysInfo method definition.""" - @dataclass class SysInfo: - """SysInfo class.""" - master_number: int serial_number: int @@ -43,12 +33,8 @@ class SysInfo: @dataclass class GetTypes: - """IIntrospection.GetTypes method definition.""" - @dataclass class Type: - """Object type definition.""" - name: str version: str @@ -60,12 +46,8 @@ class Type: @dataclass class GetVersion: - """IIntrospection.GetVersion method definition.""" - @dataclass class Version: - """Method return value.""" - kernel: str | None = field(default=None, metadata={"name": "kernel"}) rootfs: str | None = field(default=None, metadata={"name": "rootfs"}) app: str | None = field(default=None, metadata={"name": "app"}) @@ -76,8 +58,6 @@ class Version: @dataclass(kw_only=True) class IIntrospection: - """IIntrospection interface.""" - get_interfaces: GetInterfaces | None = None get_sys_info: GetSysInfo | None = None get_types: GetTypes | None = None @@ -97,7 +77,7 @@ async def get_interfaces(client: ConfigClient) -> list[GetInterfaces.Interface]: Returns: A list of interfaces. """ - return await client.rpc_call(IIntrospection, GetInterfaces) + return await client.rpc(IIntrospection, GetInterfaces) @staticmethod async def get_sys_info(client: ConfigClient) -> GetSysInfo.SysInfo: @@ -109,7 +89,7 @@ async def get_sys_info(client: ConfigClient) -> GetSysInfo.SysInfo: Returns: A system information object. """ - return await client.rpc_call(IIntrospection, GetSysInfo) + return await client.rpc(IIntrospection, GetSysInfo) @staticmethod async def get_types(client: ConfigClient) -> list[GetTypes.Type]: @@ -121,7 +101,7 @@ async def get_types(client: ConfigClient) -> list[GetTypes.Type]: Returns: A list of object types. """ - return await client.rpc_call(IIntrospection, GetTypes) + return await client.rpc(IIntrospection, GetTypes) @staticmethod async def get_version(client: ConfigClient) -> GetVersion.Version: @@ -133,4 +113,4 @@ async def get_version(client: ConfigClient) -> GetVersion.Version: Returns: A version information object. """ - return await client.rpc_call(IIntrospection, GetVersion) + return await client.rpc(IIntrospection, GetVersion) diff --git a/src/aiovantage/_config_client/interfaces/login.py b/src/aiovantage/_config_client/interfaces/login.py index 660817e..7e528bc 100644 --- a/src/aiovantage/_config_client/interfaces/login.py +++ b/src/aiovantage/_config_client/interfaces/login.py @@ -1,5 +1,3 @@ -"""ILogin.Login method definition.""" - from dataclasses import dataclass, field from ..client import ConfigClient @@ -7,12 +5,8 @@ @dataclass class Login: - """ILogin.Login method definition.""" - @dataclass class Params: - """Method parameters.""" - user: str password: str @@ -22,8 +16,6 @@ class Params: @dataclass(kw_only=True) class ILogin: - """ILogin interface.""" - login: Login | None = None @@ -42,6 +34,6 @@ async def login(client: ConfigClient, user: str, password: str) -> bool: Returns: True if the login was successful, False otherwise """ - return await client.rpc_call( + return await client.rpc( ILogin, Login, Login.Params(user=user, password=password) ) diff --git a/src/aiovantage/_controllers/blind_groups.py b/src/aiovantage/_controllers/blind_groups.py index 9b0d0c5..6939f1e 100644 --- a/src/aiovantage/_controllers/blind_groups.py +++ b/src/aiovantage/_controllers/blind_groups.py @@ -2,10 +2,11 @@ from .base import BaseController +BlindGroupTypes = BlindGroup | SomfyRS485GroupChild | SomfyURTSI2GroupChild +"""Types managed by the blind groups controller.""" -class BlindGroupsController( - BaseController[BlindGroup | SomfyRS485GroupChild | SomfyURTSI2GroupChild] -): + +class BlindGroupsController(BaseController[BlindGroupTypes]): """Blind groups controller.""" vantage_types = ( diff --git a/src/aiovantage/_controllers/blinds.py b/src/aiovantage/_controllers/blinds.py index b86430f..85967ea 100644 --- a/src/aiovantage/_controllers/blinds.py +++ b/src/aiovantage/_controllers/blinds.py @@ -1,4 +1,5 @@ from aiovantage.objects import ( + BlindGroup, QISBlind, QubeBlind, RelayBlind, @@ -7,6 +8,7 @@ ) from .base import BaseController +from .query import QuerySet BlindTypes = ( QISBlind | QubeBlind | RelayBlind | SomfyRS485ShadeChild | SomfyURTSI2ShadeChild @@ -24,3 +26,7 @@ class BlindsController(BaseController[BlindTypes]): "Somfy.RS-485_Shade_CHILD", "Somfy.URTSI_2_Shade_CHILD", ) + + def in_blind_group(self, blind_group: BlindGroup) -> QuerySet[BlindTypes]: + """Return a queryset of all loads in the given blind group.""" + return self.filter(lambda load: load.vid in blind_group.blind_table) diff --git a/src/aiovantage/_controllers/load_groups.py b/src/aiovantage/_controllers/load_groups.py index 41c90dc..d63d736 100644 --- a/src/aiovantage/_controllers/load_groups.py +++ b/src/aiovantage/_controllers/load_groups.py @@ -1,5 +1,4 @@ -from aiovantage._controllers.query import QuerySet -from aiovantage.objects import Load, LoadGroup +from aiovantage.objects import LoadGroup from .base import BaseController @@ -8,9 +7,3 @@ class LoadGroupsController(BaseController[LoadGroup]): """Load groups controller.""" vantage_types = ("LoadGroup",) - - def loads(self, vid: int) -> QuerySet[Load]: - """Return a queryset of all loads in this load group.""" - load_group = self[vid] - - return self._vantage.loads.filter(lambda load: load.id in load_group.load_table) diff --git a/src/aiovantage/_controllers/loads.py b/src/aiovantage/_controllers/loads.py index 8fe4a2d..8b9da7d 100644 --- a/src/aiovantage/_controllers/loads.py +++ b/src/aiovantage/_controllers/loads.py @@ -1,7 +1,7 @@ -from aiovantage._controllers.query import QuerySet -from aiovantage.objects import Load +from aiovantage.objects import Load, LoadGroup from .base import BaseController +from .query import QuerySet class LoadsController(BaseController[Load]): @@ -33,3 +33,7 @@ def motors(self) -> QuerySet[Load]: def lights(self) -> QuerySet[Load]: """Return a queryset of all loads that are lights.""" return self.filter(lambda load: load.is_light) + + def in_load_group(self, load_group: LoadGroup) -> QuerySet[Load]: + """Return a queryset of all loads in the given load group.""" + return self.filter(lambda load: load.vid in load_group.load_table) diff --git a/src/aiovantage/_controllers/rgb_loads.py b/src/aiovantage/_controllers/rgb_loads.py index 9c2c66a..bfa222e 100644 --- a/src/aiovantage/_controllers/rgb_loads.py +++ b/src/aiovantage/_controllers/rgb_loads.py @@ -1,7 +1,7 @@ -from aiovantage._controllers.query import QuerySet from aiovantage.objects import VantageDDGColorLoad, VantageDGColorLoad from .base import BaseController +from .query import QuerySet RGBLoadTypes = VantageDDGColorLoad | VantageDGColorLoad """Types managed by the RGB loads controller.""" diff --git a/src/aiovantage/_object_interfaces/rgb_load.py b/src/aiovantage/_object_interfaces/rgb_load.py index 5fef2eb..34ad850 100644 --- a/src/aiovantage/_object_interfaces/rgb_load.py +++ b/src/aiovantage/_object_interfaces/rgb_load.py @@ -441,29 +441,35 @@ async def get_transition_level(self) -> Decimal: return await self.invoke("RGBLoad.GetTransitionLevel") # Convenience functions, not part of the interface - async def get_rgb_color(self) -> tuple[int, ...]: + async def get_rgb_color(self) -> tuple[int, int, int]: """Get the RGB color of a load from the controller. Returns: The value of the RGB color as a tuple of (red, green, blue). """ - return tuple([await self.get_rgb(chan) for chan in islice(self.RGBChannel, 3)]) + return tuple[int, int, int]( + [await self.get_rgb(chan) for chan in islice(self.RGBChannel, 3)] + ) - async def get_rgbw_color(self) -> tuple[int, ...]: + async def get_rgbw_color(self) -> tuple[int, int, int, int]: """Get the RGBW color of a load from the controller. Returns: The value of the RGBW color as a tuple of (red, green, blue, white). """ - return tuple([await self.get_rgbw(chan) for chan in self.RGBChannel]) + return tuple[int, int, int, int]( + [await self.get_rgbw(chan) for chan in self.RGBChannel] + ) - async def get_hsl_color(self) -> tuple[int, ...]: + async def get_hsl_color(self) -> tuple[int, int, int]: """Get the HSL color of a load from the controller. Returns: The value of the HSL color as a tuple of (hue, saturation, lightness). """ - return tuple([await self.get_hsl(attr) for attr in self.HSLAttribute]) + return tuple[int, int, int]( + [await self.get_hsl(attr) for attr in self.HSLAttribute] + ) # Status updates for GetRGB, GetRGBW, and GetHSL arrive on multiple lines, # one line per channel/attribute. We always want to fetch a complete set of diff --git a/src/aiovantage/controllers.py b/src/aiovantage/controllers.py index f44d329..9b61853 100644 --- a/src/aiovantage/controllers.py +++ b/src/aiovantage/controllers.py @@ -31,7 +31,7 @@ from ._controllers.areas import AreasController from ._controllers.back_boxes import BackBoxesController from ._controllers.base import BaseController -from ._controllers.blind_groups import BlindGroupsController +from ._controllers.blind_groups import BlindGroupsController, BlindGroupTypes from ._controllers.blinds import BlindsController, BlindTypes from ._controllers.buttons import ButtonsController from ._controllers.dry_contacts import DryContactsController @@ -57,6 +57,7 @@ "BackBoxesController", "BaseController", "BlindGroupsController", + "BlindGroupTypes", "BlindsController", "BlindTypes", "ButtonsController",