Repository navigation
ADR 0046 Device Settings As Capabilities
Accepted (2026-10-23); amends ADR 0043 (the mower's own turn setting may now be written).
A mower keeps settings of its own that change how it behaves, not what it does right now: whether its rain sensor stops it, the battery levels at which it returns and resumes, its lights, its voice, the hours it must not work. The vendor app and the community Home Assistant integration for Mammotion expose them; MowDeck exposed none, so an owner still needed the vendor app, and MowDeck's own Home Assistant integration (mowdeck-hacs) had nothing to offer for them.
Which settings exist differs per manufacturer, model and firmware: the LUBA 2 has rain detection but not the rain delay of the X5 series; the battery levels need firmware 2.1.1.5; wildlife safety depends on model and firmware; only some models have the night light of the camera's fill light. A typed command per setting (like set_light or set_drive_speed) would add a contract change, a permission entry, a simulator path and a web control for every key, and still not tell a client which of them the connected mower has.
The mower also reports when its rain sensor stops it from starting; clients want that as a plain "it is raining" signal (a binary sensor in Home Assistant), not as one of many self-check codes.
Settings are declared, not hard-coded. MowerCapabilities.settings lists what the connection can read and change, one SettingSpec per setting: a manufacturer-neutral key, a type (bool, int, enum, time_window), a group for clients (weather, battery, lights, driving, safety, voice, schedule), and for int its min, max, step and unit, for enum its options. The known keys and what they mean are SETTING_KEYS in the contracts; an adapter may only declare those, so clients can label every setting without knowing the vendor. A time_window value is {start, end} (HH:MM, end before start crosses midnight) or null for "none".
Current values in the state. MowerState.settings maps each declared key to its value as the mower reported it. A key the mower has not answered yet is missing (never a guessed default), so a client shows "unknown" instead of a wrong value.
One generic command. SetSetting {kind: "set_setting", key, value}. The controller (rule 14) refuses an undeclared key (after reloading the capabilities once: the declaration may depend on the model or firmware the mower reported after connecting) and a value that does not fit the declaration (SettingSpec.value_problem: type, range, step, options) before anything reaches the adapter. The adapter implements the optional SettingsCapable protocol (set_setting(key, value) -> Outcome): executed when the mower's answer shows the new value, accepted when it was sent but not confirmed.
Safety: a setting never moves the mower. An adapter declares no key whose change starts the wheels or the blades. A setting therefore keeps an open drive session and pending confirmations and is allowed in every activity, also during an error (like the light); it is refused, like every command, while a firmware update, a spot run or a drive calibration runs.
Permission and plan. set_setting needs can_manage_mowers on the mower (it changes the device itself, like firmware_update) and the plan feature control. The Fleet command record is its audit. It may be sent to a site or group (each mower checks its own declaration).
Rain sensor. MowerState.rain_detected (bool, null when the connection does not report the sensor) says that the mower's own rain sensor reports rain. The existing self_check keeps the detail.
Considered and rejected. A typed command per setting (see Context). A separate settings endpoint with its own read model: the state already flows to every client over the realtime socket and the telemetry API, and the capabilities endpoint already serves the declaration. Free-form vendor keys passed through: clients could not label them, and the controller could not check them.
Generic: the contracts (SettingSpec, SettingType, SettingGroup, TimeWindow, SETTING_KEYS, SetSetting, MowerCapabilities.settings, MowerState.settings, MowerState.rain_detected), the controller's rule 14, the SettingsCapable protocol, the Fleet permission and plan rule, and the simulator (SimulationConfig.settings, one setting of each type; raining and set_raining for the sensor, which with rain_detection on blocks the self-check with rain and refuses a start).
Mammotion (mowdeck_control_mammotion.settings, the commands and replies Mammotion-HA uses on PyMammotion 0.10.9): only the LUBA 2 family declares settings (DeviceType.is_luba_pro, not a Yuka), because the reply routing of the read/write parameters is that family's and the owner's LUBA 2 AWD X is what MowDeck is tested against.
| Key | Vendor command | Gate |
|---|---|---|
rain_detection |
read_write_device parameter 3 |
LUBA 2 family |
turn_style (in_place, multi_point) |
turning_mode, parameter 6 |
LUBA 2 family |
traversal_mode (direct, follow_perimeter) |
traverse_mode, parameter 7 |
LUBA 2 family |
side_led |
read_and_set_sidelight |
LUBA 2 family |
night_light |
set_car_light, read get_car_light(1123)
|
is_support_fill_light |
wildlife_safety (off, stop_mowing, low_speed_mowing) |
set_animal_protection_mode |
supports_wildlife_safety (model, firmware) |
recharge_level (15 to 30 %), smart_recharge_level
|
set_recharge_level (-1 is smart) |
supports_charge_limit (firmware 2.1.1.5) |
resume_level (40 to 100 %), smart_resume_level
|
set_resume_level |
supports_charge_limit |
voice_enabled, voice_volume, voice_gender (man, woman) |
set_car_volume, set_car_volume_sex
|
LUBA 2 family |
non_work_hours |
job_do_not_disturb, job_do_not_disturb_del
|
LUBA 2 family |
Writes go out once with user priority, then the setting is read back; the declared settings are read at connect with the device information (background priority, again while unanswered). rain_detected is report_data.dev.self_check_status == 20 ("Mowing is disabled on rainy days"), which the mower reports only while rain_detection is on.
Left out on purpose: the rain delay and the rain protection modes (X5 series only), the charge limit and smart charging (a read-modify-write of the off-peak charging window, which the vendor resets to 00:00 to 00:00 when it is sent unread; and not available on the owner's firmware), auto_change_direction (a start option that the vendor excludes for the LUBA 2), blade speed, and every Yuka and Spino setting.
- A new setting is one entry in
SETTING_KEYSand one entry in an adapter; no new command, permission or API. - mowdeck-hacs can build its switches, numbers and selects from
MowerCapabilities.settingsand read their states fromMowerState.settings, and add a rain binary sensor fromrain_detected. - The web app shows the new command kind in the command history; a settings page in the web app is a follow-up.
- The vendor's turn setting, read only since ADR 0043, can now be changed through
turn_style; spot runs keep using it as their default. - None of the Mammotion commands above was sent to the real mower by this change; they are the ones Mammotion-HA sends, verified against PyMammotion 0.10.9 with the fake port. The first hardware test should check the read back of each key, especially
side_led(Mammotion-HA reads "on" fromenable == 0).
Mirrored from docs/adr/0046-device-settings-as-capabilities.md at commit 51fb8445b5852a7dc5482895760ac5f4fe128f07. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 51fb8445b5852a7dc5482895760ac5f4fe128f07. 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
- Development
- Operations
- Spikes
- Guides
- Original plan (German)