Skip to content

Commit 9586b99

Browse files
smalnuitsacclaude
andcommitted
feat(catalog): rules that address other rules, and the platform context
None of this reads the car, so none of it carries a firmware annotation. The context conditions — media playing, the Wi-Fi network, a call in progress, how long the drive has lasted, a plain coin toss — come from Android, and a Wi-Fi name in particular answers "the car is home" the moment it is on the drive, which a GPS fix does not. ENABLE_RULE and DISABLE_RULE are the ones that change what the catalogue can express rather than what it can read: a rule that only applies during a trip is one rule switching a second on at departure and off on arrival, with no stored state and nothing new to evaluate. ValueKind.RULE exists so the chooser lists rules and a vanished target is reported instead of silently skipped. MEDIA_CONTROL sends the platform media key, the same one the wheel sends, so it reaches whichever app holds audio focus and needs no notification-listener access. IN_CALL reads the audio route, which means it cannot tell a ringing phone from a call in progress; the entry says so rather than implying more. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent cfbb125 commit 9586b99

14 files changed

Lines changed: 289 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ All notable changes to this project are documented here. Format follows
88

99
### Added
1010

11+
- Context entries that read Android rather than the car, so they carry no firmware
12+
annotation: `ConditionType.MEDIA_PLAYING`, `WIFI_SSID`, `IN_CALL`, `DRIVE_DURATION`,
13+
`RANDOM_CHANCE`. `IN_CALL` is "a call is routed through the car", not "the phone is
14+
ringing" — the difference is stated rather than papered over.
15+
- `ActionType.ENABLE_RULE` / `DISABLE_RULE` with `ValueKind.RULE`: a rule can now switch
16+
another on or off, which is what turns a catalogue of one-shot rules into chains.
17+
- `ActionType.MEDIA_CONTROL` (`MediaCommand`, the platform's own media key codes) and
18+
`SET_BLUETOOTH` / `SET_WIFI`.
19+
1120
- Climate readings that had a vendor getter and no catalogue entry: `ConditionType.ECON_MODE`,
1221
`PASSENGER_TEMP`, `FRONT_DEFROST`, `REAR_DEFROST`. The last two also become the `currentKey`
1322
of the defroster actions, which until now opened on a state nothing could read back.

‎docs/firmware-matrix.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
| FIRMWARE_GEN | — | — | — | — | — | — |
1515
| LOCATION_WITHIN | — | — | — | — | — | — |
1616
| PHYSICAL_BUTTON | — | — | — | — | — | — |
17+
| MEDIA_PLAYING | — | — | — | — | — | — |
18+
| WIFI_SSID | — | — | — | — | — | — |
19+
| IN_CALL | — | — | — | — | — | — |
20+
| DRIVE_DURATION | — | — | — | — | — | — |
21+
| RANDOM_CHANCE | — | — | — | — | — | — |
1722
| OUTSIDE_TEMP | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1823
| IGNITION_STATE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1924
| IN_PARK | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
@@ -71,6 +76,11 @@
7176
| NAVIGATE_TO | — | — | — | — | — | — |
7277
| ASK_CONFIRM | — | — | — | — | — | — |
7378
| SEND_SMS | — | — | — | — | — | — |
79+
| ENABLE_RULE | — | — | — | — | — | — |
80+
| DISABLE_RULE | — | — | — | — | — | — |
81+
| MEDIA_CONTROL | — | — | — | — | — | — |
82+
| SET_BLUETOOTH | — | — | — | — | — | — |
83+
| SET_WIFI | — | — | — | — | — | — |
7484
| WEBHOOK | — | — | — | — | — | — |
7585
| DELAY | — | — | — | — | — | — |
7686
| APPLY_PROFILE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

‎lib/src/main/java/com/evsuite/hardware/catalog/ActionType.kt‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,55 @@ enum class ActionType(
492492
R.string.act_send_sms, ActionGroup.SYSTEM,
493493
ValueSpec(ValueKind.SMS, hintRes = R.string.act_send_sms_hint), bridgeAction = null
494494
),
495+
/**
496+
* Turns another of the user's rules on, or off.
497+
*
498+
* The cheapest thing in the catalogue that changes what rules can express: a rule that
499+
* only applies during a trip is one rule enabling a second at departure and disabling it
500+
* on arrival, with no state to store and nothing new to evaluate. `Action.text` is the
501+
* target rule's id, and a rule that no longer exists is reported as unsupported — never
502+
* silently skipped, because a chain whose middle link vanished is exactly what a user
503+
* needs told.
504+
*
505+
* A rule cannot disable itself into a state it can never leave: the switch is the same one
506+
* the rule list shows, so the user can always put it back.
507+
*/
508+
ENABLE_RULE(
509+
R.string.act_enable_rule, ActionGroup.SYSTEM,
510+
ValueSpec(ValueKind.RULE), bridgeAction = null
511+
),
512+
DISABLE_RULE(
513+
R.string.act_disable_rule, ActionGroup.SYSTEM,
514+
ValueSpec(ValueKind.RULE), bridgeAction = null
515+
),
516+
/**
517+
* Play/pause, next or previous, sent as the media key the head unit's own steering
518+
* controls send.
519+
*
520+
* A media key rather than a media session: the key reaches whatever app currently holds
521+
* audio focus, exactly as the wheel button does, and needs no notification-listener
522+
* access to any of them. What it cannot do is address one app while another is playing —
523+
* which is not what a rule wants anyway.
524+
*/
525+
MEDIA_CONTROL(
526+
R.string.act_media_control, ActionGroup.SYSTEM,
527+
ValueSpec(ValueKind.ENUM, options = MediaCommand.OPTIONS), bridgeAction = null
528+
),
529+
/**
530+
* The head unit's own radios.
531+
*
532+
* Turning Bluetooth off ends the hands-free link, and with it every Bluetooth condition —
533+
* a rule that switches it off and then asks which phone is on board gets "unreadable",
534+
* which is the truth.
535+
*/
536+
SET_BLUETOOTH(
537+
R.string.act_set_bluetooth, ActionGroup.SYSTEM,
538+
ValueSpec.BOOL, bridgeAction = null
539+
),
540+
SET_WIFI(
541+
R.string.act_set_wifi, ActionGroup.SYSTEM,
542+
ValueSpec.BOOL, bridgeAction = null
543+
),
495544
/**
496545
* Calls an HTTP(S) endpoint. [Action.flag] carries the verb — false for GET, true for
497546
* POST — and [Action.payload] is the POST body, ignored for GET.
@@ -555,3 +604,22 @@ enum class ActionType(
555604
const val ASK_CONFIRM_DEFAULT_SECONDS = 10
556605
}
557606
}
607+
608+
/**
609+
* What [ActionType.MEDIA_CONTROL] sends.
610+
*
611+
* The values are the platform's own media key codes, so the runner has nothing to translate
612+
* and a wrong constant cannot become a different key by accident. Only the three a driver
613+
* asks a rule for: stop and rewind are the wheel's job, not a rule's.
614+
*/
615+
object MediaCommand {
616+
const val PLAY_PAUSE = 85
617+
const val NEXT = 87
618+
const val PREVIOUS = 88
619+
620+
val OPTIONS = listOf(
621+
EnumOption(PLAY_PAUSE, R.string.media_play_pause),
622+
EnumOption(NEXT, R.string.media_next),
623+
EnumOption(PREVIOUS, R.string.media_previous)
624+
)
625+
}

‎lib/src/main/java/com/evsuite/hardware/catalog/ConditionType.kt‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,58 @@ enum class ConditionType(
119119
ValueSpec(ValueKind.PHYSICAL_BUTTON), SnapshotKeys.KEY_PHYSICAL_BUTTON_EVENT,
120120
eventDriven = true
121121
),
122+
/** Something is coming out of the speakers, whichever app is playing it. */
123+
MEDIA_PLAYING(
124+
R.string.cond_media_playing, ConditionGroup.CONTEXT,
125+
ValueSpec.BOOL, SnapshotKeys.KEY_MEDIA_PLAYING
126+
),
127+
/**
128+
* The network the head unit is joined to, by name.
129+
*
130+
* The one context signal that says "the car is at a place it knows" without a satellite:
131+
* a home network answers the instant the car is on the drive, where a GPS fix does not.
132+
* Unreadable — radio off, or the platform declining to name it — leaves the condition
133+
* unavailable rather than saying "some other network".
134+
*/
135+
WIFI_SSID(
136+
R.string.cond_wifi_ssid, ConditionGroup.CONTEXT,
137+
ValueSpec(ValueKind.TEXT, hintRes = R.string.cond_wifi_ssid_hint),
138+
SnapshotKeys.KEY_WIFI_SSID
139+
),
140+
/**
141+
* A call is in progress through the car.
142+
*
143+
* "In a call", not "ringing": what this reads is the audio route the head unit has taken,
144+
* which a ringing phone has not taken yet. A rule that must not talk over a call is served
145+
* by it; one that wants to react to an incoming call is not, and nothing here pretends
146+
* otherwise.
147+
*/
148+
IN_CALL(
149+
R.string.cond_in_call, ConditionGroup.CONTEXT,
150+
ValueSpec.BOOL, SnapshotKeys.KEY_IN_CALL
151+
),
152+
/**
153+
* Minutes since the ignition came on.
154+
*
155+
* Counted from the transition the service already listens for, so it costs no poll. It is
156+
* unreadable until that transition has been seen — a rule cycle that starts on a car
157+
* already running does not know when the drive began, and must not answer 0.
158+
*/
159+
DRIVE_DURATION(
160+
R.string.cond_drive_duration, ConditionGroup.CONTEXT,
161+
number(0, 600, R.string.unit_minute), SnapshotKeys.KEY_DRIVE_MINUTES, comparable = true
162+
),
163+
/**
164+
* Matches with the given probability, in percent.
165+
*
166+
* Reads nothing, so it has no snapshot key: it is drawn at evaluation time. It exists for
167+
* the rules whose point is not to be the same every day — one greeting in five, a
168+
* different radio station now and then.
169+
*/
170+
RANDOM_CHANCE(
171+
R.string.cond_random_chance, ConditionGroup.CONTEXT,
172+
number(1, 100, R.string.unit_percent), snapshotKey = null
173+
),
122174

123175
// ── Environment ──────────────────────────────────────────────────────────
124176
@SupportedOn(SWI133, SWI132, SWI68, SWI69, SWI131, SWI165)

‎lib/src/main/java/com/evsuite/hardware/catalog/SnapshotKeys.kt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ object SnapshotKeys {
101101
const val KEY_CHARGE_WINDOW_STOP = "chargeWindowStop"
102102
const val KEY_BATTERY_PREHEAT = "batteryPreheat"
103103

104+
// ── Platform context (no vehicle involved) ───────────────────────────────
105+
// Read from Android, not from the car, so they carry no firmware annotation. Absent
106+
// still means unreadable: a head unit whose audio state cannot be read leaves the
107+
// media condition unavailable rather than claiming silence.
108+
const val KEY_MEDIA_PLAYING = "mediaPlaying"
109+
const val KEY_WIFI_SSID = "wifiSsid"
110+
const val KEY_IN_CALL = "inCall"
111+
/** Minutes since the ignition came on, absent before the first transition is seen. */
112+
const val KEY_DRIVE_MINUTES = "driveMinutes"
113+
104114
// ── AOSP door status (SWI133) ────────────────────────────────────────────
105115
/** True while either front door reads as open. Absent where the property is unreadable. */
106116
const val KEY_FRONT_DOOR_OPEN = "frontDoorOpen"

‎lib/src/main/java/com/evsuite/hardware/catalog/ValueSpec.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ enum class ValueKind {
3434
DATE,
3535
/** EVProfile driving profile, identified by id. */
3636
PROFILE,
37+
/**
38+
* Another of the user's own rules, identified by id.
39+
*
40+
* Not [APP] with a rule id in it: the chooser has to list rules, and a rule that names a
41+
* rule which no longer exists must be reported as such rather than silently doing nothing.
42+
*/
43+
RULE,
3744
/** Installed application, identified by package name. */
3845
APP,
3946
/** Phone-book entry; the rule stores the selected number, not a mutable contact id. */

‎lib/src/main/res/values-de/strings.xml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,19 @@
167167
<string name="charging_plugged_idle">Angeschlossen, lädt nicht</string>
168168
<string name="charging_done">Ladung abgeschlossen</string>
169169
<string name="charging_fault">Ladefehler</string>
170+
<string name="cond_media_playing">Medienwiedergabe läuft</string>
171+
<string name="cond_wifi_ssid">WLAN-Netz</string>
172+
<string name="cond_wifi_ssid_hint">z. B. Zuhause</string>
173+
<string name="cond_in_call">Anruf läuft</string>
174+
<string name="cond_drive_duration">Fahrtdauer</string>
175+
<string name="cond_random_chance">Zufallschance</string>
176+
<string name="unit_minute">Min</string>
177+
<string name="act_enable_rule">Regel aktivieren</string>
178+
<string name="act_disable_rule">Regel deaktivieren</string>
179+
<string name="act_media_control">Mediensteuerung</string>
180+
<string name="media_play_pause">Wiedergabe / Pause</string>
181+
<string name="media_next">Nächster Titel</string>
182+
<string name="media_previous">Vorheriger Titel</string>
183+
<string name="act_set_bluetooth">Bluetooth</string>
184+
<string name="act_set_wifi">WLAN</string>
170185
</resources>

‎lib/src/main/res/values-es/strings.xml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,19 @@
167167
<string name="charging_plugged_idle">Conectado, sin cargar</string>
168168
<string name="charging_done">Carga completa</string>
169169
<string name="charging_fault">Fallo de carga</string>
170+
<string name="cond_media_playing">Reproducción en curso</string>
171+
<string name="cond_wifi_ssid">Red Wi-Fi</string>
172+
<string name="cond_wifi_ssid_hint">p. ej. Casa</string>
173+
<string name="cond_in_call">Llamada en curso</string>
174+
<string name="cond_drive_duration">Duración del trayecto</string>
175+
<string name="cond_random_chance">Probabilidad aleatoria</string>
176+
<string name="unit_minute">min</string>
177+
<string name="act_enable_rule">Activar una regla</string>
178+
<string name="act_disable_rule">Desactivar una regla</string>
179+
<string name="act_media_control">Control multimedia</string>
180+
<string name="media_play_pause">Reproducir / pausar</string>
181+
<string name="media_next">Pista siguiente</string>
182+
<string name="media_previous">Pista anterior</string>
183+
<string name="act_set_bluetooth">Bluetooth</string>
184+
<string name="act_set_wifi">Wi-Fi</string>
170185
</resources>

‎lib/src/main/res/values-fr/strings.xml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,19 @@
189189
<string name="charging_plugged_idle">Branché, pas de charge</string>
190190
<string name="charging_done">Charge terminée</string>
191191
<string name="charging_fault">Défaut de charge</string>
192+
<string name="cond_media_playing">Lecture en cours</string>
193+
<string name="cond_wifi_ssid">Réseau Wi-Fi</string>
194+
<string name="cond_wifi_ssid_hint">ex. Maison</string>
195+
<string name="cond_in_call">Appel en cours</string>
196+
<string name="cond_drive_duration">Durée du trajet</string>
197+
<string name="cond_random_chance">Chance aléatoire</string>
198+
<string name="unit_minute">min</string>
199+
<string name="act_enable_rule">Activer une règle</string>
200+
<string name="act_disable_rule">Désactiver une règle</string>
201+
<string name="act_media_control">Contrôle média</string>
202+
<string name="media_play_pause">Lecture / pause</string>
203+
<string name="media_next">Piste suivante</string>
204+
<string name="media_previous">Piste précédente</string>
205+
<string name="act_set_bluetooth">Bluetooth</string>
206+
<string name="act_set_wifi">Wi-Fi</string>
192207
</resources>

‎lib/src/main/res/values-it/strings.xml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,19 @@
167167
<string name="charging_plugged_idle">Collegato, non in ricarica</string>
168168
<string name="charging_done">Ricarica completata</string>
169169
<string name="charging_fault">Guasto di ricarica</string>
170+
<string name="cond_media_playing">Riproduzione in corso</string>
171+
<string name="cond_wifi_ssid">Rete Wi-Fi</string>
172+
<string name="cond_wifi_ssid_hint">es. Casa</string>
173+
<string name="cond_in_call">Chiamata in corso</string>
174+
<string name="cond_drive_duration">Durata del viaggio</string>
175+
<string name="cond_random_chance">Probabilità casuale</string>
176+
<string name="unit_minute">min</string>
177+
<string name="act_enable_rule">Attiva una regola</string>
178+
<string name="act_disable_rule">Disattiva una regola</string>
179+
<string name="act_media_control">Controllo multimediale</string>
180+
<string name="media_play_pause">Riproduci / pausa</string>
181+
<string name="media_next">Brano successivo</string>
182+
<string name="media_previous">Brano precedente</string>
183+
<string name="act_set_bluetooth">Bluetooth</string>
184+
<string name="act_set_wifi">Wi-Fi</string>
170185
</resources>

0 commit comments

Comments
 (0)