diff --git a/sunnypilot/sunnylink/docs/README.md b/sunnypilot/sunnylink/docs/README.md index 99f7a08f9e..23151ad5b2 100644 --- a/sunnypilot/sunnylink/docs/README.md +++ b/sunnypilot/sunnylink/docs/README.md @@ -31,7 +31,7 @@ All metadata (titles, descriptions, options, min/max/step/unit) lives **inline o | `description` | No | Explanatory text below the title | | `options` | For selectors | Array of `{"value": 0, "label": "Off"}` objects | | `min`, `max`, `step` | For sliders | Numeric range constraints | -| `unit` | No | Unit label (e.g., `"seconds"`, `"mph"`) | +| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (resolved by IsMetric) | | `visibility` | No | Rules for show/hide (all must pass) | | `enablement` | No | Rules for enabled/disabled (all must pass) | | `sub_items` | No | Nested child items | @@ -119,6 +119,24 @@ Capability fields: `has_longitudinal_control`, `has_icbm`, `icbm_available`, `to } ``` +### Add a slider with metric/imperial unit + +For speed or distance values that change based on the user's `IsMetric` preference: + +```json +{ + "key": "MinSpeed", + "widget": "option", + "title": "Minimum Speed", + "min": 0, + "max": 100, + "step": 5, + "unit": {"metric": "km/h", "imperial": "mph"} +} +``` + +The frontend resolves the correct unit string based on the device's `IsMetric` param value. Static units (like `"seconds"`, `"m/s²"`) remain plain strings. + ### Add a dropdown ```json diff --git a/sunnypilot/sunnylink/docs/REFERENCE.md b/sunnypilot/sunnylink/docs/REFERENCE.md index 0c56f68677..d53ae3e5da 100644 --- a/sunnypilot/sunnylink/docs/REFERENCE.md +++ b/sunnypilot/sunnylink/docs/REFERENCE.md @@ -169,7 +169,7 @@ Root | `min` | For sliders | Minimum value (renders `option` widget as a slider) | | `max` | For sliders | Maximum value | | `step` | For sliders | Step increment | -| `unit` | No | Unit label displayed next to values (e.g., `"seconds"`, `"mph"`) | +| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (frontend resolves based on `IsMetric` param). See [Dynamic Units](#dynamic-units). | | `value_map` | No | Maps stored values to display labels | | `visibility` | No | Rules that control show/hide (all must pass) | | `enablement` | No | Rules that control enabled/disabled (all must pass) | @@ -761,6 +761,51 @@ These panels exist on the device but are intentionally excluded from the schema --- +## Dynamic Units + +Speed and distance units depend on the user's `IsMetric` preference. The `unit` field supports two forms: + +**Static** (constant regardless of IsMetric): +```json +"unit": "seconds" +"unit": "m/s²" +"unit": "meters" +``` + +**Dynamic** (resolved by the frontend based on `IsMetric` param): +```json +"unit": {"metric": "km/h", "imperial": "mph"} +``` + +The frontend reads `IsMetric` from the device's param values and displays the appropriate string. When `IsMetric` is `true` or `1`, the `metric` value is shown. Otherwise, the `imperial` value is shown. + +### Items with dynamic units + +| Param | Unit | +|-------|------| +| `BlinkerMinLateralControlSpeed` | `{"metric": "km/h", "imperial": "mph"}` | +| `SpeedLimitValueOffset` | `{"metric": "km/h", "imperial": "mph"}` | +| `LaneTurnValue` | `{"metric": "km/h", "imperial": "mph"}` | + +### When to use dynamic units + +Use `{"metric": ..., "imperial": ...}` whenever the param stores a value whose interpretation depends on `IsMetric`. This includes: +- Speed values (km/h vs mph) +- Distance values (km vs mi) if applicable + +Use a plain string when the unit is always the same regardless of user preference (seconds, m/s², meters, percentages). + +### Descriptions with speed references + +For **fixed thresholds** in descriptions (not user-configurable), include both units inline: +```json +"description": "Alerts when driving over 31 mph (50 km/h)." +``` + +For **user-configurable values**, use the `unit` field instead of embedding units in the description. + +--- + ## Remote Onroad Cycle When the sunnylink frontend pushes a param change for a setting marked with `needs_onroad_cycle: true`, the device triggers a confirmation flow before cycling the system. diff --git a/sunnypilot/sunnylink/settings_ui.json b/sunnypilot/sunnylink/settings_ui.json index 4bea302a69..1701b8be08 100644 --- a/sunnypilot/sunnylink/settings_ui.json +++ b/sunnypilot/sunnylink/settings_ui.json @@ -112,7 +112,7 @@ "min": 0, "max": 255, "step": 5, - "unit": "km/h" + "unit": {"metric": "km/h", "imperial": "mph"} }, { "key": "BlinkerLateralReengageDelay", @@ -642,7 +642,8 @@ "title": "Speed Limit Offset Value", "min": -30, "max": 30, - "step": 1 + "step": 1, + "unit": {"metric": "km/h", "imperial": "mph"} } ] } @@ -1656,10 +1657,11 @@ } ], "title": "Adjust Lane Turn Speed", - "description": "Set the maximum speed for lane turn desires. Default is 19 mph.", + "description": "Set the maximum speed for lane turn desires.", "min": 0, "max": 20, - "step": 1 + "step": 1, + "unit": {"metric": "km/h", "imperial": "mph"} }, { "key": "LagdToggle", diff --git a/sunnypilot/sunnylink/settings_ui.schema.json b/sunnypilot/sunnylink/settings_ui.schema.json index 68394c4ebb..bcf353f122 100644 --- a/sunnypilot/sunnylink/settings_ui.schema.json +++ b/sunnypilot/sunnylink/settings_ui.schema.json @@ -172,8 +172,29 @@ "description": "Step increment for numeric option widgets." }, "unit": { - "type": "string", - "description": "Unit label for numeric values (e.g. 'mph', 'seconds')." + "oneOf": [ + { + "type": "string", + "description": "Static unit label (e.g. 'seconds', 'm/s²')." + }, + { + "type": "object", + "description": "Dynamic unit that changes based on IsMetric param.", + "required": ["metric", "imperial"], + "additionalProperties": false, + "properties": { + "metric": { + "type": "string", + "description": "Unit label when IsMetric is true (e.g. 'km/h')." + }, + "imperial": { + "type": "string", + "description": "Unit label when IsMetric is false (e.g. 'mph')." + } + } + } + ], + "description": "Unit label for numeric values. Use a string for static units or an object with metric/imperial variants for units that depend on the IsMetric param." }, "value_map": { "type": "object",