This release is a pre-release and may not be stable for production use.
Unofficial Matic SDK
An async Python SDK and CLI for connecting directly to a Matic robot you own. Pair once over Bluetooth, then use the robot's authenticated local-network service without a cloud relay.
What you can do
- Pair a Linux computer with Matic and store its per-client credential privately.
- Authenticate directly to the robot over encrypted local-network HTTP/2 and gRPC.
- Stream or record 43 robot collection surfaces covering pose, activity, device state, settings, schedules, history, media, and maps.
- Assemble captured RGB, integrated, coverage, and semantic map tiles into correctly oriented PNG mosaics.
- Export Matic's actual
32 x 32 x 24sparse colored surface representation as a standard PLY point cloud. - Recover retained WebP thumbnails and images embedded in captured collection responses.
- Stop, pause, tell the robot to stay put, or send it back to charge using typed Python APIs exercised against a real robot.
- Send direct joystick velocities or navigate to a mission pose through typed APIs exercised against a real robot.
- Run normal room coverage through a typed API exercised against a real robot.
- Reprioritize an active plan or clean a drawn dry-stain/wet-spill area through typed, wire-verified APIs.
- Build room partitions; rename, merge, or split rooms; and manage no-go, drive-only, stair, semantics, and sink-summon map data.
- Create, modify, toggle, or remove regular and sink-summon schedules.
- Send direct cleaning-mechanism setpoints through the wire-verified raw-motor codec; this path has not been exercised live and applies no device-specific range limits.
The command verification ledger shows exactly which controls were exercised live and which currently have offline wire proof only. All 65 documented protocol-25 commands have registered codecs.
Quick start
You need Python 3.11 or newer and a robot reachable from the same local network. Bluetooth enrollment currently requires Linux with BlueZ; Bluetooth is not needed after enrollment.
python -m pip install --pre "unofficial-matic-sdk[all]"
matic --help
With uv, use
uv tool install --python 3.13 --prerelease allow "unofficial-matic-sdk[all]"
for the CLI, or uv add --prerelease allow "unofficial-matic-sdk[all]" inside
a Python project.
No license is granted at this stage.
Pair once over Bluetooth
- In the Matic app, open Settings → Connectivity → Add another user and enable pairing mode.
- Keep the Linux Bluetooth adapter close to the robot.
- Start enrollment and complete the BlueZ passkey prompt using the six-digit code displayed by Matic.
uv run matic enroll --device living-room
living-room is only a local alias. The SDK creates its own client UUID and
saves the returned BotToken in an owner-only credential directory. It refuses
to overwrite an existing enrollment.
If you already have an owner-only serialized token, import it without exposing the secret in a command argument:
uv run matic credentials import-token \
--device living-room \
--source /secure/path/to/bot-token.pb
Never put a token in an environment variable, shell argument, .env file,
issue, log, fixture, or Git commit.
Verify the local connection
Set a local alias and the robot's LAN hostname or IP:
export MATIC_DEVICE=living-room
export MATIC_HOST=ROBOT_HOST
Fetch the certificate fingerprint without sending credentials, verify it independently, and then pin the verified value:
uv run matic tls fingerprint
export MATIC_CERT_SHA256=VERIFIED_64_CHARACTER_SHA256
uv run matic status
The status command authenticates, performs the Hermes handshake, and prints robot identity metadata including its serial number and network addresses. Treat that output as private even though it does not contain the BotToken. Routine SDK use is now LAN-only.
Read live robot data
List the 43 accepted collection targets:
uv run matic collections list
Stream event metadata without printing the raw latest-pose payload:
uv run matic collections stream latest_pose --count 20 --duration 30
Record raw responses from several targets into a new private directory:
uv run matic collections record captures/telemetry \
--target latest_pose \
--target kabuki_state \
--target motor_status \
--duration 30
The stream command does not print raw payloads, keys, or stable content hashes, but its target, timing, sequence, and payload-size metadata can still be sensitive. The record command saves raw data because it is needed for offline decoding; capture directories can contain household and device information.
Capture and decode maps
Record compressed map tiles:
uv run matic collections record captures/map \
--target map_compressed_rgb \
--duration 20
Assemble the tiles into correctly oriented PNGs:
uv run matic maps decode captures/map \
--output decoded-map \
--target map_compressed_rgb
The decoder also supports integrated, combined-coverage, semantic, and
semantic-override captures. For an ambiguous capture, pass its exact
--target. Use fresh capture and output paths; existing output files are never
overwritten.
Export the sparse voxel surface
The compressed RGB map includes a sparse depth bit field rather than only a flat image. Export it as a colored binary PLY point cloud:
uv run matic voxels export captures/map --output surface-map.ply
Use --all-depths to include depths hidden by the official app's normal
surface rendering, or --coordinate-mode app-native to preserve the app's
native coordinate convention.
Extract retained images
Capture the thumbnail collection and extract any validated WebP containers it contains:
uv run matic collections record captures/thumbnails \
--target coverage_session_thumbnails \
--duration 10
uv run matic media extract captures/thumbnails --output extracted-images
This recovers images already present in retained collection responses. It does not start a recording or provide a live optical-camera stream.
Use the Python API
Subscribe to a live collection
import asyncio
from matic_sdk import MaticClient, MaticConfig, TlsConfig
async def watch_pose() -> None:
config = MaticConfig(
host="ROBOT_HOST",
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
async with await robot.collections.subscribe("latest_pose") as events:
async for event in events:
print(event.operation.value, len(event.payload or b""))
asyncio.run(watch_pose())
Collection envelopes are typed and automatically acknowledged. Most application payloads are intentionally exposed as raw bytes until their exact schema has been proven.
Control the robot
The CLI can inspect command support with matic control list and
matic control status, but it deliberately has no generic command sender.
Writes use typed Python APIs, verified TLS, an explicitly selected protocol
version, and command-specific wire codecs.
Stop, pause, or stay put
import asyncio
from matic_sdk import MaticClient, MaticConfig, TlsConfig
async def stop_robot() -> None:
config = MaticConfig(
host="ROBOT_HOST",
command_protocol_version=25,
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
receipt = await robot.commands.stop()
assert receipt.transport_acknowledged
asyncio.run(stop_robot())
robot.commands.pause() and robot.commands.stay_put() use the same stationary
path. Stop, Pause, and StayPut have each been acknowledged by a real robot while
it remained docked.
Send a motion command
Motion commands are direct method calls. For example, this program requests Dock:
import asyncio
from matic_sdk import MaticClient, MaticConfig, TlsConfig
async def request_dock() -> None:
config = MaticConfig(
host="ROBOT_HOST",
command_protocol_version=25,
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
await robot.commands.dock()
asyncio.run(request_dock())
Dock has been exercised live through a ready-to-returning-to-charging state transition. Other autonomous motion codecs retain their individual evidence labels in the command ledger. Joystick delivery has been acknowledged live through a docked-to-ready state transition.
Drive with direct joystick commands
Each joystick() call sends one linear/angular velocity command:
import asyncio
from matic_sdk import MaticClient, MaticConfig, TlsConfig
async def drive_briefly() -> None:
config = MaticConfig(
host="ROBOT_HOST",
command_protocol_version=25,
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
await robot.commands.joystick(0.05, 0.0)
await asyncio.sleep(0.5)
await robot.commands.joystick(0.0, 0.0)
asyncio.run(drive_briefly())
The SDK does not repeat joystick commands in the background, expire stale
input, send zero velocity, or issue Stop automatically. Send every command your
control loop requires, including an explicit zero or robot.commands.stop()
when that is the behavior you want.
Send cleaning-mechanism setpoints
The raw-motor codec exposes vacuum RPM and sweeper, mopper, cleaning-head, and side-brush setpoints directly:
async def set_cleaning_motors(
robot,
*,
vacuum_rpm: float,
sweeper_duty: float,
) -> None:
await robot.commands.set_raw_motors(
vacuum_rpm=vacuum_rpm,
sweeper_duty=sweeper_duty,
)
Each call sends one motor_command. The wire format is verified, but this path
has not been exercised live and the SDK applies no device-specific range
limits.
Navigate or start coverage
These methods use mission-relative coordinates and map UUIDs. Choose the one
operation you intend to run; the calls are shown together only as an API
reference. x_meters, y_meters, and yaw_radians all use the same canonical
mission frame; a zero yaw points along positive X. The numeric mission, region,
and partition values below are placeholders, not coordinates for your robot.
The SDK does not yet provide a friendly decoder for latest_pose, so this is
an advanced API reference rather than a turnkey navigation example.
import asyncio
from uuid import UUID
from matic_sdk import (
DrawnCircle,
MaticClient,
MaticConfig,
MissionPosture,
StainMode,
TlsConfig,
)
async def run_one_motion_command() -> None:
config = MaticConfig(
host="ROBOT_HOST",
command_protocol_version=25,
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
destination = MissionPosture(
mission_id=42,
x_meters=1.0,
y_meters=2.0,
yaw_radians=0.0,
)
# Run exactly one intended operation. This example selects Navigate.
await robot.commands.navigate(destination)
# Alternatives:
# await robot.commands.navigate_and_wait(destination)
# await robot.commands.navigate_and_explore(destination)
# await robot.commands.normal_coverage(
# mission_id=42,
# partition_id=UUID("PARTITION_UUID"),
# region_ids=[UUID("REGION_UUID")],
# )
# await robot.commands.stain_mode(
# mission_id=42,
# stain_mode=StainMode.DRY_STAIN,
# circles=[
# DrawnCircle(
# x_meters=1.0,
# y_meters=2.0,
# radius_meters=0.25,
# )
# ],
# )
asyncio.run(run_one_motion_command())
Normal coverage supports vacuum, mop, or both plus Quick/Standard coverage. Stain mode reproduces the official DryStain and WetSpill goal plans. The typed model rejects empty or non-positive drawn circles so it cannot serialize malformed geometry.
Reprioritize active coverage
reprioritize_coverage() implements the official Prioritize and Skip
transformations. The live-tested decoder joins the robot's active session and
coverage plan by mission ID, preserving the goal IDs and cleaning specs the
command must send back:
from matic_sdk import ReprioritizeAction
async def prioritize_next_region(robot, selected_region_id) -> None:
snapshot = await robot.reprioritization_snapshot(timeout=5.0)
if snapshot is None:
print("The robot does not have an active coverage session.")
return
if selected_region_id not in snapshot.region_ids:
raise ValueError("selected_region_id is not in the active plan")
await robot.commands.reprioritize_coverage(
action=ReprioritizeAction.PRIORITIZE,
mission_id=snapshot.mission_id,
goals=snapshot.goals,
current_region_id=snapshot.current_region_id,
selected_region_id=selected_region_id,
current_session_id=snapshot.current_session_id,
)
Use snapshot.region_ids to show the currently scheduled rooms. Use
ReprioritizeAction.SKIP to remove the current region block; Skip does not need
selected_region_id. The read is safe while the robot is idle and returns
None. The official Add and Redo reprioritization helpers are not exposed yet.
Edit rooms and map zones
Map commands use the same mission coordinates and UUIDs returned by map collections. The convenience methods cover partition building, room rename/merge/split, no-go and drive-only zones, stairs, semantic overrides, and the sink-summon location:
from uuid import UUID
from matic_sdk import DrawnCircle, RoomLabel
async def edit_one_map_item(robot) -> None:
await robot.commands.rename_room(
mission_id=42,
partition_id=UUID("10213243-5465-7687-98a9-babcbddcedfe"),
region_id=UUID("00112233-4455-6677-8899-aabbccddeeff"),
label=RoomLabel.KITCHEN,
)
# Other typed operations include:
# await robot.commands.build_partition(mission_id=42, overwrite=False)
# await robot.commands.add_no_go_zones(
# mission_id=42,
# circles=[DrawnCircle(1.0, 2.0, 0.4)],
# )
These persistent commands have exact offline native-serializer wire proof but have not been exercised against a live robot through this SDK.
Create a cleaning schedule
Regular schedules support mapped rooms or custom drawn areas, Sunday through Saturday, vacuum/mop/both modes, Quick/Standard/legacy Deep vacuum settings, ordered goals, names, and enabled/disabled/suggested states:
from uuid import UUID, uuid4
from matic_sdk import (
CoverageCleaningMode,
ScheduleCoverageSetting,
ScheduleEvent,
ScheduleEventKey,
ScheduleTime,
StandardScheduleTarget,
Weekday,
)
async def add_weekday_schedule(robot) -> None:
key = ScheduleEventKey(mission_id=42, event_id=uuid4())
event = ScheduleEvent(
name="Weekday morning",
weekdays=(
Weekday.MONDAY,
Weekday.TUESDAY,
Weekday.WEDNESDAY,
Weekday.THURSDAY,
Weekday.FRIDAY,
),
time=ScheduleTime(
seconds_since_midnight=8 * 60 * 60,
timezone_id="America/Chicago",
utc_offset_seconds=-5 * 60 * 60,
),
target=StandardScheduleTarget(
(UUID("00112233-4455-6677-8899-aabbccddeeff"),)
),
partition_id=UUID("10213243-5465-7687-98a9-babcbddcedfe"),
cleaning_mode=CoverageCleaningMode.BOTH,
vacuum_setting=ScheduleCoverageSetting.STANDARD,
)
await robot.commands.add_or_modify_schedule(key=key, event=event)
Use toggle_schedule(key) or remove_schedule(key) for an existing event.
Sink-summon schedules use SinkSummonScheduleEvent and
add_or_modify_sink_summon_schedule(). Schedule writes have exact offline wire
proof but have not been exercised live.
Change a supported preference
Persistent settings are direct calls too. This example requests child lock to be enabled. Change the boolean only when you intend to change that setting:
import asyncio
from matic_sdk import MaticClient, MaticConfig, TlsConfig
from matic_sdk.models.control import SettingAction
async def enable_child_lock() -> None:
config = MaticConfig(
host="ROBOT_HOST",
command_protocol_version=25,
tls=TlsConfig.pinned("VERIFIED_CERTIFICATE_SHA256"),
)
async with await MaticClient.connect_from_store(
"living-room", config
) as robot:
await robot.commands.set_binary_setting(
SettingAction.CHILD_LOCK,
True,
)
asyncio.run(enable_child_lock())
Child lock, pet-waste avoidance, and voice are supported. Their live checks wrote each setting's already-observed value, so delivery was verified without claiming that a setting transition was tested.
See the full command ledger and caller-responsibility notes before using additional codecs. Unknown, partially reconstructed commands fail before network I/O. The documented top-level API exposes no arbitrary-payload escape hatch, and an ambiguous command outcome is never retried automatically.
All enabled typed commands are direct calls. The SDK does not require confirmation phrases or capability objects; callers are responsible for using commands safely and understanding their effects.
Safety and privacy
- BotTokens remain in owner-only files and are never printed by the SDK.
- TLS identity must be verified before authenticated commands are available.
- Map, media, schedule, pose, and raw collection captures can disclose details about a home. Keep them out of source control and public bug reports.
- Capture and decoder tools create new private outputs and refuse accidental overwrite.
- Tests and repository fixtures contain synthetic protocol data only.
Current limits
- Command writes currently require an explicitly selected protocol version 25.
- Bluetooth enrollment currently requires Linux and BlueZ.
- Most collection payloads have a safe raw-byte interface but not yet a friendly high-level model.
- Maps and voxels are decoded from captures; this is not a live map dashboard.
- Each joystick call sends exactly one command; the SDK does not provide a background resend loop, dead-man lease, automatic zero, or automatic Stop.
- Raw-motor setpoints are direct and wire-verified but have not been exercised live; the SDK applies no device-specific range limits.
- The portal-backed remote transport is experimental and not currently reliable; normal operation is local-network only.
- The SDK does not provide firmware images, root access, a filesystem, or an SSH shell. Update and support-permission command types do not change that.
Documentation
- Command verification ledger
- Control behavior and caller responsibility
- Protocol notes
- Research method
- SDK status
- Experimental remote transport
Development
uv sync --all-extras --group dev
uv run ruff check .
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run pytest -p pytest_asyncio.plugin
uv run python -m build
uv run python tools/scan_repository.py
Keep real household captures, application binaries, robot identifiers, tokens, maps, recordings, and packet captures out of Git.
Metadata
Release files for unofficial-matic-sdk 0.1.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| unofficial_matic_sdk-0.1.0a1.tar.gz | 190.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unofficial_matic_sdk-0.1.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 291.6 kB
Release files / unofficial_matic_sdk-0.1.0a1.tar.gz
| Download URL | unofficial_matic_sdk-0.1.0a1.tar.gz |
|---|---|
| Size | 190.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f4fae7907ec6c0622d44365334cebaa0f5875c78902192f4503d053bff670167
|
|
BLAKE2b-256 checksum How to use checksums |
e5d3e5c6399b85cd093e39ac5a6cb36a1f1ee874cec409992420a3bd6eb04b00
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / unofficial_matic_sdk-0.1.0a1-py3-none-any.whl
| Download URL | unofficial_matic_sdk-0.1.0a1-py3-none-any.whl |
|---|---|
| Size | 100.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
05841122c76d254f6634cf1b9e0c6d9f145f41f0261065f9043b4e658737d480
|
|
BLAKE2b-256 checksum How to use checksums |
9d7e130cea5680419b510c46501af814eed09084f76dcf40d615505b5647c31a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency log