The settings card stops being a Modbus card
Both transports that read the charger can also be told things, so both get the card now. Modbus has four writable registers and keeps the four controls it had. The cloud has the charger's whole settings group — two dozen of them, everything the Anker app sets short of the card list — and they were readable in the readings card and settable nowhere. Drawn from a table rather than written out one control at a time, because the charger's commands own sets of fields and take a command whole: a schedule frame carrying only its switch is a schedule whose times have just been set to midnight. So a section is one command, its fields are sent together, and Apply is per section. Only what the charger has reported gets a control — a blank box that writes whatever it was left at is worse than no box when the value travels as a sibling. The meter and monitor the balancing features watch stay read-only: there is no command for them, and what their modes select is undocumented. The phase select offers the automatic and single-phase this command carries, and shows a reported three-phase rather than reading as something the charger did not say. Nothing left the readings card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3064bff8ad
commit
9d4aecb668
@@ -154,6 +154,18 @@
|
||||
"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.",
|
||||
"blockCharging": "Opladning",
|
||||
"blockSchedule": "Tidsplan",
|
||||
"blockBalancing": "Belastningsbalancering",
|
||||
"blockSolar": "Sol",
|
||||
"blockPanel": "Panel og lys",
|
||||
"blockLocal": "Lokalt netværk",
|
||||
"windowStart": "Start",
|
||||
"windowEnd": "Slut",
|
||||
"reset": "Fortryd",
|
||||
"cloudSettingsHint": "Laderens egne indstillinger, skrevet via Anker-skyen. Et afsnit er én kommando til laderen, så dets felter anvendes samlet.",
|
||||
"cloudSettingsReportedHint": "Laderen rapporterer disse; ingen kommando skriver dem. Hvad de to tilstande og flaget vælger, er udokumenteret, så de vises som de tal, de er.",
|
||||
"modbusOffWarning": "Med Modbus TCP-serveren slået fra svarer laderen ikke længere på det lokale netværk, og Modbus-styringstilstanden har intet at ringe op.",
|
||||
"device": "Enhed",
|
||||
"alarms": "Alarmer",
|
||||
"phase": "Fase",
|
||||
|
||||
@@ -115,6 +115,18 @@
|
||||
"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.",
|
||||
"blockCharging": "Charging",
|
||||
"blockSchedule": "Schedule",
|
||||
"blockBalancing": "Load balancing",
|
||||
"blockSolar": "Solar",
|
||||
"blockPanel": "Panel and light",
|
||||
"blockLocal": "Local network",
|
||||
"windowStart": "Start",
|
||||
"windowEnd": "End",
|
||||
"reset": "Undo",
|
||||
"cloudSettingsHint": "The charger's own settings, written over the Anker cloud. A section is one command to the charger, so its fields are applied together.",
|
||||
"cloudSettingsReportedHint": "The charger reports these; no command writes them. What the two modes and the flag select is undocumented, so they are shown as the numbers they are.",
|
||||
"modbusOffWarning": "With the Modbus TCP server off the charger stops answering on the local network, and the Modbus control mode has nothing left to dial.",
|
||||
"device": "Device",
|
||||
"alarms": "Alarms",
|
||||
"phase": "Phase",
|
||||
|
||||
@@ -156,6 +156,18 @@
|
||||
"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.",
|
||||
"blockCharging": "Ładowanie",
|
||||
"blockSchedule": "Harmonogram",
|
||||
"blockBalancing": "Balansowanie obciążenia",
|
||||
"blockSolar": "Fotowoltaika",
|
||||
"blockPanel": "Panel i podświetlenie",
|
||||
"blockLocal": "Sieć lokalna",
|
||||
"windowStart": "Początek",
|
||||
"windowEnd": "Koniec",
|
||||
"reset": "Cofnij",
|
||||
"cloudSettingsHint": "Własne ustawienia ładowarki, zapisywane przez chmurę Anker. Jedna sekcja to jedno polecenie do ładowarki, więc jej pola są zapisywane razem.",
|
||||
"cloudSettingsReportedHint": "Ładowarka je zgłasza, ale żadne polecenie ich nie zapisuje. Nie wiadomo, co wybierają te dwa tryby i flaga, więc pokazane są jako liczby, którymi są.",
|
||||
"modbusOffWarning": "Przy wyłączonym serwerze Modbus TCP ładowarka przestaje odpowiadać w sieci lokalnej, a tryb sterowania Modbus nie ma już pod co zadzwonić.",
|
||||
"device": "Urządzenie",
|
||||
"alarms": "Alarmy",
|
||||
"phase": "Faza",
|
||||
|
||||
@@ -527,6 +527,7 @@ function syncSettingsDraft() {
|
||||
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;
|
||||
syncMqttSettings();
|
||||
}
|
||||
|
||||
const boostOn = computed(() => !!(dev.value.settings || {}).boost);
|
||||
@@ -578,6 +579,213 @@ const deviceSettingsReported = computed(() => {
|
||||
]);
|
||||
});
|
||||
|
||||
// --- What the cloud transport can be told -------------------------------------
|
||||
//
|
||||
// Modbus has four writable registers. The cloud has the charger's whole settings
|
||||
// group: everything the Anker app can set on it short of the card list. They are
|
||||
// the same names a settings write takes and the same ones the snapshot reports
|
||||
// them under, so every control here is seeded from the charger, edited, and sent
|
||||
// back by name.
|
||||
//
|
||||
// A table rather than two dozen hand-written controls, because the charger's own
|
||||
// commands own *sets* of fields: a command is taken whole, and a schedule that
|
||||
// arrives carrying only its switch is a schedule whose times have just been set
|
||||
// to midnight. A block is one write, and that stays true as fields are added to
|
||||
// it only if the blocks are data.
|
||||
//
|
||||
// `at` is where the value is read back from in the snapshot - the settings
|
||||
// object for most, the top level for the ones the charger reports outside it,
|
||||
// and `local` for the Modbus server switch. `max: null` means the ceiling is the
|
||||
// charger's own rating rather than a constant.
|
||||
const MQTT_SETTING_BLOCKS = [
|
||||
{
|
||||
id: "charging",
|
||||
title: "blockCharging",
|
||||
fields: [
|
||||
{ key: "maxCurrentA", at: "settings.maxCurrentA", label: "maxCurrentSet", type: "number", min: LIMIT_FLOOR, max: null, step: 1, unit: "A" },
|
||||
{ key: "autoStart", at: "settings.autoStart", label: "autoStart", type: "switch" },
|
||||
{ key: "randomDelay", at: "settings.randomDelay", label: "randomDelay", type: "switch" },
|
||||
{ key: "plugLock", at: "settings.plugLock", label: "plugLock", type: "switch" },
|
||||
{ key: "autoRestart", at: "settings.autoRestart", label: "autoRestart", type: "switch" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "schedule",
|
||||
title: "blockSchedule",
|
||||
fields: [
|
||||
{ key: "scheduleEnabled", at: "settings.scheduleEnabled", label: "scheduleEnabled", type: "switch" },
|
||||
{ key: "scheduleMode", at: "settings.scheduleMode", label: "scheduleMode", type: "option", enum: "scheduleMode", values: [0, 1] },
|
||||
{ type: "window", label: "scheduleWindow", from: "weekStart", to: "weekEnd", at: ["settings.weekStart", "settings.weekEnd"] },
|
||||
{ key: "weekendMode", at: "settings.weekendMode", label: "weekendMode", type: "option", enum: "weekendMode", values: [1, 2] },
|
||||
{ type: "window", label: "weekendWindow", from: "weekendStart", to: "weekendEnd", at: ["settings.weekendStart", "settings.weekendEnd"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "balancing",
|
||||
title: "blockBalancing",
|
||||
fields: [
|
||||
{ key: "loadBalancing", at: "loadBalancing", label: "loadBalancing", type: "switch" },
|
||||
{ key: "mainBreakerLimitA", at: "settings.mainBreakerLimitA", label: "mainBreakerLimit", type: "number", min: 10, max: 500, step: 1, unit: "A" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "solar",
|
||||
title: "blockSolar",
|
||||
fields: [
|
||||
{ key: "solarBalancing", at: "solarBalancing", label: "solarBalancing", type: "switch" },
|
||||
{ key: "solarChargeMode", at: "settings.solarChargeMode", label: "solarChargeMode", type: "option", enum: "solarMode", values: [0, 1] },
|
||||
{ key: "solarMinCurrentA", at: "settings.solarMinCurrentA", label: "solarMinCurrent", type: "number", min: LIMIT_FLOOR, max: 32, step: 1, unit: "A" },
|
||||
// This command offers automatic and single-phase only. The three-phase
|
||||
// setting is a Modbus register, and offering it here would be offering a
|
||||
// write that comes back refused.
|
||||
{ key: "phaseMode", at: "phaseMode", label: "phaseSetting", type: "option", enum: "phaseSet", values: [0, 1] },
|
||||
{ key: "autoPhaseSwitching", at: "settings.autoPhaseSwitching", label: "autoPhaseSwitching", type: "switch" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "panel",
|
||||
title: "blockPanel",
|
||||
fields: [
|
||||
{ key: "ledBrightness", at: "ledBrightness", label: "ledBrightness", type: "number", min: 0, max: 100, step: 10, unit: "%" },
|
||||
{ key: "lightOffSchedule", at: "settings.lightOffSchedule", label: "lightOff", type: "switch" },
|
||||
{ type: "window", label: "lightOffWindow", from: "lightOffStart", to: "lightOffEnd", at: ["settings.lightOffStart", "settings.lightOffEnd"] },
|
||||
{ key: "swipeUpMode", at: "swipeUpMode", label: "swipeUp", type: "option", enum: "gesture", values: [0, 1, 2, 3] },
|
||||
{ key: "swipeDownMode", at: "swipeDownMode", label: "swipeDown", type: "option", enum: "gesture", values: [0, 1, 2, 3] },
|
||||
{ key: "smartTouchMode", at: "smartTouchMode", label: "smartTouch", type: "option", enum: "touch", values: [0, 1] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "local",
|
||||
title: "blockLocal",
|
||||
// The one setting here that can cost you a control mode: with the server off
|
||||
// the charger stops answering on the LAN, and Modbus mode has nothing left
|
||||
// to dial. Said next to the switch rather than after it has been thrown.
|
||||
warning: "modbusOffWarning",
|
||||
fields: [{ key: "modbusEnabled", at: "local.modbusEnabled", label: "modbusServer", type: "switch" }],
|
||||
},
|
||||
];
|
||||
|
||||
// A field is one setting, or for a window the two ends of one.
|
||||
const fieldKeys = (f) => (f.type === "window" ? [f.from, f.to] : [f.key]);
|
||||
const fieldEntries = (f) =>
|
||||
f.type === "window"
|
||||
? [
|
||||
[f.from, f.at[0]],
|
||||
[f.to, f.at[1]],
|
||||
]
|
||||
: [[f.key, f.at]];
|
||||
|
||||
const snapshotValue = (path) => path.split(".").reduce((o, k) => (o == null ? o : o[k]), dev.value);
|
||||
|
||||
// The number input's ceiling: the charger's own rating where the field's is null.
|
||||
const fieldMax = (f) => (f.max === null ? limitCeiling.value : f.max);
|
||||
|
||||
// What the controls hold, and what the charger last said, so a block can tell
|
||||
// whether it has anything to send and can be put back if it has not sent it.
|
||||
const mqttDraft = ref({});
|
||||
const mqttBase = ref({});
|
||||
const mqttBusy = ref("");
|
||||
|
||||
function syncMqttSettings() {
|
||||
const draft = {};
|
||||
for (const block of MQTT_SETTING_BLOCKS) {
|
||||
for (const f of block.fields) {
|
||||
for (const [key, at] of fieldEntries(f)) {
|
||||
const v = snapshotValue(at);
|
||||
if (isSet(v) && v !== "") draft[key] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
mqttDraft.value = draft;
|
||||
mqttBase.value = { ...draft };
|
||||
}
|
||||
|
||||
// Only the settings the charger has actually reported get a control. A value it
|
||||
// has not sent is one nothing here could seed a control from, and a blank box
|
||||
// that writes whatever it was left at is worse than no box: several of these
|
||||
// travel as siblings on one command, where an invented value is not ignored but
|
||||
// applied.
|
||||
const mqttSettingBlocks = computed(() => {
|
||||
const base = mqttBase.value;
|
||||
return MQTT_SETTING_BLOCKS.map((b) => ({
|
||||
...b,
|
||||
fields: b.fields.filter((f) => fieldKeys(f).every((k) => k in base)),
|
||||
})).filter((b) => b.fields.length);
|
||||
});
|
||||
|
||||
const blockDirty = (block) =>
|
||||
block.fields.some((f) => fieldKeys(f).some((k) => mqttDraft.value[k] !== mqttBase.value[k]));
|
||||
|
||||
// An empty number box reads as NaN, which would travel as null and come back as
|
||||
// a parse error from the server. The block simply cannot be applied until it
|
||||
// holds a number again.
|
||||
const blockValid = (block) =>
|
||||
block.fields.every((f) => fieldKeys(f).every((k) => !Number.isNaN(mqttDraft.value[k])));
|
||||
|
||||
// The options a select offers: the ones this command accepts, plus whatever the
|
||||
// charger actually reported if that is not among them. The phase field is why —
|
||||
// the charger reports the phase it is *running* on, which can be the three-phase
|
||||
// setting the solar command has no value for. Showing the reported value keeps
|
||||
// the select from silently reading as something the charger did not say; sending
|
||||
// it earns a refusal from the server, which is the honest outcome for a value
|
||||
// this command cannot carry.
|
||||
function fieldOptions(f) {
|
||||
const reported = mqttBase.value[f.key];
|
||||
return isSet(reported) && !f.values.includes(reported) ? [...f.values, reported] : f.values;
|
||||
}
|
||||
|
||||
function resetMqttBlock(block) {
|
||||
const draft = { ...mqttDraft.value };
|
||||
for (const f of block.fields) {
|
||||
for (const k of fieldKeys(f)) draft[k] = mqttBase.value[k];
|
||||
}
|
||||
mqttDraft.value = draft;
|
||||
}
|
||||
|
||||
// Applying one block. The whole block goes, not only what changed: several of
|
||||
// these are one command on the wire, and the charger takes a command as the new
|
||||
// truth for every field it carries, so the siblings travel back with the change.
|
||||
// The server would refill them from the charger's last report anyway; sending
|
||||
// what is on screen means what is on screen is what gets written.
|
||||
async function applyMqttBlock(block) {
|
||||
const sn = ctlSerial.value.trim();
|
||||
if (!sn || mqttBusy.value) return;
|
||||
const settings = {};
|
||||
for (const f of block.fields) {
|
||||
for (const k of fieldKeys(f)) {
|
||||
if (isSet(mqttDraft.value[k]) && mqttDraft.value[k] !== "") settings[k] = mqttDraft.value[k];
|
||||
}
|
||||
}
|
||||
if (!Object.keys(settings).length) return;
|
||||
mqttBusy.value = block.id;
|
||||
ctlError.value = "";
|
||||
try {
|
||||
await api.ankerControlAction(sn, "settings", { settings });
|
||||
// refreshCtl reseeds every control from the charger, so a value it clamped
|
||||
// or refused shows as what it took rather than as what was asked for.
|
||||
await refreshCtl();
|
||||
} catch (e) {
|
||||
ctlError.value = e.message;
|
||||
} finally {
|
||||
mqttBusy.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
// What the cloud reports in the settings group but has no command to write: the
|
||||
// meter and the monitor the two balancing features watch. The reference has not
|
||||
// pinned down what the modes and the flag select, which is also why there is no
|
||||
// control for them - a control would imply knowing what the values mean.
|
||||
const mqttSettingsReported = computed(() => {
|
||||
const s = dev.value;
|
||||
return rows([
|
||||
["loadBalanceMeter", s.loadBalanceMonitorSN],
|
||||
["loadBalanceMonitorMode", isSet(s.loadBalanceMonitorMode) ? String(s.loadBalanceMonitorMode) : null],
|
||||
["loadBalanceMeterFlag", isSet(s.loadBalanceMeterFlag) ? String(s.loadBalanceMeterFlag) : null],
|
||||
["solarMonitor", s.solarMonitorSN],
|
||||
["solarMonitoringMode", isSet(s.solarMonitoringMode) ? String(s.solarMonitoringMode) : null],
|
||||
]);
|
||||
});
|
||||
|
||||
const deviceIdentity = computed(() => {
|
||||
const s = dev.value;
|
||||
return rows([
|
||||
@@ -2020,9 +2228,14 @@ onMounted(async () => {
|
||||
<!-- What the charger is set to, as it reports it back. Its own card
|
||||
under the control one: these are the values those buttons write, so
|
||||
they are read right after pressing them, and they were buried at
|
||||
the bottom of a long readings card. -->
|
||||
the bottom of a long readings card.
|
||||
|
||||
Both transports that read the charger can also be told things, so
|
||||
both get this card - but they can be told very different things.
|
||||
Modbus has four registers; the cloud has the charger's whole
|
||||
settings group, which is why that half is drawn from a table. -->
|
||||
<div
|
||||
v-if="ctlIsModbus && ctlConnected && deviceSettings.length"
|
||||
v-if="ctlConnected && ((ctlIsModbus && deviceSettings.length) || (ctlIsCloud && mqttSettingBlocks.length))"
|
||||
class="dh-card p-4 transition-shadow duration-150"
|
||||
:class="dropCard === 'settings' ? 'ring-2 ring-accent' : ''"
|
||||
:style="{ order: cardOrder('settings') }"
|
||||
@@ -2049,6 +2262,7 @@ onMounted(async () => {
|
||||
</button>
|
||||
|
||||
<div v-show="isOpen('settings')">
|
||||
<template v-if="ctlIsModbus">
|
||||
<!-- 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">
|
||||
@@ -2145,6 +2359,123 @@ onMounted(async () => {
|
||||
</dl>
|
||||
<p class="mt-2 text-[11px] text-muted">{{ t("charging.modbus.settingsReportedHint") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- The cloud's half. One section per block, and one write per
|
||||
section: the charger takes a command whole, so its fields are
|
||||
sent together and Apply is per block rather than per control. -->
|
||||
<template v-else-if="ctlIsCloud">
|
||||
<p class="mt-3 text-[11px] text-muted">{{ t("charging.modbus.cloudSettingsHint") }}</p>
|
||||
|
||||
<section v-for="block in mqttSettingBlocks" :key="block.id" class="mt-2 rounded-control bg-sunken p-3">
|
||||
<h4 class="eyebrow">{{ t(`charging.modbus.${block.title}`) }}</h4>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<div
|
||||
v-for="f in block.fields"
|
||||
:key="f.key || f.from"
|
||||
class="flex items-center justify-between gap-3"
|
||||
>
|
||||
<label class="dh-label !mb-0 min-w-0 grow" :for="`set-${f.key || f.from}`">
|
||||
{{ t(`charging.modbus.${f.label}`) }}
|
||||
</label>
|
||||
|
||||
<!-- A switch reads as what it is set to, not as a verb: the
|
||||
card is a form, and the button says the value it will
|
||||
send rather than the action it would take. -->
|
||||
<button
|
||||
v-if="f.type === 'switch'"
|
||||
:id="`set-${f.key}`"
|
||||
type="button"
|
||||
class="dh-btn shrink-0 !px-3 !py-1 text-xs"
|
||||
:class="mqttDraft[f.key] ? 'dh-btn-primary' : 'dh-btn-ghost'"
|
||||
:aria-pressed="!!mqttDraft[f.key]"
|
||||
@click="mqttDraft[f.key] = !mqttDraft[f.key]"
|
||||
>
|
||||
{{ mqttDraft[f.key] ? t("common.yes") : t("common.no") }}
|
||||
</button>
|
||||
|
||||
<select
|
||||
v-else-if="f.type === 'option'"
|
||||
:id="`set-${f.key}`"
|
||||
v-model.number="mqttDraft[f.key]"
|
||||
class="dh-input w-48 shrink-0"
|
||||
>
|
||||
<option v-for="v in fieldOptions(f)" :key="v" :value="v">{{ enumLabel(f.enum, v) }}</option>
|
||||
</select>
|
||||
|
||||
<span v-else-if="f.type === 'number'" class="flex shrink-0 items-center gap-1">
|
||||
<input
|
||||
:id="`set-${f.key}`"
|
||||
v-model.number="mqttDraft[f.key]"
|
||||
type="number"
|
||||
:min="f.min"
|
||||
:max="fieldMax(f)"
|
||||
:step="f.step"
|
||||
class="dh-input w-24"
|
||||
/>
|
||||
<span class="text-[11px] text-muted">{{ f.unit }}</span>
|
||||
</span>
|
||||
|
||||
<span v-else class="flex shrink-0 items-center gap-1">
|
||||
<input
|
||||
:id="`set-${f.from}`"
|
||||
v-model="mqttDraft[f.from]"
|
||||
type="time"
|
||||
class="dh-input w-28"
|
||||
:aria-label="`${t(`charging.modbus.${f.label}`)} — ${t('charging.modbus.windowStart')}`"
|
||||
/>
|
||||
<span class="text-[11px] text-muted">–</span>
|
||||
<input
|
||||
v-model="mqttDraft[f.to]"
|
||||
type="time"
|
||||
class="dh-input w-28"
|
||||
:aria-label="`${t(`charging.modbus.${f.label}`)} — ${t('charging.modbus.windowEnd')}`"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="block.warning" class="mt-2 text-[11px]" style="color: var(--warning-600)">
|
||||
{{ t(`charging.modbus.${block.warning}`) }}
|
||||
</p>
|
||||
|
||||
<!-- Nothing to apply until something differs from what the
|
||||
charger reported, so the button says so by being off. -->
|
||||
<div class="mt-2 flex items-center justify-end gap-2">
|
||||
<button
|
||||
v-if="blockDirty(block)"
|
||||
type="button"
|
||||
class="dh-btn dh-btn-ghost !px-2 !py-1 text-xs"
|
||||
:disabled="mqttBusy !== ''"
|
||||
@click="resetMqttBlock(block)"
|
||||
>
|
||||
{{ t("charging.modbus.reset") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dh-btn !px-3 !py-1 text-xs"
|
||||
:class="blockDirty(block) ? 'dh-btn-primary' : 'dh-btn-ghost'"
|
||||
:disabled="mqttBusy !== '' || !blockDirty(block) || !blockValid(block)"
|
||||
@click="applyMqttBlock(block)"
|
||||
>
|
||||
{{ mqttBusy === block.id ? t("common.loading") : t("charging.modbus.apply") }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- The settings group's remainder: reported, and with no command
|
||||
to write them. -->
|
||||
<div v-if="mqttSettingsReported.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 mqttSettingsReported" :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.cloudSettingsReportedHint") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="ctlError" class="mt-2 text-sm text-danger">{{ ctlError }}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user