From 7ccd7857421e74cc2423af3fabb26bef3b877f4c Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:27:06 +0200 Subject: [PATCH] The charger's address, asked for where control is set up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Web App/web/src/api.js | 25 ++++- Web App/web/src/i18n/da.json | 13 ++- Web App/web/src/i18n/en.json | 13 ++- Web App/web/src/i18n/pl.json | 13 ++- Web App/web/src/views/Charging.vue | 152 +++++++++++++++++++++++++++-- 5 files changed, 199 insertions(+), 17 deletions(-) diff --git a/Web App/web/src/api.js b/Web App/web/src/api.js index c347e37..c8a570d 100644 --- a/Web App/web/src/api.js +++ b/Web App/web/src/api.js @@ -329,16 +329,31 @@ export const api = { // the reason rather than an error. listAnkerChargers: () => request("/integrations/anker-solix/chargers"), - // Anker Solix OCPP control (per charger). getAnkerControl returns the control - // mode, connection status, provisioning endpoint + token, and a live status - // snapshot; ankerControlToken (re)generates the per-charger token the operator - // installs into the charger; ankerControlAction issues one OCPP command - // (start/stop/limit/clear-limit/availability/reset/unlock/trigger/config). + // Anker Solix control (per charger), over whichever transport the user's + // control mode selects. getAnkerControl returns the control mode, connection + // status, and a live status snapshot — an OCPP session snapshot in own/proxy + // mode, a Modbus register snapshot in modbus mode. + // + // The two modes are provisioned differently, and each has its own pair here: + // OCPP needs a token the operator installs into the charger (ankerControlToken + // / ankerControlRevoke), Modbus needs the charger's address on the local + // network (ankerControlAddress / ankerControlForgetAddress). A charger may + // hold both; setting one leaves the other alone. getAnkerControl: (sn) => request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/control`), ankerControlToken: (sn) => request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/control/token`, { method: "POST" }), ankerControlRevoke: (sn) => request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/control/token`, { method: "DELETE" }), + ankerControlAddress: (sn, host, port) => + request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/control/address`, { + method: "PUT", + body: JSON.stringify({ host, port }), + }), + ankerControlForgetAddress: (sn) => + request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/control/address`, { method: "DELETE" }), + // One control command. Over OCPP: start, stop, limit, clear-limit, + // availability, reset, unlock, trigger, config. Over Modbus TCP: start, stop, + // limit, boost, phase, timeout, status. ankerControlAction: (sn, action, body = {}) => request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/${action}`, { method: "POST", diff --git a/Web App/web/src/i18n/da.json b/Web App/web/src/i18n/da.json index 44b16a8..973f175 100644 --- a/Web App/web/src/i18n/da.json +++ b/Web App/web/src/i18n/da.json @@ -66,7 +66,7 @@ "stations": { "heading": "I nærheden", "homeHeading": "Dine ladere", - "noControlHint": "Aktivér Own eller Proxy CSMS under Indstillinger → Integrationer for at styre din hjemmelader her.", + "noControlHint": "Vælg en styringstilstand under Indstillinger → Integrationer for at styre din hjemmelader her: Modbus TCP på det lokale netværk eller Own/Proxy CSMS via OCPP.", "count": { "one": "{n} station", "other": "{n} stationer" @@ -93,7 +93,16 @@ "reset": "Genstart laderen", "resetConfirm": "Genstart denne lader nu? En igangværende opladning bliver afbrudt. Indtast din adgangskode igen for at bekræfte.", "resetPassword": "Din kontoadgangskode", - "connectHint": "Indtast laderens serienummer og opdater. Laderen skal være forbundet til DriverVaults OCPP-backend (opsættes under Indstillinger → Integrationer)." + "connectHint": "Indtast laderens serienummer og opdater. Laderen skal være forbundet til DriverVaults OCPP-backend (opsættes under Indstillinger → Integrationer).", + "address": "Laderens adresse på dette netværk", + "addressPlaceholder": "IP-adresse (f.eks. 192.168.1.40)", + "addressPort": "Port", + "addressHint": "Slå Modbus TCP til i Anker-appen under Indstillinger → Integrationer; appen viser derefter denne adresse.", + "saveAddress": "Gem adresse", + "forgetAddress": "Glem", + "forgetAddressConfirm": "Glem laderens lokale adresse? Styring over det lokale netværk stopper, indtil du indtaster den igen.", + "boost": "Boost denne session", + "modbusHint": "Indtast laderens adresse på dette netværk og gem den. Laderen skal være tændt og på samme netværk som DriverVault." }, "info": { "title": "Laderoplysninger", diff --git a/Web App/web/src/i18n/en.json b/Web App/web/src/i18n/en.json index 495976a..439a9a1 100644 --- a/Web App/web/src/i18n/en.json +++ b/Web App/web/src/i18n/en.json @@ -79,7 +79,16 @@ "reset": "Reset charger", "resetConfirm": "Reboot this charger now? Any active charging session will be interrupted. Re-enter your password to confirm.", "resetPassword": "Your account password", - "connectHint": "Enter your charger's serial and refresh. The charger must be connected to DriverVault's OCPP backend (set up in Settings → Integrations)." + "connectHint": "Enter your charger's serial and refresh. The charger must be connected to DriverVault's OCPP backend (set up in Settings → Integrations).", + "address": "Charger address on this network", + "addressPlaceholder": "IP address (e.g. 192.168.1.40)", + "addressPort": "Port", + "addressHint": "Enable Modbus TCP in the Anker app under Settings → Integrations; the app then shows this address.", + "saveAddress": "Save address", + "forgetAddress": "Forget", + "forgetAddressConfirm": "Forget this charger's local address? Control over the local network stops until you enter it again.", + "boost": "Boost this session", + "modbusHint": "Enter the charger's address on this network and save it. The charger must be powered on and on the same network as DriverVault." }, "info": { "title": "Charger information", @@ -110,7 +119,7 @@ "stations": { "heading": "Nearby", "homeHeading": "Your chargers", - "noControlHint": "Turn on Own or Proxy CSMS in Settings → Integrations to control your home charger here.", + "noControlHint": "Pick a control mode in Settings → Integrations to control your home charger here: Modbus TCP over the local network, or Own/Proxy CSMS over OCPP.", "count": { "one": "{n} station", "other": "{n} stations" diff --git a/Web App/web/src/i18n/pl.json b/Web App/web/src/i18n/pl.json index c05d6c0..2bc73c1 100644 --- a/Web App/web/src/i18n/pl.json +++ b/Web App/web/src/i18n/pl.json @@ -66,7 +66,7 @@ "stations": { "heading": "W pobliżu", "homeHeading": "Twoje ładowarki", - "noControlHint": "Włącz tryb Own lub Proxy CSMS w Ustawienia → Integracje, aby sterować domową ładowarką.", + "noControlHint": "Wybierz tryb sterowania w Ustawienia → Integracje, aby sterować domową ładowarką: Modbus TCP w sieci lokalnej albo Own/Proxy CSMS przez OCPP.", "count": { "one": "{n} stacja", "few": "{n} stacje", @@ -95,7 +95,16 @@ "reset": "Zrestartuj ładowarkę", "resetConfirm": "Zrestartować teraz tę ładowarkę? Trwająca sesja ładowania zostanie przerwana. Wpisz ponownie hasło, aby potwierdzić.", "resetPassword": "Hasło do Twojego konta", - "connectHint": "Wpisz numer seryjny ładowarki i odśwież. Ładowarka musi być połączona z backendem OCPP DriverVault (konfiguracja w Ustawienia → Integracje)." + "connectHint": "Wpisz numer seryjny ładowarki i odśwież. Ładowarka musi być połączona z backendem OCPP DriverVault (konfiguracja w Ustawienia → Integracje).", + "address": "Adres ładowarki w tej sieci", + "addressPlaceholder": "Adres IP (np. 192.168.1.40)", + "addressPort": "Port", + "addressHint": "Włącz Modbus TCP w aplikacji Anker w Ustawienia → Integracje; aplikacja pokaże wtedy ten adres.", + "saveAddress": "Zapisz adres", + "forgetAddress": "Zapomnij", + "forgetAddressConfirm": "Zapomnieć lokalny adres tej ładowarki? Sterowanie przez sieć lokalną przestanie działać, dopóki nie wpiszesz go ponownie.", + "boost": "Boost w tej sesji", + "modbusHint": "Podaj adres ładowarki w tej sieci i zapisz go. Ładowarka musi być włączona i w tej samej sieci co DriverVault." }, "info": { "title": "Informacje o ładowarce", diff --git a/Web App/web/src/views/Charging.vue b/Web App/web/src/views/Charging.vue index bdc4689..2d83246 100644 --- a/Web App/web/src/views/Charging.vue +++ b/Web App/web/src/views/Charging.vue @@ -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 () => { + +
+ +
+ + +
+

{{ t("charging.control.addressHint") }}

+
+ + +
+
+ -

{{ t("charging.control.connectHint") }}

+ +

+ {{ ctl?.detail || t(ctlIsModbus ? "charging.control.modbusHint" : "charging.control.connectHint") }} +

{{ ctlError }}