Skip to content

Commit

Permalink
Add error class wrappers to ConfigClient, add xpath support to get_ob…
Browse files Browse the repository at this point in the history
…jects helper
  • Loading branch information
loopj committed Jun 17, 2023
1 parent c51dd43 commit 5befbe5
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 44 deletions.
14 changes: 7 additions & 7 deletions examples/config_client/dump_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging

from aiovantage.config_client import ConfigClient
from aiovantage.config_client.helpers import get_objects_by_type
from aiovantage.config_client.helpers import get_objects


# Grab connection info from command line arguments
Expand All @@ -20,21 +20,21 @@ async def main() -> None:
logging.basicConfig(level=logging.DEBUG)

async with ConfigClient(args.host, args.username, args.password) as client:
# Dump all Areas using the get_objects_by_type helper
# Dump all Areas using the get_objects helper
print("# Vantage Areas")
async for area in get_objects_by_type(client, ["Area"]):
async for area in get_objects(client, type="Area"):
print(area)
print()

# Dump all Loads using the get_objects_by_type helper
# Dump all Loads using the get_objects helper
print("# Vantage Loads")
async for load in get_objects_by_type(client, ["Load"]):
async for load in get_objects(client, type="Load"):
print(load)
print()

# Dump some StationObjects using the get_objects_by_type helper
# Dump some StationObjects using the get_objects helper
print("# Vantage Stations")
async for station in get_objects_by_type(client, ("Keypad", "EqCtrl")):
async for station in get_objects(client, type=("Keypad", "EqCtrl")):
print(station)
print()

Expand Down
4 changes: 2 additions & 2 deletions src/aiovantage/config_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ Additionally, a few helper functions for fetching objects are provided by

```python
from aiovantage.config_client import ConfigClient
from aiovantage.config_client.helpers import get_objects_by_type
from aiovantage.config_client.helpers import get_objects

async with ConfigClient("hostname") as client:
loads = get_objects_by_type(client, ["Load", "Button"])
loads = get_objects(client, type=["Load", "Button"])
```

### Lookup objects by id, using a helper
Expand Down
59 changes: 36 additions & 23 deletions src/aiovantage/config_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig

from .errors import ClientConnectionError, ClientTimeoutError
from .methods import CallType, Method, ReturnType
from .methods.login import Login

Expand Down Expand Up @@ -154,16 +155,22 @@ async def raw_request(self, interface: str, payload: str) -> str:
# Get a connection
reader, writer = await self._get_connection()

# Send the request
request = f"<{interface}>{payload}</{interface}>"
writer.write(request.encode())
await writer.drain()
try:
# Send the request
request = f"<{interface}>{payload}</{interface}>"
writer.write(request.encode())
await writer.drain()

# Fetch the response
end_bytes = f"</{interface}>\n".encode()
data = await asyncio.wait_for(
reader.readuntil(end_bytes), timeout=self._read_timeout
)
# Fetch the response
end_bytes = f"</{interface}>\n".encode()
data = await asyncio.wait_for(
reader.readuntil(end_bytes), timeout=self._read_timeout
)

except asyncio.TimeoutError as err:
raise ClientTimeoutError from err
except (OSError, asyncio.IncompleteReadError) as err:
raise ClientConnectionError from err

return data.decode()

Expand Down Expand Up @@ -198,21 +205,27 @@ async def _get_connection(self) -> Connection:
if self._connection is not None and not self._connection[1].is_closing():
return self._connection

# Otherwise, open a new connection
connection = await asyncio.wait_for(
asyncio.open_connection(
self._host,
self._port,
ssl=self._ssl_context,
limit=2**20,
),
timeout=self._conn_timeout,
)
self._connection = connection
self._logger.info("Connected")
try:
# Otherwise, open a new connection
connection = await asyncio.wait_for(
asyncio.open_connection(
self._host,
self._port,
ssl=self._ssl_context,
limit=2**20,
),
timeout=self._conn_timeout,
)
self._connection = connection
self._logger.info("Connected")

# Login if we have a username and password
await self._login()

# Login if we have a username and password
await self._login()
except asyncio.TimeoutError as err:
raise ClientTimeoutError from err
except OSError as err:
raise ClientConnectionError from err

return connection

Expand Down
13 changes: 13 additions & 0 deletions src/aiovantage/config_client/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import asyncio


class ClientError(Exception):
pass


class ClientConnectionError(ClientError):
pass


class ClientTimeoutError(asyncio.TimeoutError, ClientConnectionError):
pass
31 changes: 21 additions & 10 deletions src/aiovantage/config_client/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, AsyncIterator, Sequence
from typing import Any, AsyncIterator, Optional, Sequence

from aiovantage.config_client import ConfigClient
from aiovantage.config_client.methods.configuration import (
Expand All @@ -9,27 +9,38 @@
)


async def get_objects_by_type(
client: ConfigClient, types: Sequence[str]
async def get_objects(
client: ConfigClient,
*,
type: Optional[Sequence[str]] = None,
xpath: Optional[str] = None,
) -> AsyncIterator[Any]:
"""
Helper function to get all vantage system objects of the specified types
Args:
client: The ACI client instance
types: A list of strings of the Vantage object types to fetch
object_types: An optional string, or list of strings of object types to fetch
xpath: An optional xpath to filter the results by, eg. "/Load", "/*[@VID='12']"
Yields:
The objects of the specified types
"""

try:
# Open the filter
handle = await client.request(
OpenFilter,
OpenFilter.Params(objects=OpenFilter.Filter(object_type=list(types))),
)
# Support both a single object type and a list of status types
if isinstance(type, str):
type_filter = OpenFilter.Filter(object_type=[type])
elif type is None:
type_filter = None
else:
type_filter = OpenFilter.Filter(object_type=list(type))

# Open the filter
handle = await client.request(
OpenFilter, OpenFilter.Params(objects=type_filter, xpath=xpath)
)

try:
# Get the results
while True:
response = await client.request(
Expand Down
4 changes: 2 additions & 2 deletions src/aiovantage/controllers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from aiovantage.command_client import CommandClient, Event, EventType
from aiovantage.command_client.helpers import tokenize_response
from aiovantage.config_client import ConfigClient
from aiovantage.config_client.helpers import get_objects_by_type
from aiovantage.config_client.helpers import get_objects
from aiovantage.config_client.objects import SystemObject
from aiovantage.events import VantageEvent
from aiovantage.query import QuerySet
Expand Down Expand Up @@ -87,7 +87,7 @@ async def fetch_objects(self) -> None:
# - fire OBJECT_ADDED events for new objects
# - fire OBJECT_REMOVED events for removed objects

async for obj in get_objects_by_type(self.config_client, self.vantage_types):
async for obj in get_objects(self.config_client, type=self.vantage_types):
self._items[obj.id] = cast(T, obj)
self.emit(VantageEvent.OBJECT_ADDED, cast(T, obj))

Expand Down

0 comments on commit 5befbe5

Please sign in to comment.