lametric-py
Async Python client for LaMetric devices and the LaMetric cloud API. Wraps the local device API, notification models, and the LaMetric Streaming Protocol (LMSP).
Local Device API
Control display, audio, Bluetooth, apps, and notifications over HTTPS.
Notifications
Text, goal, and chart frames with built-in or custom sounds.
LMSP Streaming
Push RGB888 frames to LaMetric SKY over UDP via the streaming protocol.
Cloud API
Fetch your user profile and registered devices from developer.lametric.com.
Installation
The package is installed from source using uv:
git clone <repository-url>
cd lametric-py
uv sync
Or with pip:
pip install -e .
Local Device Client
LaMetricDevice connects to the local HTTPS API on port 4343. Use it as an
async context manager — it manages the underlying aiohttp session for you.
import asyncio
from lametric import LaMetricDevice
async def main() -> None:
async with LaMetricDevice(host="192.168.1.42", api_key="device-api-key") as device:
state = await device.state
print(state.display.brightness)
asyncio.run(main())
The device IP and API key are visible in the LaMetric app under Settings → Wi-Fi and Settings → Developer → API Key.
Display & Audio
Basic settings
await device.set_audio(25)
await device.set_display(on=True, brightness=75)
await device.set_bluetooth(active=True, name="Office SKY")
Brightness mode
from lametric import BrightnessMode
await device.set_display(brightness_mode=BrightnessMode.AUTO)
Screensaver
from datetime import datetime
from lametric import ScreensaverConfig, ScreensaverConfigParams, ScreensaverModes
await device.set_display(
screensaver_config=ScreensaverConfig(
enabled=True,
mode=ScreensaverModes.TIME_BASED,
mode_params=ScreensaverConfigParams(
enabled=True,
start_time_gmt=datetime(2000, 1, 1, 22, 0),
end_time_gmt=datetime(2000, 1, 1, 7, 0),
),
)
)
App Management
apps = await device.installed_apps # dict[str, App]
weather = await device.get_installed_app("com.lametric.weather")
await device.activate_next_app()
await device.activate_previous_app()
await device.activate_widget(app_id="com.example.app", widget_id="main")
Widget actions
await device.activate_action(
app_id="com.example.app",
widget_id="main",
action_id="my_action",
action_parameters={"key": "value"},
visible=True,
)
Notifications
Send and dismiss
from lametric import (
BuiltinSound, Notification, NotificationData,
NotificationPriority, NotificationSound, SimpleFrame,
)
notification = Notification(
priority=NotificationPriority.INFO,
model=NotificationData(
frames=[SimpleFrame(text="Build successful")],
sound=BuiltinSound(id=NotificationSound.POSITIVE1),
),
)
notification_id = await device.send_notification(notification)
await device.dismiss_notification(notification_id)
# Dismiss helpers
await device.dismiss_current_notification()
await device.dismiss_all_notifications()
Read the queue
queued = await device.notifications # list[Notification]
current = await device.current_notification # Notification | None
Frame types
from lametric import GoalFrame, GoalFrameData, SpikeChartFrame
goal_frame = GoalFrame(
icon="i1234",
goal_data=GoalFrameData(start=0, current=42, end=100, unit="%"),
)
chart_frame = SpikeChartFrame(chart_data=[1, 3, 5, 3, 1, 0, 2])
Sound types
BuiltinSound infers its category automatically from the sound identifier:
from lametric import AlarmSound, BuiltinSound, NotificationSound
alarm = BuiltinSound(id=AlarmSound.ALARM1) # category → ALARMS
notify = BuiltinSound(id=NotificationSound.CASH) # category → NOTIFICATIONS
WebSound streams audio from a URL with an optional built-in fallback:
from lametric import WebSound
sound = WebSound(
url="https://example.com/alert.mp3",
fallback=BuiltinSound(id=NotificationSound.NOTIFICATION),
)
LMSP Streaming
LaMetric SKY supports the LaMetric Streaming Protocol (LMSP) over UDP. The flow is:
- Read stream metadata —
await device.stream_state - Start a stream —
await device.start_stream(config) - Send RGB888 frames —
await device.send_stream_data(session_id, frame) - Stop the stream —
await device.stop_stream()
Start a Stream
from lametric import (
CanvasFillType, CanvasPostProcess,
CanvasPostProcessType, CanvasRenderMode, StreamConfig,
)
config = StreamConfig(
fill_type=CanvasFillType.SCALE,
render_mode=CanvasRenderMode.PIXEL,
post_process=CanvasPostProcess(type=CanvasPostProcessType.NONE),
)
session_id = await device.start_stream(config)
Post-processing effects
from lametric import (
CanvasFadingPixelsEffectParameters, CanvasPostProcessEffectType,
CanvasPostProcessParameters,
)
config = StreamConfig(
fill_type=CanvasFillType.SCALE,
render_mode=CanvasRenderMode.PIXEL,
post_process=CanvasPostProcess(
type=CanvasPostProcessType.EFFECT,
params=CanvasPostProcessParameters(
effect_type=CanvasPostProcessEffectType.FADING_PIXELS,
effect_params=CanvasFadingPixelsEffectParameters(
smooth=True, pixel_fill=0.8, fade_speed=0.5, pixel_base=0.2,
),
),
),
)
Send Frames
send_stream_data builds a complete LMSP UDP packet and delivers it to the device. The
packet layout (little-endian) is:
| Field | Size | Notes |
|---|---|---|
| Protocol name | 4 bytes | From stream state |
| Version | 2 bytes | From stream state |
| Session ID | 16 bytes | Returned by start_stream |
| Content encoding | 1 byte | 0x00 = raw RGB888 |
| Reserved | 1 byte | |
| Canvas area count | 1 byte | Always 1 (full canvas) |
| Reserved | 1 byte | |
| X / Y / Width / Height | 2 bytes each | Canvas dimensions |
| Data length | 2 bytes | |
| Pixel data | variable | RGB888 bytes |
stream_state = await device.stream_state
width = stream_state.canvas.pixel.size.width
height = stream_state.canvas.pixel.size.height
frame = bytes([255, 0, 0] * (width * height)) # solid red
await device.send_stream_data(session_id, frame)
await device.stop_stream() when done.
Cloud Client
LaMetricCloud authenticates with developer.lametric.com using a bearer
token.
import asyncio
from lametric import LaMetricCloud
async def main() -> None:
async with LaMetricCloud(token="developer-token") as cloud:
user = await cloud.current_user # CloudUser
devices = await cloud.devices # list[CloudDevice]
print(user.name)
print([d.name for d in devices])
asyncio.run(main())
Obtain a developer token at developer.lametric.com.
Errors
Both clients raise the same exception hierarchy:
| Exception | When raised |
|---|---|
LaMetricApiError |
Generic HTTP error from the API |
LaMetricAuthenticationError |
401 / 403 — invalid credentials or token |
LaMetricConnectionError |
Timeout, DNS failure, or transport error |
LaMetricUnsupportedError |
404 / 405 — endpoint not supported on this firmware |
Development
Setup
git clone <repository-url>
cd lametric-py
uv sync --dev
pre-commit install
Quality gates
uv run ruff check .
uv run ruff format --check .
uv run mypy .
uv run pytest --cov=src --cov-report=term-missing
Project layout
| Path | Purpose |
|---|---|
src/lametric/device.py |
Local device client |
src/lametric/cloud.py |
Cloud API client |
src/lametric/device_notifications.py |
Notification payload models |
src/lametric/device_states.py |
Device & stream response models |
src/lametric/device_apps.py |
Installed app models |
src/lametric/device_configs.py |
Request payload models (display, screensaver, stream) |
src/lametric/const.py |
Enums and protocol constants |
src/lametric/exceptions.py |
Custom exception hierarchy |
tests/ |
Unit tests |
tests/runtime_test.py |
Live integration test (requires a real device) |
Runtime integration test
Exercises every public API method against a real device and restores write-only state after each test:
python tests/runtime_test.py --host <ip> --api-key <key>