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.resetConfirm") }}
{{{ t("charging.control.connectHint") }}
+ ++ {{ ctl?.detail || t(ctlIsModbus ? "charging.control.modbusHint" : "charging.control.connectHint") }} +
{{ ctlError }}