The settings card sets things
The card added last commit showed the charger's settings and did nothing with them, which for a page whose whole point is acting on the charger is half a card. It also took the settings block out of the readings card to do it, so reading the charger top to bottom now had a hole in it. The readings card is whole again — phases, live data, settings, device, alarms, exactly as before. What the settings card holds is the same values with controls on them. Which values get a control is the register map's decision, not a design one. Six holding registers are writable, and four of them are settings: the current ceiling, boost, the timeout and the phase count. Charging mode, the two balancing flags and the LED brightness sit in the measurement block, which the charger reports over FC04 and does not accept writes on — they are set in the Anker app. So the card is in two halves and says which is which, rather than offering a control that would quietly do nothing. The limits come from the same places the server's own checks do: the slider floors at 6 A because below that the charger pauses instead of charging slowly and ModbusSetMaxCurrent refuses it, its ceiling is the charger's reported rating, and the timeout floors just above the spec's "more than five seconds". Phase and boost write on the change itself, having one value each; current and timeout are typed, so they wait for Apply. Every write goes through the existing control action, so it is gated, rate limited and audited like the buttons in the control card, and the card reseeds from the snapshot afterwards — the charger clamps what it is given, and the form should show what it took rather than what was asked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e1c063d4b1
commit
65a5c67afc
@@ -117,6 +117,14 @@
|
||||
"live": "Live data",
|
||||
"settings": "Indstillinger",
|
||||
"settingsTitle": "Laderindstillinger",
|
||||
"apply": "Anvend",
|
||||
"turnOn": "Slå til",
|
||||
"turnOff": "Slå fra",
|
||||
"limitFloorHint": "{amps} A er bunden — derunder holder laderen pause i stedet for at lade langsomt.",
|
||||
"boostHint": "Kun den aktuelle session; laderen rydder det, når sessionen slutter.",
|
||||
"timeoutHint": "Mindst {n} sekunder. Laderen falder tilbage til sin egen strategi, hvis intet skriver inden for tiden.",
|
||||
"settingsReported": "Rapporteret, kan ikke indstilles",
|
||||
"settingsReportedHint": "Laderen rapporterer disse; Modbus-kortet har intet register til at skrive dem. Ret dem i Anker-appen.",
|
||||
"device": "Enhed",
|
||||
"alarms": "Alarmer",
|
||||
"phase": "Fase",
|
||||
|
||||
@@ -103,6 +103,14 @@
|
||||
"live": "Live data",
|
||||
"settings": "Settings",
|
||||
"settingsTitle": "Charger settings",
|
||||
"apply": "Apply",
|
||||
"turnOn": "Turn on",
|
||||
"turnOff": "Turn off",
|
||||
"limitFloorHint": "{amps} A is the floor — below it the charger pauses rather than charging slowly.",
|
||||
"boostHint": "The current session only; the charger clears it when the session ends.",
|
||||
"timeoutHint": "At least {n} seconds. The charger falls back to its own strategy if nothing writes within it.",
|
||||
"settingsReported": "Reported, not settable",
|
||||
"settingsReportedHint": "The charger reports these; the Modbus map has no register to write them. Change them in the Anker app.",
|
||||
"device": "Device",
|
||||
"alarms": "Alarms",
|
||||
"phase": "Phase",
|
||||
|
||||
@@ -119,6 +119,14 @@
|
||||
"live": "Dane na żywo",
|
||||
"settings": "Ustawienia",
|
||||
"settingsTitle": "Ustawienia ładowarki",
|
||||
"apply": "Zastosuj",
|
||||
"turnOn": "Włącz",
|
||||
"turnOff": "Wyłącz",
|
||||
"limitFloorHint": "{amps} A to dolna granica — poniżej ładowarka wstrzymuje ładowanie, zamiast ładować wolniej.",
|
||||
"boostHint": "Tylko bieżąca sesja; ładowarka kasuje to po jej zakończeniu.",
|
||||
"timeoutHint": "Co najmniej {n} sekund. Bez zapisu w tym czasie ładowarka wraca do własnej strategii.",
|
||||
"settingsReported": "Raportowane, nieustawialne",
|
||||
"settingsReportedHint": "Ładowarka je raportuje; mapa Modbus nie ma rejestru do ich zapisu. Zmień je w aplikacji Anker.",
|
||||
"device": "Urządzenie",
|
||||
"alarms": "Alarmy",
|
||||
"phase": "Faza",
|
||||
|
||||
@@ -379,6 +379,55 @@ const modbusSettings = computed(() => {
|
||||
]);
|
||||
});
|
||||
|
||||
// --- What can actually be set -------------------------------------------------
|
||||
//
|
||||
// Only the control block is writable: the current ceiling, boost, the timeout
|
||||
// and the phase count (21001-21005), plus the start/stop command the control
|
||||
// card sends. Everything else in the settings readback — charging mode, the two
|
||||
// balancing flags, the LED — lives in the measurement registers, which the
|
||||
// charger reports and the Anker app sets. So this card offers controls for the
|
||||
// four and shows the rest as what they are: a readback.
|
||||
const draftAmps = ref(16);
|
||||
const draftSeconds = ref(120);
|
||||
const draftPhase = ref(0);
|
||||
|
||||
// Seeded from the charger, not from the last thing typed. refreshCtl runs after
|
||||
// every write, so the form ends up showing what the charger took — which is not
|
||||
// always what was asked for, since it clamps the current to its own rating.
|
||||
function syncSettingsDraft() {
|
||||
const set = mb.value.settings || {};
|
||||
if (isSet(set.maxCurrentA)) draftAmps.value = Math.round(set.maxCurrentA);
|
||||
if (isSet(set.timeoutSeconds)) draftSeconds.value = set.timeoutSeconds;
|
||||
if (isSet(set.phaseSetting)) draftPhase.value = set.phaseSetting;
|
||||
}
|
||||
|
||||
const boostOn = computed(() => !!(mb.value.settings || {}).boost);
|
||||
|
||||
// The charger pauses below 6 A rather than charging slowly, and the server
|
||||
// refuses that case outright, so the slider does not offer it. The ceiling comes
|
||||
// from the charger's own rating where it reports one.
|
||||
const LIMIT_FLOOR = 6;
|
||||
const limitCeiling = computed(() => Math.round(mb.value.maxCurrentA || 32));
|
||||
|
||||
// The timeout the charger falls back on its own strategy after. The spec's floor
|
||||
// is "more than five seconds"; a minute of slack above it is a sane lower bound
|
||||
// for a control that is set by hand.
|
||||
const TIMEOUT_FLOOR = 6;
|
||||
|
||||
// The settings rows there is no register to write. Same labels and formatting as
|
||||
// the readings card's settings block, minus the four that have controls above.
|
||||
const modbusSettingsReported = computed(() => {
|
||||
const s = mb.value;
|
||||
const set = s.settings || {};
|
||||
return rows([
|
||||
["lastCommand", enumLabel("command", set.lastCommand)],
|
||||
["chargingMode", enumLabel("chargingMode", s.chargingMode)],
|
||||
["loadBalancing", yesNo(s.loadBalancing)],
|
||||
["solarBalancing", yesNo(s.solarBalancing)],
|
||||
["ledBrightness", isSet(s.ledBrightness) ? `${s.ledBrightness} %` : null],
|
||||
]);
|
||||
});
|
||||
|
||||
const modbusDevice = computed(() => {
|
||||
const s = mb.value;
|
||||
return rows([
|
||||
@@ -701,6 +750,7 @@ async function refreshCtl() {
|
||||
// one sitting in the field, where saving would move it to the wrong charger.
|
||||
modbusHost.value = ctl.value?.modbusHost || "";
|
||||
modbusPort.value = ctl.value?.modbusPort || 502;
|
||||
syncSettingsDraft();
|
||||
} catch (e) {
|
||||
ctlError.value = e.message;
|
||||
ctl.value = null;
|
||||
@@ -1121,13 +1171,104 @@ onMounted(async () => {
|
||||
</button>
|
||||
|
||||
<div v-show="isOpen('settings')">
|
||||
<!-- The card is the heading, so the group needs none of its own. -->
|
||||
<dl class="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 rounded-control bg-sunken p-3">
|
||||
<template v-for="r in modbusSettings" :key="r.label">
|
||||
<dt class="text-xs text-muted">{{ r.label }}</dt>
|
||||
<dd class="data text-right text-xs text-strong">{{ r.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<!-- Current limit. The slider says what it will do at the floor,
|
||||
because 6 A is a pause and not a slow charge. -->
|
||||
<div class="mt-3 rounded-control bg-sunken p-3">
|
||||
<label class="dh-label flex justify-between">
|
||||
<span>{{ t("charging.modbus.maxCurrentSet") }}</span>
|
||||
<span class="data text-body">{{ draftAmps }} A</span>
|
||||
</label>
|
||||
<input
|
||||
v-model.number="draftAmps"
|
||||
type="range"
|
||||
:min="LIMIT_FLOOR"
|
||||
:max="limitCeiling"
|
||||
step="1"
|
||||
class="w-full accent-[var(--accent)]"
|
||||
/>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<p class="grow text-[11px] text-muted">{{ t("charging.modbus.limitFloorHint", { amps: LIMIT_FLOOR }) }}</p>
|
||||
<button
|
||||
class="dh-btn dh-btn-ghost shrink-0"
|
||||
:disabled="ctlBusy === 'limit'"
|
||||
@click="doAction('limit', { amps: draftAmps })"
|
||||
>
|
||||
{{ t("charging.modbus.apply") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase count and boost both write a single register, so they are
|
||||
sent on the change itself rather than through an Apply. -->
|
||||
<div class="mt-2 rounded-control bg-sunken p-3">
|
||||
<label class="dh-label" for="phase-setting">{{ t("charging.modbus.phaseSetting") }}</label>
|
||||
<select
|
||||
id="phase-setting"
|
||||
v-model.number="draftPhase"
|
||||
class="dh-input"
|
||||
:disabled="ctlBusy === 'phase'"
|
||||
@change="doAction('phase', { phase: draftPhase })"
|
||||
>
|
||||
<option :value="0">{{ t("charging.modbus.phaseSet0") }}</option>
|
||||
<option :value="1">{{ t("charging.modbus.phaseSet1") }}</option>
|
||||
<option :value="2">{{ t("charging.modbus.phaseSet2") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex items-center justify-between gap-3 rounded-control bg-sunken p-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-strong">{{ t("charging.modbus.boostSet") }}</p>
|
||||
<p class="mt-0.5 text-[11px] text-muted">{{ t("charging.modbus.boostHint") }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="dh-btn shrink-0"
|
||||
:class="boostOn ? 'dh-btn-primary' : 'dh-btn-ghost'"
|
||||
:disabled="ctlBusy === 'boost'"
|
||||
@click="doAction('boost', { on: !boostOn })"
|
||||
>
|
||||
{{ boostOn ? t("charging.modbus.turnOff") : t("charging.modbus.turnOn") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- The charger falls back to its own strategy when nothing writes
|
||||
within this, so it is a setting worth reaching. -->
|
||||
<div class="mt-2 rounded-control bg-sunken p-3">
|
||||
<label class="dh-label" for="control-timeout">{{ t("charging.modbus.timeout") }}</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="control-timeout"
|
||||
v-model.number="draftSeconds"
|
||||
type="number"
|
||||
:min="TIMEOUT_FLOOR"
|
||||
max="65535"
|
||||
class="dh-input grow"
|
||||
@keyup.enter="doAction('timeout', { seconds: draftSeconds })"
|
||||
/>
|
||||
<button
|
||||
class="dh-btn dh-btn-ghost shrink-0"
|
||||
:disabled="ctlBusy === 'timeout' || draftSeconds < TIMEOUT_FLOOR"
|
||||
@click="doAction('timeout', { seconds: draftSeconds })"
|
||||
>
|
||||
{{ t("charging.modbus.apply") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-[11px] text-muted">{{ t("charging.modbus.timeoutHint", { n: TIMEOUT_FLOOR }) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- The rest of the settings block. The charger reports these, but
|
||||
the register map has nothing to write them with. -->
|
||||
<div v-if="modbusSettingsReported.length" class="mt-2 rounded-control bg-sunken p-3">
|
||||
<h4 class="eyebrow">{{ t("charging.modbus.settingsReported") }}</h4>
|
||||
<dl class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1">
|
||||
<template v-for="r in modbusSettingsReported" :key="r.label">
|
||||
<dt class="text-xs text-muted">{{ r.label }}</dt>
|
||||
<dd class="data text-right text-xs text-strong">{{ r.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p class="mt-2 text-[11px] text-muted">{{ t("charging.modbus.settingsReportedHint") }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="ctlError" class="mt-2 text-sm text-danger">{{ ctlError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1319,6 +1460,16 @@ onMounted(async () => {
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section v-if="modbusSettings.length" class="rounded-control bg-sunken p-3">
|
||||
<h4 class="eyebrow">{{ t("charging.modbus.settings") }}</h4>
|
||||
<dl class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1">
|
||||
<template v-for="r in modbusSettings" :key="r.label">
|
||||
<dt class="text-xs text-muted">{{ r.label }}</dt>
|
||||
<dd class="data text-right text-xs text-strong">{{ r.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section v-if="modbusDevice.length" class="rounded-control bg-sunken p-3">
|
||||
<h4 class="eyebrow">{{ t("charging.modbus.device") }}</h4>
|
||||
<dl class="mt-2 grid grid-cols-2 gap-x-3 gap-y-1">
|
||||
|
||||
Reference in New Issue
Block a user