Docs & rules

mikey0000/PyMammotionGitHub

This is a public, read-only report. Striff reads this repository's docs, turns each sentence that makes a claim about the code into a rule, and checks the rule against the code on the default branch. How this works

main963209bLast read 52 min ago

Is this yours? Install to manage it

Once installed, Striff checks every pull request.

Documents
Rules
120
100
3
14
3
16
6
12
8
20
18
20
18 docs
mikey0000/PyMammotionOpen repository

mikey0000/PyMammotion — documented rules

120 of 120 rules, printed November 2, 2026.

The sentence in your docs
Account-scoped — refresh_token_v2 rejected → TokenManager.reauth_required is set, ReLoginRequiredError propagates to the host, and MammotionClient.on_unrecoverable_auth_error fires so the user is prompted to re-authenticate.
2 rules from this sentence
TokenManager has reauth_required
checks “TokenManager.reauth_required is set”
Holds
MammotionClient has on_unrecoverable_auth_error
checks “MammotionClient.on_unrecoverable_auth_error fires”
Holds
Account lock held elsewhere is neither an auth failure nor terminal. Aliyun bind_reply 2152 (the Mammotion app holds the account's session lock) sets AliyunMQTTTransport.account_in_use (is_usable False), fires MammotionClient.on_account_in_use_changed(account_id, held) on each transition, and retries every ACCOUNT_IN_USE_RETRY_SEC (300 s) until a bind is accepted;
3 rules from this sentence
AliyunMQTTTransport has account_in_use
checks “sets AliyunMQTTTransport.account_in_use”
Holds
AliyunMQTTTransport has is_usable
checks “Account lock held elsewhere is neither an auth failure nor terminal. Aliyun bind_reply 2152 (the Mammotion app holds the account's session lock) sets AliyunMQTTTransport.account_in_use (is_usable False), fires MammotionClient.on_account_in_use_changed(account_id, held) on each transition”
Holds
MammotionClient has an on_account_in_use_changed method
checks “fires MammotionClient.on_account_in_use_changed(account_id, held)”
Holds
Note that AuthError subclasses TransportError, so its except AuthError: raise clause must stay ahead of the except TransportError catch — otherwise the terminal signal is swallowed into a log line.
AuthError extends TransportError
Holds
Recovery is one attempt, scoped to the failing transport. MammotionClient._send_with_auth_retry does one targeted refresh and one retry, then propagates.
MammotionClient has _send_with_auth_retry
Holds
Reactive refreshes are deduplicated by access token. TokenManager.refresh_invoke_token(stale_token=...) compares the token the failed request actually used against the live one and returns early if they differ — another caller already refreshed, so the caller just retries.
TokenManager has a refresh_invoke_token method
Holds
Credential renewal is clock-driven, not traffic-driven. TokenManager.start_refresh_scheduler() runs one task per account that sleeps until the earliest credential is within its lead window (5 min HTTP / 30 min MQTT JWT / 1 h Aliyun — the same thresholds the lazy getters use), renews just that one, and sleeps again. It does not poll. MammotionClient starts it from the two public entry points (login_and_initiate_cloud, restore_credentials) and stops it in _sign_out_session and stop().
MammotionClient depends on TokenManager
Holdsclient.py auth
Cloud error codes live in one table. pymammotion/aliyun/exceptions.py holds DEVICE_OFFLINE_CODES, DEVICE_UNBOUND_CODES, GATEWAY_TIMEOUT_CODES (plus the pairing-flow codes, currently unused).
3 rules from this sentence
DEVICE_OFFLINE_CODES is in pymammotion/aliyun
checks “pymammotion/aliyun/exceptions.py holds DEVICE_OFFLINE_CODES”
Holds
DEVICE_UNBOUND_CODES is in pymammotion/aliyun
checks “pymammotion/aliyun/exceptions.py holds”
Holds
GATEWAY_TIMEOUT_CODES is in pymammotion/aliyun
checks “pymammotion/aliyun/exceptions.py holds”
Holds
Read GATT status codes off __cause__, never by importing aioesphomeapi. bleak_esphome flattens BluetoothGATTAPIError into BleakError(str(exc)) from exc, so the status survives only on the cause chain. aioesphomeapi is an extras group, so a top-level import breaks import pymammotion for every installed user.
nothing in this codebase may import aioesphomeapi
Holds
pymammotion/utility/device_type.py — DeviceType.has_4g(), is_yuka(), is_rtk(), etc.
3 rules from this sentence
DeviceType has a has_4g method
checks “DeviceType.has_4g()”
Holds
DeviceType has an is_yuka method
Holds
DeviceType has an is_rtk method
Holds
"Is anything sendable right now?" → DeviceHandle.has_usable_transport / active_transport().
2 rules from this sentence
DeviceHandle has has_usable_transport
checks “DeviceHandle.has_usable_transport”
Holds
DeviceHandle has an active_transport method
Holds
"Is BLE in a usable state?" → BLETransport.is_usable.
BLETransport has is_usable
Holds
"What kind of state is the mower in for cadence?" → DeviceHandle.cadence_mode() (classification in device/modes.py) + the _MQTT_POLL_INTERVAL / _BLE_POLL_INTERVAL tables in device/mqtt_loop.py / device/ble_loop.py.
DeviceHandle has a cadence_mode method
Holds
LUBA_HM is defined as a DeviceType enum value alongside LUBA_ME, LUBA_VA, LUBA_LA, LUBA_MD, LUBA_MB.
DeviceType has LUBA_HM
Holds
Device known online? UI checks getStateMachine().getDeviceOnline() + getConnectState() != NONE. Pymammotion equivalent: DeviceHandle.has_usable_transport.
DeviceHandle has has_usable_transport
Holds
Chosen transport viable? APK requires for IoT: NetUtils.isNetworkConnected() && !isCarPowerOff() && netUsedType != 3 && deviceIotState(iotId). For BLE: espBleManager != null && system bluetooth on. Pymammotion equivalent: Transport.is_usable.
Transport has is_usable
Holds
Protocol mechanics live in messaging/transfers.py as free functions (ack_stream, indexed_fetch, frame extraction) rather than base-class methods.
2 rules from this sentence
ack_stream is in pymammotion/messaging
Holds
indexed_fetch is in pymammotion/messaging
Holds
The session paces itself: at most one frame in flight, and 150 ms between frames. All of its timers go through an injected DriveClock.
RemoteDriveSession depends on DriveClock
Holds
Question Is anything sendable right now, The one place that answers it DeviceHandle.has_usable_transport / active_transport()
2 rules from this sentence
DeviceHandle has has_usable_transport
checks “DeviceHandle.has_usable_transport”
Holds
DeviceHandle has an active_transport method
Holds
Question Is BLE usable, The one place that answers it BLETransport.is_usable (BLEDevice, RSSI, cooldown)
BLETransport has an is_usable method
Holds
Question Is a cloud transport usable, The one place that answers it CloudTransport.is_usable (terminal auth flags)
CloudTransport has an is_usable method
Holds
Question Does this command queue at all, The one place that answers it Priority.is_direct
Priority has is_direct
Holds
Question What cadence does this device need, The one place that answers it DeviceHandle.cadence_mode() + the _MQTT_POLL_INTERVAL / _BLE_POLL_INTERVAL tables
DeviceHandle has a cadence_mode method
Holds
Question Is this exception expected by the queue, The one place that answers it the demotion buckets in DeviceCommandQueue._process
DeviceCommandQueue has _process
Holds
Question Which handle owns this device's BLE, The one place that answers it DeviceRegistry.find_ble_owner
DeviceRegistry has find_ble_owner
Holds
Question Which cloud transport would a cloud-only send use, The one place that answers it DeviceHandle.usable_cloud_transport
DeviceHandle has usable_cloud_transport
Holds
Question Wait for the device to report a state, The one place that answers it DeviceHandle.wait_for(predicate, timeout=...) ( MammotionClient.wait_for by name) "fresh before acting" is ensure_fresh_state(wait=True) — never a host-side listener + future
2 rules from this sentence
DeviceHandle has a wait_for method
checks “DeviceHandle.wait_for”
Holds
MammotionClient has a wait_for method
checks “MammotionClient.wait_for”
Holds
APK component MaIoTRequestHelper.getJWTForAuth(), Python equivalent TokenManager.get_mammotion_mqtt_credentials()
TokenManager has a get_mammotion_mqtt_credentials method
Holds
APK component MaIoTRequestHelper.refreshToken(), Python equivalent TokenManager.refresh_http() / refresh_invoke_token()
2 rules from this sentence
TokenManager has a refresh_http method
checks “Python equivalent TokenManager.refresh_http()”
Holds
TokenManager has a refresh_invoke_token method
Holds
Keyed by hash: Yes — HashList.svg (stores SvgMessage objects rather than FrameList).
HashList depends on SvgMessage
Holds
PathType.DYNAMICS_LINE = 18
PathType has DYNAMICS_LINE
Holds
Storage: HashList.dynamics_line: list[CommDataCouple] Updated atomically via HashList.update_dynamics_line(points) once all frames are assembled.
2 rules from this sentence
HashList has dynamics_line
checks “HashList.dynamics_line: list[CommDataCouple]”
Holds
HashList has an update_dynamics_line method
checks “HashList.update_dynamics_line(points)”
Holds
Trigger: Call MammotionClient.get_dynamics_line(device_name) while device.report_data.dev.sys_status == WorkMode.MODE_WORKING.
2 rules from this sentence
MammotionClient has a get_dynamics_line method
checks “MammotionClient.get_dynamics_line(device_name)”
Holds
WorkMode has MODE_WORKING
checks “device.report_data.dev.sys_status == WorkMode.MODE_WORKING”
Holds
PathType.VISUAL_SAFETY_ZONE = 25
PathType has VISUAL_SAFETY_ZONE
Holds
Keyed by hash: Yes — HashList.visual_safety_zone.
HashList has visual_safety_zone
Holds
PathType.VISUAL_OBSTACLE_ZONE = 26
PathType has VISUAL_OBSTACLE_ZONE
Holds
Keyed by hash: Yes — HashList.visual_obstacle_zone.
HashList has visual_obstacle_zone
Holds
Type 0, Name AREA, Hash-keyed Yes, Storage HashList.area, Notes Zone boundary polygon
HashList has area
Holds
Type 1, Name OBSTACLE, Hash-keyed Yes, Storage HashList.obstacle, Notes Keep-out zone
HashList has obstacle
Holds
Type 2, Name PATH, Hash-keyed Yes, Storage HashList.path, Notes Luba 1 recorded path
HashList has path
Holds
Type 10, Name LINE, Hash-keyed Yes, Storage HashList.line, Notes Planned mow lines / breakpoint
HashList has line
Holds
Type 12, Name DUMP, Hash-keyed Yes, Storage HashList.dump, Notes Clippings collection zone
HashList has dump
Holds
Type 13, Name SVG, Hash-keyed Yes, Storage HashList.svg, Notes Pre-rendered map tile
HashList has svg
Holds
What is shared lives in transport/envelope.py (unwrap_envelope, which never raises — a corrupt base64 payload once escaped the old guard and reconnected the transport) and as MQTT_RECONNECT_* constants on CloudTransport, side by side so the 60 s vs 120 s asymmetry reads as intentional.
unwrap_envelope is in pymammotion/transport
Holds
Priority.USER and Priority.EMERGENCY are direct-send levels: the client dispatches them on the caller's task and DeviceCommandQueue.enqueue raises ValueError if handed one.
2 rules from this sentence
Priority has USER
Holds
Priority has EMERGENCY
Holds
the client dispatches them on the caller's task and DeviceCommandQueue.enqueue raises ValueError if handed one.
DeviceCommandQueue has enqueue
Holds
is_quota_exhausted (our own 600-per-12 h window) exists to pace polling and is skipped for user_initiated=True via CloudTransport.send_user(), which still calls record_send() so the window reflects real traffic.
CloudTransport has a send_user method
Holds
So RemoteDriveSession (device/remote_drive.py) is owned by the DeviceHandle, so stop() can halt the mower.
DeviceHandle depends on RemoteDriveSession
Holds
How they are stored: HashList.area: dict[int, FrameList] — mapping from hash ID to a list of coordinate frames that make up the area boundary polygon.
HashList has area
Holds
How they are stored: HashList.obstacle: dict[int, FrameList]
HashList has obstacle
Holds
How they are stored: HashList.path: dict[int, FrameList]
HashList has path
Holds
Code reference: MowPathSaga in pymammotion/messaging/mow_path_saga.py, HashList.current_mow_path.
HashList has current_mow_path
Holds
Trigger: AutoFetchWatchers watches work.path_hash with DeviceHandle.watch_until_handled.
AutoFetchWatchers depends on DeviceHandle
Holds
Cleared: a reported work.path_hash <= 1 means no route, and HashList.invalidate_mow_path drops the sub_cmd=3 line list and every cached cover path (APK deleteLineListDBAndHashDB).
HashList has invalidate_mow_path
Holds
Before a fetch, HashList.invalidate_stale_route does the same when the stored route does not hash to the live path_hash;
HashList has invalidate_stale_route
Holds
How they are stored: HashList.dump: dict[int, FrameList]
HashList has dump
Holds
How they are stored: HashList.svg: dict[int, SvgMessage]
HashList has svg
Holds
How they are stored: HashList.visual_safety_zone: dict[int, FrameList]
HashList has visual_safety_zone
Holds
How they are stored: HashList.visual_obstacle_zone: dict[int, FrameList]
HashList has visual_obstacle_zone
Holds
How they are stored: HashList.plan: dict[int, Plan]
HashList has plan
Holds
check it with DeviceHandle.supports_wifi_movement().
DeviceHandle has a supports_wifi_movement method
Holds
The Python model is RTKData in pymammotion/data/model/report_info.py.
RTKData is in pymammotion/data/model
Holds
Proto field status, Python name status, Type int32, Description RTK fix status — see RTKStatus enum
RTKData has a status field
Holds
Proto field pos_level, Python name pos_level, Type int32, Description Positioning solution level — see PositionMode enum
RTKData has a pos_level field
Holds
Proto field gps_stars, Python name gps_stars, Type int32, Description Total tracked satellite count (all constellations)
RTKData has a gps_stars field
Holds
Proto field age, Python name age, Type int32, Description Age of differential correction (seconds)
RTKData has an age field
Holds
Proto field lat_std, Python name lat_std, Type int32, Description Latitude accuracy standard deviation (scaled int)
RTKData has a lat_std field
Holds
Proto field lon_std, Python name lon_std, Type int32, Description Longitude accuracy standard deviation (scaled int)
RTKData has a lon_std field
Holds
Proto field l2_stars, Python name l2_stars, Type int32, Description L2 dual-frequency satellite count
RTKData has a l2_stars field
Holds
Proto field dis_status, Python name dis_status, Type int64, Description Packed signal quality — see get_dis_status()
RTKData has a dis_status field
Holds
Proto field top4_total_mean, Python name top4_total_mean, Type int64, Description Mean CNR of best 4 satellites
RTKData has a top4_total_mean field
Holds
Proto field co_view_stars, Python name co_view_stars, Type int32, Description Packed co-view satellite counts (L1 + L2)
RTKData has a co_view_stars field
Holds
Proto field reset, Python name reset, Type int32, Description RTK reset count since boot
RTKData has a reset field
Holds
Proto field lora_info, Python name lora_info, Type LoraInfo, Description LoRa base-station pairing state
RTKData has a lora_info field, and RTKData depends on LoraInfo
Holds
Proto field mqtt_rtk_info, Python name mqtt_rtk_info, Type MqttRtkInfo, Description Correction channel config
RTKData has a mqtt_rtk_info field, and RTKData depends on MqttRtkInfo
Holds
Proto field score_info, Python name score_info, Type RtkPositionScore, Description Positioning quality scores
RTKData has a score_info field, and RTKData depends on RtkPositionScore
Holds
Value 0, Enum NONE, Meaning No GNSS fix
RTKStatus has NONE
Holds
Value 1, Enum SINGLE, Meaning Single-point positioning (SPP)
RTKStatus has SINGLE
Holds
Value 4, Enum FIX, Meaning RTK fixed solution (best)
RTKStatus has FIX
Holds
Value 5, Enum FLOAT, Meaning RTK float solution
RTKStatus has FLOAT
Holds
Value other, Enum UNKNOWN, Meaning Unrecognised
RTKStatus has UNKNOWN
Holds
Plan.reserved_for_send() does exactly that, and send_schedule (every full plan write goes through it) applies it, so the normalisation happens once per transmission and cannot compound.
Plan has a reserved_for_send method
Holds
Plan.with_enabled only sets byte 2 locally.
Plan has with_enabled
Holds
Plan.from_wire(wire, device_name, product_key) decodes it (used by the reducer and PlanFetchSaga; clamped to 0–2, which the app does not do), and reserved_for_send(device_name, product_key) writes Plan.toward_mode into byte 4.
3 rules from this sentence
PlanFetchSaga depends on Plan
checks “Plan.from_wire(wire, device_name, product_key) decodes it (used by the reducer and PlanFetchSaga”
Holdsmessaging data
Plan has a from_wire method
checks “Plan.from_wire(wire, device_name, product_key) decodes it”
Holds
Plan has toward_mode
checks “Plan.toward_mode into byte 4”
Holds
Plan.week_for_send does the same.
Plan has week_for_send
Holds
Field zone_hashs, Type repeated fixed64, Notes Area hashes selected for this job Each hash matches a key in device.map.area
Plan has a zone_hashs field
Holds
Field knife_height, Type int32, Notes Cutting height in mm
Plan has a knife_height field
Holds
Field speed, Type float, Notes Driving speed
Plan has a speed field
Holds
Field route_angle, Type int32, Notes Mowing pattern angle (0–179 degrees)
Plan has a route_angle field
Holds
Field route_spacing, Type int32, Notes Spacing between mow lines (cm)
Plan has a route_spacing field
Holds
Field edge_mode, Type int32, Notes Border laps (0/1/2 → off / once / twice) the app's edgeMode = mowing_laps
Plan has an edge_mode field
Holds
Field ultrasonic_barrier, Type int32, Notes Ultrasonic obstacle avoidance toggle
Plan has an ultrasonic_barrier field
Holds
Field toward_included_angle, Type int32 (field 38), Notes The app's demond_angle , sent verbatim on every full write, ungated by model
Plan has a toward_included_angle field
Holds
pymammotion exposes this as MammotionCommand.single_schedule(plan_id) (navigation.py), the HA-facing helper MammotionMowerApi.start_task(...) (mower_api.py), and the HA-Luba coordinator method MammotionReportUpdateCoordinator.start_task(plan_id).
MammotionCommand has a single_schedule method
Holds
pymammotion implements this loop for the mower in PlanFetchSaga (pymammotion/messaging/plan_saga.py), enqueued via MammotionClient.start_plan_sync(name).
MammotionClient depends on PlanFetchSaga
Holdsclient.py messaging
pymammotion implements this loop for the mower in PlanFetchSaga (pymammotion/messaging/plan_saga.py), enqueued via MammotionClient.start_plan_sync(name). The Spino equivalent is SpinoPlanFetchSaga (pymammotion/messaging/spino_plan_saga.py), enqueued via start_spino_plan_sync(name).
MammotionClient depends on SpinoPlanFetchSaga
Holdsclient.py messaging
The Spino reply rides the ctrl envelope, not nav, so SpinoPlanFetchSaga overrides _collect_frames/_extract_plan_job_set to dispatch on LubaMsg.ctrl.plan_job_set instead of the default nav path.
SpinoPlanFetchSaga has _collect_frames
Holds
120 rules from 10 docs

More open-source reports

Every report →

Other public repositories whose docs Striff read and checked against their code.