Repository navigation
ADR 0071 Capability Matrix
Proposed (2026-10-30). Extends ADR 0066 (ProviderSpec gains an optional field) and builds on ADR 0012 (capabilities), ADR 0046 (settings as capabilities) and ADR 0067 (error lexicons).
MowDeck shows exactly what a connected mower can do: every feature is gated by MowerCapabilities, by the settings the mower declares (ADR 0046) and by the options of its provider (ADR 0066). Nobody can see that before a mower is linked, though. Someone who wants to know whether MowDeck drives a LUBA 1 by joystick, whether a Landroid reports its position or whether an Automower can be started from MowDeck has to read adapter code or ADRs, and the tables in ADR 0066 and the root README are written by hand, so they age with every change.
Three things make a static table hard:
-
Capabilities are a run-time value.
MowerConnection.capabilities()depends on the exact model (Mammotion reads blade height and speed ranges from PyMammotion's model table once the mower names its sub model), the firmware (some settings, obstacle detection modes), add-on modules (Worx cutting height and GPS modules), the account setup (Worx zones) and the links that are up (reverse driving only over the Bluetooth bridge). -
"No" has two meanings. A mower can lack a feature because the mower or its vendor's API does not offer it (no Husqvarna API call drives a mower by joystick), or because MowDeck has not built it yet (the Husqvarna API sets a cutting height, as a level that MowDeck does not map yet).
MowerCapabilitiesdefaults everything to "not supported" and cannot tell the two apart. -
The core must not learn vendor names (
test_vendor_isolation.py): whatever renders the table may only reach the adapters through their entry points.
Adapters declare capability profiles; a generator renders the matrix from them.
-
Contract (
mowdeck_contracts.v1.capability_profiles, Apache-2.0, neutral): aCapabilityProfiledescribes one model family of a provider:family(id),name,models(models or device name prefixes),capabilities(aMowerCapabilitiesvalue),reports(the state the family fills,ProfileReport: position, map geometry, job progress, blade height, satellites, signal, error codes, lift detection, rain sensor, battery cycles, firmware version, base station),varies,not_offered,tested_on_hardwareandnotes(LocalizedText, English and German like every provider text). The keys are neutral words:ProfileFeature(derived fromMowerCapabilitiesbyprofile_features(): transports, patterns, start options and plans, dock keeps job, blade height, spacing, speed, manual driving, reverse, blades while driving, drive speed setting, spot mowing, undock, custom paths, light, camera, RTK, firmware updates, base station updates, maintenance),ProfileReportand the setting keys ofSETTING_KEYS. The model refuses unknown keys, a key that both varies and is not offered, and a key marked not offered that the capabilities declare. -
Supported, varies, not offered, not yet. A key is supported when the profile's capabilities, reports or settings declare it; it varies when the adapter lists it in
varies(depends on model, firmware, module, link or setup; supported or not on the representative); it is not offered when the adapter lists it innot_offered(the mower or its vendor's API does not have it, so MowDeck cannot); every other key is not yet supported by MowDeck. This is the minimal, provider-neutral way to tell "not by the vendor" from "not yet in MowDeck": one set of neutral keys per family, declared next to the code that knows the vendor, validated by the contract.MowerCapabilitiesitself stays unchanged: a running mower needs no reason for a "no". -
Capabilities are computed, not written. Each adapter builds its profile with its own capability code for a representative mower of the family: Mammotion calls
mapping.capabilities_ofwith a device of the family's first device type, a sub model from PyMammotion's model table and the arguments a dual connection passes while every link is up and every option is on (firmware newer than every gate); it checks settings, camera and light for every device type of the family and marks what only some of them have asvaries. Worx computescapabilities_offor a fully equipped mower (every module, zones, edge cut, rain delay); Husqvarna for its single capability set. A change in the capability code changes the matrix. -
Families are the adapter's choice. Mammotion declares six (LUBA 1, LUBA 2 AWD, LUBA 2 AWD X, LUBA mini and LUBA 3, Yuka, Yuka mini; the device types are PyMammotion's, a test keeps them disjoint and checks every device name prefix classifies into its family), Husqvarna and Worx one each (their APIs answer every model the same way; equipment differences are
varieswith notes).tested_on_hardwareis per family (only the LUBA 2 AWD X) and may only be true when the provider's descriptor says so. -
Where the profiles come from.
ProviderSpec.capability_profiles: Callable[[], Sequence[CapabilityProfile]] | None(default None). It is a callable, so the spec stays importable without the vendor SDK. No new entry point group: profiles belong to a provider, and every adapter that reaches mowers through a vendor cloud already registers a spec. The simulator is no provider and has no profile. -
The generator.
tools/repo-checks/scripts/capability_matrix.py(uv run poe capability-matrix, checkcapability-matrix-checkin the CI docs job, likeapi-reference-check) loads the specs withmowdeck_control.providers.installed_specs()(themowdeck.cloud_providersentry points), reads each descriptor from the plugin on its fake cloud (no network), takes the error lexicon that serves the provider fromErrorLexiconRegistry(themowdeck.error_lexiconsentry points) and writesdocs/reference/capabilities.md: a legend, an overview (families, connections, lexicon size, tested on hardware, on by default), then per provider its summary, warnings (ProviderDescriptor.warnings), the untested note, the families with their models, and one table per group (commands, connection, mowing jobs, driving, hardware and maintenance, state reported, device settings) with a column per family and shared or per-family notes. It imports no vendor package by name. The output is deterministic, so the check compares the committed page with a fresh rendering. It fails when a family is declared twice or claims hardware tests the provider does not. -
Basic commands. Start, pause, resume, return to dock and the emergency stop are part of every
MowerConnection(ADR 0012), so the matrix shows them as supported everywhere; how a vendor carries out the emergency stop (Husqvarna and Worx: an immediate pause) is a note of the family.
Web app: not now. Showing the matrix in the web app (for example next to the provider choice of "link an account") would need the profiles on the bus and in Fleet's API: a field on CloudProvidersReply or a gateway.cloud.profiles subject, a Fleet schema with resolved texts, the OpenAPI export and generated types. That is more than a small change, so it is a follow-up; the profiles are a contract model already, so the data is ready to be served. Until then the documentation page is the one place.
Alternatives considered.
- A hand-written table (as in ADR 0066 and the README): ages with every change; rejected.
-
A reason field per capability in
MowerCapabilities: changes the run-time contract every client reads for a documentation need, and the vendor's limits are a property of the family, not of each connection; rejected. -
A new entry point group
mowdeck.capability_profiles: also covers connection kinds without a provider, but the vendor boundary test and every adapter would gain a third declaration for data that belongs to the provider; not now. -
Grouping device types automatically by equal capabilities: exhaustive, but gives columns without meaningful names and splits families over firmware details; families stay curated, with
variesfor what differs inside one.
-
docs/reference/capabilities.mdis generated and in the MkDocs navigation (and the wiki mirror); CI fails when it disagrees with the code. A new provider declares profiles (docs/dev/cloud-providers.md), andtest_capability_matrix.pyfails when an installed provider declares none. -
not_offeredis a claim about the vendor; the adapters base theirs on the ADR 0066 research and their own docstrings. A wrong claim is fixed in the adapter, like a wrong capability. - The first rendering already shows gaps: the Husqvarna connection reads plan references (
schedule,duration,work_area) but declares nostart_plans, so the web app offers no start for Husqvarna mowers (row "Start plans": not yet); Husqvarna headlight mode and cutting height level are not mapped yet. - Follow-ups: serve the profiles to the web app (see above); a profile for connection kinds without a cloud provider if one appears; let the conformance kit (ADR 0069) check that a profile's
capabilitiesequal what the connection declares for the representative mower.
Mirrored from docs/adr/0071-capability-matrix.md at commit 0031f1857a87c89bad7356661ca60578f18fedaf. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 0031f1857a87c89bad7356661ca60578f18fedaf. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- ADR 0065: Security center and new-login alerts
- ADR 0066: Cloud provider plugins and adapters for more vendors
- ADR 0067: Error code lexicon, provider independent
- ADR 0068: Public demo mode
- ADR 0069: Provider conformance kit and provider package template
- ADR 0070: Virtual zones for mowers without a vendor map
- ADR 0071: Capability matrix generated from the adapters
- ADR 0072: API explorer in the web app, for the public demo
-
Development
- Getting started
- Service template
- Shared libraries
- Cloud providers: adding a vendor
- Adding a provider: template and conformance kit
- Adding an error lexicon for a vendor
- Design: identity and authorization
- Design: fleet, telemetry, realtime
- Design: web app
- Web app
- Home Assistant integration
- Camera source bridge protocol
- Fuzzing and property-based tests
- GitHub wiki mirror
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Vendor library updates
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Matter server
- Source code offer (AGPL-3.0 section 13)
- Public demo
- Spikes
-
Guides
- Guides
- ESP32 bridge: purchase and setup
- Signing in with a passkey
- Signing in with GitHub
- Error codes in plain words
- Aerial photo under the map
- Signing in with Apple
- Confirming critical actions
- Your security page
- iPhone widgets and the mowing Live Activity
- Apple Watch
- The setup tour
- Apple Home and Google Home (Matter)
- Worx Landroid, Kress and LandXcape mowers
- Scripting MowDeck
- Capability matrix
- API reference
- Original plan (German)