The charger's address, asked for where control is set up
Modbus mode had no way in from the app: the mode could be picked in Settings, but the address the server dials had to be PUT by hand. The control card now asks for it in the place the charger is already being controlled from. Which provisioning the card shows follows the mode, because the two are not alternatives to each other — OCPP installs a token into the charger, Modbus records where the charger is. So does what the card offers: boost is a Modbus command and appears there, clear-limit and reset are OCPP ones the register map has no equivalent for and stay behind. The status tiles read whichever snapshot arrived. An OCPP session counts a meter in Wh and names a connector state; a Modbus snapshot counts the session's own energy and names the charger's. Both land in the same two tiles. When nothing answers, the server has already tried the address and says what it found, so the card shows that rather than a hint written in advance. The hint naming Own and Proxy CSMS as the way to control a charger was true until there was a third mode; it now names all three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f7472bada3
commit
7ccd785742
5 files changed
+199
-17
No files matched your search
@@ -66,7 +66,25 @@ const limitAmps = ref(16);
|
||||
|
||||
const ctlActive = computed(() => ctlMode.value !== "off");
|
||||
const ctlConnected = computed(() => !!ctl.value?.connected);
|
||||
const ctlMeterKwh = computed(() => ((ctl.value?.status?.meterWh || 0) / 1000).toFixed(2));
|
||||
|
||||
// Modbus is the local path: we dial the charger rather than wait for it to dial
|
||||
// us, so what the card asks for is an address rather than a token, and what it
|
||||
// can offer differs — boost is a Modbus command, reset and clear-limit are OCPP
|
||||
// ones the register map has no equivalent for.
|
||||
const ctlIsModbus = computed(() => ctlMode.value === "modbus");
|
||||
|
||||
// The two transports report a charging session in different words: an OCPP
|
||||
// session snapshot counts a meter in Wh, a Modbus snapshot counts the session's
|
||||
// own energy. Both land in the same tile.
|
||||
const ctlMeterKwh = computed(() => {
|
||||
const s = ctl.value?.status || {};
|
||||
const wh = ctlIsModbus.value ? s.sessionWh : s.meterWh;
|
||||
return ((wh || 0) / 1000).toFixed(2);
|
||||
});
|
||||
const ctlStatusLabel = computed(() => {
|
||||
const s = ctl.value?.status || {};
|
||||
return (ctlIsModbus.value ? s.statusDesc : s.connectorStatus) || "—";
|
||||
});
|
||||
|
||||
async function loadCtlMode() {
|
||||
try {
|
||||
@@ -305,12 +323,63 @@ async function refreshCtl() {
|
||||
ctlError.value = "";
|
||||
try {
|
||||
ctl.value = await api.getAnkerControl(sn);
|
||||
// The saved address belongs to the charger, not to the form: switching
|
||||
// chargers has to bring its own address along rather than leave the previous
|
||||
// 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;
|
||||
} catch (e) {
|
||||
ctlError.value = e.message;
|
||||
ctl.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- The charger's address on the local network (Modbus mode) ---
|
||||
// Enabled on the device in the Anker app under Settings > Integrations > Modbus
|
||||
// TCP, which is where the address to type here comes from.
|
||||
const modbusHost = ref("");
|
||||
const modbusPort = ref(502);
|
||||
const savingAddress = ref(false);
|
||||
|
||||
// Saving is only worth offering when there is something to save that is not
|
||||
// already saved, so the button goes quiet once the form matches the server.
|
||||
const addressChanged = computed(
|
||||
() =>
|
||||
modbusHost.value.trim() !== (ctl.value?.modbusHost || "") ||
|
||||
Number(modbusPort.value || 502) !== (ctl.value?.modbusPort || 502)
|
||||
);
|
||||
|
||||
async function saveModbusAddress() {
|
||||
const sn = ctlSerial.value.trim();
|
||||
if (!sn || !modbusHost.value.trim()) return;
|
||||
savingAddress.value = true;
|
||||
ctlError.value = "";
|
||||
try {
|
||||
await api.ankerControlAddress(sn, modbusHost.value.trim(), Number(modbusPort.value) || 502);
|
||||
await refreshCtl();
|
||||
} catch (e) {
|
||||
ctlError.value = e.message;
|
||||
} finally {
|
||||
savingAddress.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function forgetModbusAddress() {
|
||||
const sn = ctlSerial.value.trim();
|
||||
if (!sn) return;
|
||||
if (!(await askConfirm(t("charging.control.forgetAddressConfirm")))) return;
|
||||
savingAddress.value = true;
|
||||
ctlError.value = "";
|
||||
try {
|
||||
await api.ankerControlForgetAddress(sn);
|
||||
await refreshCtl();
|
||||
} catch (e) {
|
||||
ctlError.value = e.message;
|
||||
} finally {
|
||||
savingAddress.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doAction(action, body) {
|
||||
const sn = ctlSerial.value.trim();
|
||||
if (!sn) return;
|
||||
@@ -534,10 +603,53 @@ onMounted(async () => {
|
||||
<button class="dh-btn dh-btn-ghost shrink-0" @click="refreshCtl">{{ t("charging.control.refresh") }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Modbus mode dials the charger, so it needs the charger's address on
|
||||
this network rather than a token installed into the charger. -->
|
||||
<div v-if="ctlIsModbus" class="mt-3 rounded-control bg-sunken p-3">
|
||||
<label class="dh-label" for="modbus-host">{{ t("charging.control.address") }}</label>
|
||||
<div class="mt-1 flex gap-2">
|
||||
<input
|
||||
id="modbus-host"
|
||||
v-model="modbusHost"
|
||||
class="dh-input grow"
|
||||
:placeholder="t('charging.control.addressPlaceholder')"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@keyup.enter="saveModbusAddress"
|
||||
/>
|
||||
<input
|
||||
v-model.number="modbusPort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
class="dh-input w-24 shrink-0"
|
||||
:aria-label="t('charging.control.addressPort')"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1.5 text-[11px] text-muted">{{ t("charging.control.addressHint") }}</p>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<button
|
||||
class="dh-btn dh-btn-primary grow"
|
||||
:disabled="!modbusHost.trim() || !addressChanged || savingAddress"
|
||||
@click="saveModbusAddress"
|
||||
>
|
||||
{{ t("charging.control.saveAddress") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="ctl?.modbusHost"
|
||||
class="dh-btn dh-btn-ghost shrink-0"
|
||||
:disabled="savingAddress"
|
||||
@click="forgetModbusAddress"
|
||||
>
|
||||
{{ t("charging.control.forgetAddress") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="ctlConnected">
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
<div class="rounded-control bg-sunken px-3 py-2">
|
||||
<div class="data text-sm font-semibold text-strong">{{ ctl.status?.connectorStatus || "—" }}</div>
|
||||
<div class="data text-sm font-semibold text-strong">{{ ctlStatusLabel }}</div>
|
||||
<div class="text-[11px] text-muted">{{ t("charging.control.status") }}</div>
|
||||
</div>
|
||||
<div class="rounded-control bg-sunken px-3 py-2">
|
||||
@@ -564,17 +676,41 @@ onMounted(async () => {
|
||||
<button class="dh-btn dh-btn-ghost grow" :disabled="ctlBusy === 'limit'" @click="doAction('limit', { amps: limitAmps })">
|
||||
{{ t("charging.control.applyLimit") }}
|
||||
</button>
|
||||
<button class="dh-btn dh-btn-ghost grow" :disabled="ctlBusy === 'clear-limit'" @click="doAction('clear-limit')">
|
||||
<!-- Clearing a limit is an OCPP command; the register map takes an
|
||||
explicit ceiling, so in Modbus mode there is nothing to clear to. -->
|
||||
<button
|
||||
v-if="!ctlIsModbus"
|
||||
class="dh-btn dh-btn-ghost grow"
|
||||
:disabled="ctlBusy === 'clear-limit'"
|
||||
@click="doAction('clear-limit')"
|
||||
>
|
||||
{{ t("charging.control.clearLimit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-if="!resetPrompt" class="dh-btn dh-btn-ghost mt-3 w-full" :disabled="ctlBusy === 'reset'" @click="askReset">
|
||||
<!-- Boost is a Modbus command, and lasts for the current session only. -->
|
||||
<button
|
||||
v-if="ctlIsModbus"
|
||||
class="dh-btn dh-btn-ghost mt-3 w-full"
|
||||
:disabled="ctlBusy === 'boost'"
|
||||
@click="doAction('boost', { on: true })"
|
||||
>
|
||||
{{ t("charging.control.boost") }}
|
||||
</button>
|
||||
|
||||
<!-- Reset reboots the charger over OCPP; the register map has no
|
||||
equivalent, so the button is not offered on the local path. -->
|
||||
<button
|
||||
v-if="!ctlIsModbus && !resetPrompt"
|
||||
class="dh-btn dh-btn-ghost mt-3 w-full"
|
||||
:disabled="ctlBusy === 'reset'"
|
||||
@click="askReset"
|
||||
>
|
||||
{{ t("charging.control.reset") }}
|
||||
</button>
|
||||
<!-- Step-up: destructive reset requires re-entering the password. -->
|
||||
<div v-else class="mt-3 rounded-control border border-danger/40 bg-danger-soft p-3">
|
||||
<div v-else-if="resetPrompt" class="mt-3 rounded-control border border-danger/40 bg-danger-soft p-3">
|
||||
<p class="text-xs font-medium text-danger">{{ t("charging.control.resetConfirm") }}</p>
|
||||
<input
|
||||
v-model="resetPassword"
|
||||
@@ -592,7 +728,11 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="mt-3 text-xs text-muted">{{ t("charging.control.connectHint") }}</p>
|
||||
<!-- Not connected. In Modbus mode the server has already tried to reach
|
||||
the charger and says what it found, which beats a generic hint. -->
|
||||
<p v-else class="mt-3 text-xs text-muted">
|
||||
{{ ctl?.detail || t(ctlIsModbus ? "charging.control.modbusHint" : "charging.control.connectHint") }}
|
||||
</p>
|
||||
<p v-if="ctlError" class="mt-2 text-sm text-danger">{{ ctlError }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user