Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/demetriek/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
LaMetricConnectionError,
LaMetricConnectionTimeoutError,
LaMetricError,
LaMetricUnsupportedError,
)
from .models import (
Application,
Audio,
Bluetooth,
Chart,
Expand All @@ -37,11 +39,13 @@
Sound,
SoundURL,
User,
Widget,
Wifi,
)

__all__ = [
"AlarmSound",
"Application",
"Audio",
"Bluetooth",
"BrightnessMode",
Expand All @@ -60,6 +64,7 @@
"LaMetricConnectionTimeoutError",
"LaMetricDevice",
"LaMetricError",
"LaMetricUnsupportedError",
"Model",
"Notification",
"NotificationIconType",
Expand All @@ -72,6 +77,7 @@
"Sound",
"SoundURL",
"User",
"Widget",
"Wifi",
"WifiMode",
]
152 changes: 148 additions & 4 deletions src/demetriek/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
LaMetricConnectionError,
LaMetricConnectionTimeoutError,
LaMetricError,
LaMetricUnsupportedError,
)
from .models import (
Application,
Audio,
Bluetooth,
Device,
Display,
Notification,
Widget,
Wifi,
)

Expand Down Expand Up @@ -73,6 +76,8 @@ async def _request(

Raises:
------
LaMetricUnsupportedError: If the requested endpoint is not supported
by the current API version of the LaMetric device.
LaMetricAuthenticationError: If the API key is invalid.
LaMetricConnectionError: An error occurred while communication with
the LaMetric device.
Expand Down Expand Up @@ -117,6 +122,12 @@ async def _request(
if exception.status in [401, 403]:
msg = f"Authentication to the LaMetric device at {self.host} failed"
raise LaMetricAuthenticationError(msg) from exception
if exception.status == 404:
msg = (
f"The requested endpoint {uri} is not supported in the current"
f" API version by the LaMetric device at {self.host}."
)
raise LaMetricUnsupportedError(msg) from exception
msg = (
f"Error occurred while connecting to the LaMetric device at {self.host}"
)
Expand Down Expand Up @@ -264,20 +275,153 @@ async def wifi(self) -> Wifi:
data.update(ip=data.get("ipv4"), rssi=data.get("signal_strength"))
return Wifi.from_dict(data)

async def apps(self) -> dict[str, Application] | None:
"""Get installed apps on LaMetric device.

Note: This feature is only available on API v2.1.0+
Devices with OS version < 2.1.0 may not support this.

Returns
-------
A dictionary of Application objects keyed by package name,
or None if the device doesn't support apps API.

"""
try:
response = await self._request("/api/v2/device/apps")
return {
pkg: Application.from_dict(app_data)
for pkg, app_data in response.items()
}
except LaMetricUnsupportedError:
return None

async def app(self, *, package: str) -> Application | None:
"""Get details of a specific app.

Args:
----
package: The package name of the app (e.g., 'com.lametric.clock').

Returns:
-------
An Application object with the app details.

"""
try:
response = await self._request(f"/api/v2/device/apps/{package}")
return Application.from_dict(response)
except LaMetricUnsupportedError:
return None

async def widget(self, *, package: str, widget_id: str) -> Widget | None:
"""Get details of a specific widget.

Args:
----
package: The package name of the app (e.g., 'com.lametric.clock').
widget_id: The widget ID.

Returns:
-------
A Widget object with the widget details.

"""
try:
response = await self._request(
f"/api/v2/device/apps/{package}/widgets/{widget_id}"
)
return Widget.from_dict(response)
except LaMetricUnsupportedError:
return None

async def app_next(self) -> None:
"""Switch to the next app on LaMetric Time.
"""Switch to the next app on LaMetric device.

App order is controlled by the user via LaMetric Time app.
App order is controlled by the user via LaMetric mobile app.
"""
await self._request("/api/v2/device/apps/next", method=hdrs.METH_PUT)

async def app_previous(self) -> None:
"""Switch to the next app on LaMetric Time.
"""Switch to the previous app on LaMetric device.

App order is controlled by the user via LaMetric Time app.
App order is controlled by the user via LaMetric mobile app.
"""
await self._request("/api/v2/device/apps/prev", method=hdrs.METH_PUT)

async def app_activate(self, *, package: str, widget_id: str) -> None:
"""Activate a specific app widget on LaMetric device.

Args:
----
package: The package name of the app (e.g., 'com.lametric.clock').
widget_id: The widget ID to activate.

"""
await self._request(
f"/api/v2/device/apps/{package}/widgets/{widget_id}/activate",
method=hdrs.METH_PUT,
)

# pylint: disable=too-many-arguments
async def widget_action(
self,
*,
package: str,
widget_id: str,
action_id: str,
parameters: dict[str, Any] | None = None,
activate: bool = True,
) -> None:
"""Interact with a running widget by triggering an action.

Args:
----
package: The package name of the app (e.g., 'com.lametric.clock').
widget_id: The widget ID.
action_id: The action to trigger (e.g., 'button.press', 'clock.alarm').
parameters: Optional parameters for the action.
activate: Whether to activate the widget when performing the action.

"""
payload = {
"id": action_id,
"params": parameters or {},
"activate": activate,
}
await self._request(
f"/api/v2/device/apps/{package}/widgets/{widget_id}/actions",
method=hdrs.METH_POST,
data=payload,
)

async def widget_update(
self,
*,
package: str,
widget_id: str,
settings: dict[str, Any],
) -> Widget:
"""Update widget settings.

Args:
----
package: The package name of the app (e.g., 'com.lametric.clock').
widget_id: The widget ID.
settings: The settings to update.

Returns:
-------
A Widget object with the updated widget details.

"""
response = await self._request(
f"/api/v2/device/apps/{package}/widgets/{widget_id}",
method=hdrs.METH_PUT,
data=settings,
)
return Widget.from_dict(response)

async def notify(
self,
*,
Expand Down
4 changes: 4 additions & 0 deletions src/demetriek/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ class LaMetricAuthenticationError(LaMetricError):

class LaMetricConnectionTimeoutError(LaMetricConnectionError):
"""LaMetric connection Timeout exception."""


class LaMetricUnsupportedError(LaMetricError):
"""LaMetric API feature not supported on this device version."""
43 changes: 34 additions & 9 deletions src/demetriek/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass, field
from datetime import datetime
from ipaddress import IPv4Address
from typing import Any

from awesomeversion import AwesomeVersion
from mashumaro import field_options
Expand Down Expand Up @@ -39,21 +40,21 @@ class Audio(DataClassORJSONMixin):
"""Object holding the audio state of an LaMetric device."""

available: bool = True
volume: int | None
volume_limit: Range | None
volume_range: Range | None
volume: int | None = None
volume_limit: Range | None = None
volume_range: Range | None = None


@dataclass(kw_only=True)
class Bluetooth(DataClassORJSONMixin):
"""Object holding the Bluetooth state of an LaMetric device."""

active: bool
address: str
available: bool
discoverable: bool
name: str
pairable: bool
active: bool | None = None
address: str | None = None
available: bool = False
discoverable: bool | None = None
name: str | None = None
pairable: bool | None = None


@dataclass(kw_only=True)
Expand Down Expand Up @@ -267,3 +268,27 @@ class CloudDevice(DataClassORJSONMixin):
ssid: str = field(metadata=field_options(alias="wifi_ssid"))
state: DeviceState
updated_at: datetime


@dataclass(kw_only=True)
class Widget(DataClassORJSONMixin):
"""Object holding LaMetric Widget information."""

index: int
package: str
settings: dict[str, Any] | None = None
visible: bool | None = None


@dataclass(kw_only=True)
class Application(DataClassORJSONMixin):
"""Object holding LaMetric Application information."""

actions: dict[str, dict[str, Any]] | None = None
package: str
title: str | None = None
triggers: dict[str, dict[str, Any]] | None = None
vendor: str
version: str
version_code: str
widgets: dict[str, Widget] | None = None
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Asynchronous Python client for LaMetric TIME devices."""
"""Asynchronous Python client for LaMetric devices."""

from pathlib import Path

Expand Down
31 changes: 31 additions & 0 deletions tests/__snapshots__/test_app.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# serializer version: 1
# name: test_app
dict({
'actions': dict({
'clock.alarm': dict({
'id': 'clock.alarm',
'title': 'Alarm',
'type': 'native',
}),
'clock.clockface': dict({
'id': 'clock.clockface',
'title': 'Clock Face',
'type': 'native',
}),
}),
'package': 'com.lametric.clock',
'title': 'Clock',
'triggers': None,
'vendor': 'LaMetric',
'version': '1.0.17',
'version_code': '23',
'widgets': dict({
'08b8eac21074f8f7e5a29f2855ba8060': dict({
'index': 0,
'package': 'com.lametric.clock',
'settings': None,
'visible': True,
}),
}),
})
# ---
Loading