EVHardware: translate the French comments, log texts and test names

The vendored library's French and half-translated comments, its French
log messages and the French test names and assertion messages are now
English. Comments, log texts and test names only; no code changed. The
French resources, place names in test data and two quoted phrases stay;
"Katman" (Turkish for layer) stays as the layers' name and is explained.
SPECS: the Grafana host is confirmed; both items ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-25 21:53:15 +01:00
co-authored by Claude Opus 5.5
parent aca9e090d8
commit 0ff56285bc
7 changed files with 290 additions and 283 deletions
File diff suppressed because it is too large Load Diff
@@ -30,19 +30,19 @@ class FirmwareSupportTest {
ConditionType.FIRMWARE_GEN, ConditionType.FIRMWARE_GEN,
ConditionType.LOCATION_WITHIN, ConditionType.LOCATION_WITHIN,
ConditionType.PHYSICAL_BUTTON, ConditionType.PHYSICAL_BUTTON,
// Lus dans Android, pas dans la voiture : rien à router par génération. // Read from Android, not from the car: nothing to route per generation.
ConditionType.MEDIA_PLAYING, ConditionType.MEDIA_PLAYING,
ConditionType.WIFI_SSID, ConditionType.WIFI_SSID,
ConditionType.IN_CALL, ConditionType.IN_CALL,
ConditionType.DRIVE_DURATION, ConditionType.DRIVE_DURATION,
ConditionType.RANDOM_CHANCE, ConditionType.RANDOM_CHANCE,
// Services des applications du poste (adaptateur de navigation, météo) : ce ne sont // Services of the head unit's apps (navigation adapter, weather): these are not
// pas des services du SDK véhicule, rien ne les route par génération. Ils répondent // vehicle-SDK services, nothing routes them per generation. They answer or they do
// ou non, comme le SMS. // not, like SMS.
ConditionType.ODOMETER, ConditionType.ODOMETER,
ConditionType.WEATHER_NOW, ConditionType.WEATHER_NOW,
// La connexion du poste, comptée par Android : elle dépend d'une permission, pas // The head unit's connection, as counted by Android: it depends on a permission, not
// d'une génération de firmware. // on a firmware generation.
ConditionType.DATA_USED_TODAY, ConditionType.DATA_USED_TODAY,
ConditionType.DATA_USED_MONTH ConditionType.DATA_USED_MONTH
) )
@@ -54,10 +54,10 @@ class FirmwareSupportTest {
ActionType.WEBHOOK, ActionType.WEBHOOK,
ActionType.ASK_CONFIRM, ActionType.ASK_CONFIRM,
ActionType.DELAY, ActionType.DELAY,
// Le SMS part par le profil Bluetooth MAP, pas par un service SAIC : rien à router // SMS goes out over the Bluetooth MAP profile, not through a SAIC service: nothing to
// par génération de firmware. Sa disponibilité se lit au bind, pas dans la matrice. // route per firmware generation. Whether it is available shows at bind, not in the matrix.
ActionType.SEND_SMS, ActionType.SEND_SMS,
// Règles, touche média et radios du poste : rien de tout cela ne passe par la voiture. // Rules, the media key and the head unit's radios: none of this goes through the car.
ActionType.ENABLE_RULE, ActionType.ENABLE_RULE,
ActionType.DISABLE_RULE, ActionType.DISABLE_RULE,
ActionType.MEDIA_CONTROL, ActionType.MEDIA_CONTROL,
@@ -7,11 +7,11 @@ import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
/** /**
* Ce que la preuve relevée sur une voiture change — et ce qu'elle ne change pas. * What evidence recorded on a car changes — and what it does not.
* *
* L'enjeu : `writeProven` parle du projet, `effectProven` parle de la voiture. Confondre les * What is at stake: `writeProven` speaks about the project, `effectProven` about the car.
* deux rendrait soit les actions vitres définitivement inaccessibles, soit accessibles sur des * Mixing the two up would make the window actions either unreachable for good, or reachable on
* voitures où rien n'a été constaté. * cars where nothing was observed.
*/ */
class GlassEvidenceTest { class GlassEvidenceTest {
@@ -21,7 +21,7 @@ class GlassEvidenceTest {
fun tearDown() = GlassEvidence.setForTest(null) fun tearDown() = GlassEvidence.setForTest(null)
@Test @Test
fun `sans preuve, effectProven suit writeProven`() { fun `without evidence, effectProven follows writeProven`() {
GlassEvidence.setForTest(null) GlassEvidence.setForTest(null)
ActionType.entries.forEach { ActionType.entries.forEach {
assertTrue("${it.name}", it.writeProven == it.effectProven) assertTrue("${it.name}", it.writeProven == it.effectProven)
@@ -29,29 +29,29 @@ class GlassEvidenceTest {
} }
@Test @Test
fun `la preuve debloque les actions vitres, et elles seules`() { fun `evidence unlocks the window actions, and only them`() {
GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, generation, 0L)) GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, generation, 0L))
GlassEvidence.ACTIONS.forEach { GlassEvidence.ACTIONS.forEach {
assertFalse("${it.name} reste non prouvée dans le catalogue", it.writeProven) assertFalse("${it.name} stays unproven in the catalogue", it.writeProven)
assertTrue("${it.name} doit être débloquée par la preuve", it.effectProven) assertTrue("${it.name} must be unlocked by the evidence", it.effectProven)
} }
// Une preuve sur les vitres ne dit rien d'une autre action : le catalogue reste seul // Evidence about the windows says nothing about another action: everywhere else the
// juge partout ailleurs. // catalogue alone decides.
ActionType.entries.filterNot { it in GlassEvidence.ACTIONS }.forEach { ActionType.entries.filterNot { it in GlassEvidence.ACTIONS }.forEach {
assertTrue("${it.name}", it.writeProven == it.effectProven) assertTrue("${it.name}", it.writeProven == it.effectProven)
} }
} }
@Test @Test
fun `une preuve d'un autre firmware ne vaut rien`() { fun `evidence from another firmware counts for nothing`() {
// Les commandes sont propres au SDK de la génération : ce qui ouvre sur l'une n'a // The commands belong to each generation's SDK: what opens on one has no reason to
// aucune raison d'ouvrir sur l'autre, et une mise à jour ne doit pas laisser derrière // open on another, and an update must not leave behind a permission that no longer
// elle une autorisation qui n'a plus de fondement. // has any grounds.
GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, "SOME_OTHER_GEN", 0L)) GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, "SOME_OTHER_GEN", 0L))
GlassEvidence.ACTIONS.forEach { GlassEvidence.ACTIONS.forEach {
assertFalse("${it.name} ne doit pas hériter d'une preuve étrangère", it.effectProven) assertFalse("${it.name} must not inherit evidence from elsewhere", it.effectProven)
} }
} }
} }
@@ -8,15 +8,15 @@ import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
/** /**
* Ce que la sonde doit établir sans voiture : l'enchaînement des deux passes et les refus. * What the probe must establish without a car: the sequence of its two passes, and its refusals.
* *
* La vitre simulée reproduit le comportement qui a rendu la sonde nécessaire : le service * The simulated window reproduces the behaviour that made the probe necessary: the service
* accepte n'importe quelle commande et n'en applique que deux. Une passe unique ne peut donc * accepts any command and applies only two of them. A single pass therefore cannot see both
* pas voir les deux sens — c'est exactement ce que ces tests vérifient. * directions — which is exactly what these tests check.
*/ */
class GlassProbeTest { class GlassProbeTest {
/** Vitre où [openCommand] ouvre en grand et [closeCommand] referme ; le reste est inerte. */ /** A window where [openCommand] opens fully and [closeCommand] closes; the rest do nothing. */
private class FakeGlass( private class FakeGlass(
private val openCommand: Int, private val openCommand: Int,
private val closeCommand: Int, private val closeCommand: Int,
@@ -48,9 +48,9 @@ class GlassProbeTest {
) )
@Test @Test
fun `les deux sens sont trouves depuis une vitre fermee`() { fun `both directions are found from a closed window`() {
// Depuis 0 %, la commande qui ferme ne bouge rien : sans seconde passe elle passerait // From 0 %, the closing command moves nothing: without a second pass it would pass
// pour inerte, et l'action « fermer » resterait à jamais non prouvée. // for inert, and the "close" action would stay unproven forever.
val glass = FakeGlass(openCommand = 5, closeCommand = 2) val glass = FakeGlass(openCommand = 5, closeCommand = 2)
val result = probe(glass) val result = probe(glass)
@@ -58,27 +58,27 @@ class GlassProbeTest {
assertEquals(2, result.closeCommand) assertEquals(2, result.closeCommand)
assertTrue(result.proven) assertTrue(result.proven)
assertNull(result.refusal) assertNull(result.refusal)
// Repartie de 0 %, la vitre doit y revenir : une sonde qui laisse la vitre ouverte // Having started at 0 %, the window must end there: a probe that leaves the window
// est un défaut, pas un résultat. // open is a defect, not a result.
assertTrue(result.restored) assertTrue(result.restored)
assertEquals(0, glass.percent) assertEquals(0, glass.percent)
} }
@Test @Test
fun `une commande deja classee n'est pas renvoyee`() { fun `a command already classified is not sent again`() {
val glass = FakeGlass(openCommand = 0, closeCommand = 1) val glass = FakeGlass(openCommand = 0, closeCommand = 1)
val result = probe(glass) val result = probe(glass)
assertEquals(0, result.openCommand) assertEquals(0, result.openCommand)
assertEquals(1, result.closeCommand) assertEquals(1, result.closeCommand)
// 0 ouvre, 1 referme, les deux sens sont connus : la sonde s'arrête là, plus une // 0 opens, 1 closes, both directions are known: the probe stops there, plus one
// remise en place. Rien au-delà de la commande 1 ne doit avoir été envoyé. // restore. Nothing beyond command 1 may have been sent.
assertEquals(listOf(0, 1), glass.sent.take(2)) assertEquals(listOf(0, 1), glass.sent.take(2))
assertTrue(glass.sent.none { it > 1 }) assertTrue(glass.sent.none { it > 1 })
} }
@Test @Test
fun `une vitre inerte ne prouve rien`() { fun `an inert window proves nothing`() {
val glass = FakeGlass(openCommand = 99, closeCommand = 98) val glass = FakeGlass(openCommand = 99, closeCommand = 98)
val result = probe(glass) val result = probe(glass)
@@ -86,28 +86,28 @@ class GlassProbeTest {
assertNull(result.closeCommand) assertNull(result.closeCommand)
assertEquals(GlassProbe.Refusal.NOTHING_MOVED, result.refusal) assertEquals(GlassProbe.Refusal.NOTHING_MOVED, result.refusal)
assertFalse(result.proven) assertFalse(result.proven)
// Les huit commandes, deux fois : la seconde passe ne peut être sautée sur la foi de // All eight commands, twice: the second pass cannot be skipped on the strength of
// la première, qui ne voit pas les fermetures. // the first, which does not see closing.
assertEquals(16, glass.sent.size) assertEquals(16, glass.sent.size)
} }
@Test @Test
fun `un seul sens trouve n'est pas une preuve`() { fun `one direction found is not proof`() {
val glass = FakeGlass(openCommand = 3, closeCommand = 99) val glass = FakeGlass(openCommand = 3, closeCommand = 99)
val result = probe(glass) val result = probe(glass)
assertEquals(3, result.openCommand) assertEquals(3, result.openCommand)
assertNull(result.closeCommand) assertNull(result.closeCommand)
assertEquals(GlassProbe.Refusal.ONE_DIRECTION_ONLY, result.refusal) assertEquals(GlassProbe.Refusal.ONE_DIRECTION_ONLY, result.refusal)
// Rien ne peut refermer la vitre : il faut le dire, pas laisser croire au contraire. // Nothing can close the window: that must be said, not left to look otherwise.
assertFalse(result.restored) assertFalse(result.restored)
} }
@Test @Test
fun `la voiture qui repart interrompt la sonde`() { fun `the car moving off interrupts the probe`() {
val glass = FakeGlass(openCommand = 5, closeCommand = 2) val glass = FakeGlass(openCommand = 5, closeCommand = 2)
var checks = 0 var checks = 0
// Arrêtée pour la lecture initiale et la première commande, puis en mouvement. // Stopped for the first reading and the first command, then moving.
val result = probe(glass, stopped = { checks++ < 2 }) val result = probe(glass, stopped = { checks++ < 2 })
assertEquals(GlassProbe.Refusal.NOT_STOPPED, result.refusal) assertEquals(GlassProbe.Refusal.NOT_STOPPED, result.refusal)
@@ -115,7 +115,7 @@ class GlassProbeTest {
} }
@Test @Test
fun `contact coupe, aucune commande n'est envoyee`() { fun `ignition off, no command is sent`() {
val glass = FakeGlass(openCommand = 5, closeCommand = 2) val glass = FakeGlass(openCommand = 5, closeCommand = 2)
val result = probe(glass, ignitionRun = { false }) val result = probe(glass, ignitionRun = { false })
@@ -124,7 +124,7 @@ class GlassProbeTest {
} }
@Test @Test
fun `une position illisible arrete tout`() { fun `an unreadable position stops everything`() {
val glass = FakeGlass(openCommand = 5, closeCommand = 2).apply { readable = false } val glass = FakeGlass(openCommand = 5, closeCommand = 2).apply { readable = false }
val result = probe(glass) val result = probe(glass)
@@ -109,7 +109,7 @@ class PhysicalButtonEventDecoderTest {
} }
@Test fun `three presses give one double and then a single`() { @Test fun `three presses give one double and then a single`() {
// Sinon un troisième appui rendrait un second DOUBLE, et la règle partirait deux fois. // Otherwise a third press would make a second DOUBLE, and the rule would fire twice.
val decoder = PhysicalButtonEventDecoder() val decoder = PhysicalButtonEventDecoder()
val presses = listOf(0L, 100L, 200L).map { val presses = listOf(0L, 100L, 200L).map {
decoder.accept(17, true, false, atMillis = it) decoder.accept(17, true, false, atMillis = it)
@@ -13,9 +13,8 @@ import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
/** /**
* Garde-fous sur le catalogue. Les erreurs visées ne cassent pas la compilation : elles * Guard rails on the catalogue. The mistakes they catch do not break the build: they produce a
* produisent une entrée d'interface qui ne fait rien, ou pire, une écriture véhicule qui * UI entry that does nothing or, worse, a vehicle write that gets round the speed lock.
* contourne le verrou de vitesse.
*/ */
class CatalogConsistencyTest { class CatalogConsistencyTest {
@@ -30,42 +29,42 @@ class CatalogConsistencyTest {
} }
@Test @Test
fun `la destination et le point utilisent des controles distincts`() { fun `the destination and the point use separate controls`() {
// Un point a un rayon, une destination n'en a pas : deux ValueKind, sinon l'éditeur // A point has a radius, a destination does not: two ValueKinds, or the editor would
// demanderait un rayon pour une adresse. // ask for a radius for an address.
assertEquals(ValueKind.DESTINATION, ActionType.NAVIGATE_TO.spec.kind) assertEquals(ValueKind.DESTINATION, ActionType.NAVIGATE_TO.spec.kind)
assertEquals(ValueKind.LOCATION, ConditionType.LOCATION_WITHIN.spec.kind) assertEquals(ValueKind.LOCATION, ConditionType.LOCATION_WITHIN.spec.kind)
assertEquals(0, ActionType.NAVIGATE_TO.spec.max) assertEquals(0, ActionType.NAVIGATE_TO.spec.max)
} }
@Test @Test
fun `le sms porte un destinataire et un message`() { fun `an sms carries a recipient and a message`() {
// Le destinataire est un numéro (ou un contact du répertoire PBAP) et le message est // The recipient is a number (or a contact from the PBAP phone book) and the message is
// du texte libre : un seul contrôle ne pourrait pas porter les deux. // free text: a single control could not carry both.
assertEquals(ValueKind.SMS, ActionType.SEND_SMS.spec.kind) assertEquals(ValueKind.SMS, ActionType.SEND_SMS.spec.kind)
assertTrue(ActionType.SEND_SMS.spec.hintRes != 0) assertTrue(ActionType.SEND_SMS.spec.hintRes != 0)
assertFalse("le SMS n'écrit rien dans le véhicule", ActionType.SEND_SMS.gated) assertFalse("SMS writes nothing to the vehicle", ActionType.SEND_SMS.gated)
} }
@Test @Test
fun `la demande de confirmation porte sa question et son delai`() { fun `the confirmation request carries its question and its delay`() {
// Le texte EST la question posée au conducteur ; un exemple est indispensable, la // The text IS the question put to the driver; an example is essential, the default
// formulation par défaut d'un champ vide n'apprend rien. Le nombre est l'attente en // wording of an empty field teaches nothing. The number is the wait in seconds,
// secondes, bornée pour qu'une question reste lisible sans bloquer le cycle. // bounded so that a question stays readable without blocking the cycle.
assertEquals(ValueKind.CONFIRM, ActionType.ASK_CONFIRM.spec.kind) assertEquals(ValueKind.CONFIRM, ActionType.ASK_CONFIRM.spec.kind)
assertTrue(ActionType.ASK_CONFIRM.spec.hintRes != 0) assertTrue(ActionType.ASK_CONFIRM.spec.hintRes != 0)
assertTrue(ActionType.ASK_CONFIRM.spec.min > 0) assertTrue(ActionType.ASK_CONFIRM.spec.min > 0)
assertTrue(ActionType.ASK_CONFIRM.spec.max > ActionType.ASK_CONFIRM.spec.min) assertTrue(ActionType.ASK_CONFIRM.spec.max > ActionType.ASK_CONFIRM.spec.min)
// Le défaut des règles enregistrées sans délai doit rester dans les bornes offertes. // The default for rules saved without a delay must stay within the offered bounds.
assertTrue(ActionType.ASK_CONFIRM_DEFAULT_SECONDS in assertTrue(ActionType.ASK_CONFIRM_DEFAULT_SECONDS in
ActionType.ASK_CONFIRM.spec.min..ActionType.ASK_CONFIRM.spec.max) ActionType.ASK_CONFIRM.spec.min..ActionType.ASK_CONFIRM.spec.max)
assertFalse("la question ne touche pas le véhicule", ActionType.ASK_CONFIRM.gated) assertFalse("the question does not touch the vehicle", ActionType.ASK_CONFIRM.gated)
} }
@Test @Test
fun `toute action vehicule cible une action du pont`() { fun `every vehicle action targets a bridge action`() {
// bridgeAction=null est réservé aux actions locales (lancer une app, notifier). // bridgeAction=null is reserved for local actions (launch an app, notify).
// Une action véhicule sans cible serait silencieusement ignorée à l'exécution. // A vehicle action without a target would be silently ignored when run.
val localOnly = setOf( val localOnly = setOf(
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT, ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM, ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
@@ -75,12 +74,12 @@ class CatalogConsistencyTest {
) )
ActionType.entries.filterNot { it in localOnly }.forEach { type -> ActionType.entries.filterNot { it in localOnly }.forEach { type ->
assertNotNull("${type.name} n'a pas de bridgeAction", type.bridgeAction) assertNotNull("${type.name} has no bridgeAction", type.bridgeAction)
} }
} }
@Test @Test
fun `les actions locales ne passent pas par le pont`() { fun `local actions do not go through the bridge`() {
listOf( listOf(
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT, ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM, ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
@@ -89,17 +88,17 @@ class CatalogConsistencyTest {
ActionType.SET_BLUETOOTH, ActionType.SET_WIFI ActionType.SET_BLUETOOTH, ActionType.SET_WIFI
).forEach { type -> ).forEach { type ->
assertTrue( assertTrue(
"${type.name} ne touche pas le véhicule et ne doit pas avoir de bridgeAction", "${type.name} does not touch the vehicle and must not have a bridgeAction",
type.bridgeAction == null type.bridgeAction == null
) )
} }
} }
@Test @Test
fun `les ecritures de comportement routier sont marquees gated`() { fun `writes that change driving behaviour are marked gated`() {
// Le marquage ne remplace PAS le verrou côté EVProfile, qui reste l'autorité. // The marking does NOT replace the lock on the EVProfile side, which stays the authority.
// Il conditionne l'avertissement affiché à l'utilisateur au moment du choix : // It decides the warning shown to the user when choosing:
// une action gated non marquée se présenterait comme applicable en roulant. // an unmarked gated action would present itself as usable while driving.
val mustBeGated = setOf( val mustBeGated = setOf(
ActionType.APPLY_PROFILE, ActionType.APPLY_PROFILE,
ActionType.SET_DRIVE_MODE, ActionType.SET_REGEN_LEVEL, ActionType.SET_ONE_PEDAL, ActionType.SET_DRIVE_MODE, ActionType.SET_REGEN_LEVEL, ActionType.SET_ONE_PEDAL,
@@ -113,16 +112,16 @@ class CatalogConsistencyTest {
) )
mustBeGated.forEach { type -> mustBeGated.forEach { type ->
assertTrue("${type.name} modifie le comportement routier et doit être gated", type.gated) assertTrue("${type.name} changes driving behaviour and must be gated", type.gated)
} }
} }
@Test @Test
fun `couper le vehicule n est pas automatisable`() { fun `switching the vehicle off cannot be automated`() {
// Une règle ne doit jamais pouvoir éteindre la voiture : c'est irréversible pour // A rule must never be able to switch the car off: that is irreversible for the
// le conducteur, et ça reste réservé à un geste humain dans EVProfile. // driver, and it stays reserved for a human gesture in EVProfile.
assertFalse( assertFalse(
"aucune action du catalogue ne doit couper le véhicule", "no catalogue action may switch the vehicle off",
ActionType.entries.any { ActionType.entries.any {
it.bridgeAction?.contains("POWER_OFF", ignoreCase = true) == true it.bridgeAction?.contains("POWER_OFF", ignoreCase = true) == true
} }
@@ -130,9 +129,9 @@ class CatalogConsistencyTest {
} }
@Test @Test
fun `toute condition vehicule lit une cle de l instantane`() { fun `every vehicle condition reads a snapshot key`() {
// Les conditions calculées localement (heure, jour, Bluetooth) n'ont pas de clé ; // Conditions computed locally (time, day, Bluetooth) have no key; all the others need
// toutes les autres en ont besoin, sinon elles seraient toujours indisponibles. // one, or they would always be unavailable.
val localOnly = setOf( val localOnly = setOf(
ConditionType.BT_DEVICE_CONNECTED, ConditionType.BT_DEVICE_CONNECTED,
ConditionType.ANY_BT_CONNECTED, ConditionType.ANY_BT_CONNECTED,
@@ -142,12 +141,12 @@ class CatalogConsistencyTest {
ConditionType.DAY_OF_WEEK, ConditionType.DAY_OF_WEEK,
ConditionType.DATE, ConditionType.DATE,
ConditionType.LOCATION_WITHIN, ConditionType.LOCATION_WITHIN,
// Tiré au sort à l'évaluation : il n'y a rien à lire. // Drawn at random when evaluated: there is nothing to read.
ConditionType.RANDOM_CHANCE ConditionType.RANDOM_CHANCE
) )
ConditionType.entries.filterNot { it in localOnly }.forEach { type -> ConditionType.entries.filterNot { it in localOnly }.forEach { type ->
assertNotNull("${type.name} n'a pas de clé d'instantané", type.snapshotKey) assertNotNull("${type.name} has no snapshot key", type.snapshotKey)
} }
} }
@@ -363,32 +362,32 @@ class CatalogConsistencyTest {
} }
@Test @Test
fun `les enumerations ont des options`() { fun `enumerations have options`() {
// Une ENUM sans option produit une liste déroulante vide : l'utilisateur ne peut // An ENUM without options gives an empty drop-down: the user cannot choose anything
// rien choisir et la règle reste inutilisable. // and the rule stays unusable.
ConditionType.entries ConditionType.entries
.filter { it.spec.kind == ValueKind.ENUM && it != ConditionType.FIRMWARE_GEN } .filter { it.spec.kind == ValueKind.ENUM && it != ConditionType.FIRMWARE_GEN }
.forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) } .forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) }
// RADIO dessine aussi une liste déroulante — celle des bandes — donc la même règle // RADIO draws a drop-down too — the bands — so the same rule applies: without
// s'applique : sans option, l'action ne peut nommer aucune bande. // options, the action cannot name any band.
ActionType.entries ActionType.entries
.filter { it.spec.kind == ValueKind.ENUM || it.spec.kind == ValueKind.RADIO } .filter { it.spec.kind == ValueKind.ENUM || it.spec.kind == ValueKind.RADIO }
.forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) } .forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) }
} }
@Test @Test
fun `les bornes numeriques sont coherentes`() { fun `numeric bounds are consistent`() {
val specs = ConditionType.entries.map { it.name to it.spec } + val specs = ConditionType.entries.map { it.name to it.spec } +
ActionType.entries.map { it.name to it.spec } ActionType.entries.map { it.name to it.spec }
specs.filter { it.second.kind == ValueKind.NUMBER }.forEach { (name, spec) -> specs.filter { it.second.kind == ValueKind.NUMBER }.forEach { (name, spec) ->
if (spec.max >= 0) { if (spec.max >= 0) {
assertTrue("$name : max doit dépasser min", spec.max > spec.min) assertTrue("$name: max must exceed min", spec.max > spec.min)
} else { } else {
// max = -1 : borne résolue à l'exécution. Le repli doit rester utilisable // max = -1: the bound is resolved at run time. The fallback must stay usable
// même si le véhicule ne répond pas. // even if the vehicle does not answer.
assertTrue("$name : fallbackMax doit dépasser min", spec.fallbackMax > spec.min) assertTrue("$name: fallbackMax must exceed min", spec.fallbackMax > spec.min)
} }
} }
} }
+12 -4
View File
@@ -48,7 +48,8 @@ on the cluster since 2026-10-25).
(telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all (telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all
dashboard queries return data). dashboard queries return data).
- [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.** - [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.**
- [ ] Check Grafana's host in `k8s/ingress.yaml` (placeholder `car-grafana.k8s.stinnesbeck.com`). - [x] Grafana's host in `k8s/ingress.yaml` is `car-grafana.k8s.stinnesbeck.com`, the one in use
(owner, 2026-10-25).
- [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN). - [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN).
- [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour - [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour
is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing
@@ -88,9 +89,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL
(`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead (`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead
of a volume. The car data itself is in InfluxDB and unaffected. of a volume. The car data itself is in InfluxDB and unaffected.
- [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored - [x] **EVHardware's comments are partly French.** Translated 2026-10-25 (see the done log).
`EVHardware/` library (mostly `EVHardware.kt`, some tests), half-translated upstream. The
app, the docs and the build files are English.
- [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits - [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits
energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V
and the rest. SAIC's charging service, which EVHardware already binds, has counters that and the rest. SAIC's charging service, which EVHardware already binds, has counters that
@@ -139,6 +138,15 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
- 2026-10-25 — EVHardware in English: the French comments (and the half-translated ones like
"l'screen", "registersment", "initialisesd") in `EVHardware.kt` and five test files, the French
log messages ("aucun init() fonctionnel", "introuvable", "state initial", "ERREUR"), the French
test names and assertion messages (`CatalogConsistencyTest`, `GlassEvidenceTest`,
`GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the
diff). Kept on purpose: the French translation `values-fr/strings.xml`, place names in test
data, the quoted French sentence in `ChargeStopPlan` and the voice phrase in `SaicNavGuidance`,
and "Katman" (Turkish for layer), which names the layers in function names and logs — now
explained at the top of `EVHardware.kt`. EVHardware, app tests and lint pass.
- 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the - 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the
emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed), emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed),
"read-only" now names the drive mode exception, the upload frequency matches the app (1 min by "read-only" now names the drive mode exception, the upload frequency matches the app (1 min by