OCPP UI: improve setup flow (#24453)
Co-authored-by: andig <andi@evcc.io>
This commit is contained in:
parent
b4a0d2d0a0
commit
117963dcb5
50 changed files with 1319 additions and 103 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/charger/ocpp"
|
||||
"github.com/evcc-io/evcc/hems/shm"
|
||||
"github.com/evcc-io/evcc/plugin/mqtt"
|
||||
"github.com/evcc-io/evcc/server/eebus"
|
||||
|
|
@ -19,6 +20,7 @@ import (
|
|||
|
||||
type All struct {
|
||||
Network Network
|
||||
Ocpp ocpp.Config
|
||||
Log string
|
||||
SponsorToken string
|
||||
Plant string // telemetry plant id
|
||||
|
|
|
|||
|
|
@ -16,20 +16,67 @@
|
|||
:apply-custom-defaults="applyCustomDefaults"
|
||||
:custom-fields="customFields"
|
||||
:get-product-name="getProductName"
|
||||
:hide-template-fields="hideTemplateFields"
|
||||
@added="$emit('added', $event)"
|
||||
@updated="$emit('updated')"
|
||||
@removed="$emit('removed')"
|
||||
@close="$emit('close')"
|
||||
@reset="reset"
|
||||
>
|
||||
<template #after-template-info>
|
||||
<template v-if="isOcpp" #template-description>
|
||||
<p>{{ $t("config.charger.ocppDescription") }}</p>
|
||||
<ol class="mb-4">
|
||||
<li>{{ $t("config.charger.ocppStep1") }}</li>
|
||||
<li>{{ $t("config.charger.ocppStep2") }}</li>
|
||||
<li>{{ $t("config.charger.ocppStep3") }}</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<template v-if="isOcpp" #after-template-info="{ values }">
|
||||
<FormRow
|
||||
v-if="ocppUrl"
|
||||
id="chargerOcppUrl"
|
||||
:label="$t('config.charger.ocppLabel')"
|
||||
:help="$t('config.charger.ocppHelp')"
|
||||
:help="$t('config.charger.ocppHelp', { url: ocppUrlWithStationId })"
|
||||
>
|
||||
<input type="text" class="form-control border" :value="ocppUrl" readonly />
|
||||
<input
|
||||
id="chargerOcppUrl"
|
||||
type="text"
|
||||
class="form-control border"
|
||||
:value="ocppUrl"
|
||||
readonly
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<div
|
||||
v-if="!ocppNextStepConfirmed && !values.stationid"
|
||||
class="my-4 d-flex justify-content-end gap-2 align-items-center"
|
||||
>
|
||||
<span v-if="ocppStationIdDetected">{{ $t("config.charger.ocppConnected") }}</span>
|
||||
<button
|
||||
v-if="ocppStationIdDetected"
|
||||
type="button"
|
||||
class="btn btn-primary text-nowrap ms-2"
|
||||
@click="
|
||||
values.stationid = ocppStationIdDetected;
|
||||
ocppNextStepConfirmed = true;
|
||||
"
|
||||
>
|
||||
{{ $t("config.charger.ocppNextStep") }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-outline-primary text-nowrap"
|
||||
@click="confirmOcppNextStep"
|
||||
>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.charger.ocppWaiting") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</DeviceModalBase>
|
||||
</template>
|
||||
|
|
@ -56,7 +103,8 @@ import switchsocketHeaterYaml from "./defaultYaml/switchsocketHeater.yaml?raw";
|
|||
import switchsocketChargerYaml from "./defaultYaml/switchsocketCharger.yaml?raw";
|
||||
import sgreadyYaml from "./defaultYaml/sgready.yaml?raw";
|
||||
import sgreadyRelayYaml from "./defaultYaml/sgreadyRelay.yaml?raw";
|
||||
import { LOADPOINT_TYPE, type LoadpointType } from "@/types/evcc";
|
||||
import { LOADPOINT_TYPE, type LoadpointType, type Ocpp } from "@/types/evcc";
|
||||
import { getOcppUrl, getOcppUrlWithStationId } from "@/utils/ocpp";
|
||||
|
||||
const initialValues = {
|
||||
type: ConfigType.Template,
|
||||
|
|
@ -78,6 +126,10 @@ export default defineComponent({
|
|||
id: Number,
|
||||
loadpointType: { type: String as PropType<LoadpointType>, default: null },
|
||||
fade: String as PropType<ModalFade>,
|
||||
ocpp: {
|
||||
type: Object as PropType<Ocpp>,
|
||||
default: () => ({ config: { port: 0 }, status: { stations: [] } }),
|
||||
},
|
||||
isSponsor: Boolean,
|
||||
},
|
||||
emits: ["added", "updated", "removed", "close"],
|
||||
|
|
@ -85,6 +137,7 @@ export default defineComponent({
|
|||
return {
|
||||
initialValues,
|
||||
customFields: CUSTOM_FIELDS,
|
||||
ocppNextStepConfirmed: false,
|
||||
currentTemplate: null as Template | null,
|
||||
currentValues: {} as DeviceValues,
|
||||
};
|
||||
|
|
@ -102,14 +155,28 @@ export default defineComponent({
|
|||
isHeating(): boolean {
|
||||
return this.loadpointType === LOADPOINT_TYPE.HEATING;
|
||||
},
|
||||
ocppUrl(): string | null {
|
||||
const isOcpp =
|
||||
isOcpp(): boolean {
|
||||
return (
|
||||
this.currentTemplate !== null &&
|
||||
this.currentTemplate?.Params.some((p: TemplateParam) => p.Name === "connector") &&
|
||||
this.currentTemplate?.Params.some((p: TemplateParam) => p.Name === "stationid");
|
||||
if (isOcpp && this.currentValues) {
|
||||
return `ws://${window.location.hostname}:8887/${this.currentValues.stationid || ""}`;
|
||||
this.currentTemplate?.Params.some((p: TemplateParam) => p.Name === "stationid")
|
||||
);
|
||||
},
|
||||
ocppUrl(): string | null {
|
||||
return this.isOcpp ? getOcppUrl(this.ocpp) : null;
|
||||
},
|
||||
ocppUrlWithStationId(): string | null {
|
||||
return this.isOcpp ? getOcppUrlWithStationId(this.ocpp) : null;
|
||||
},
|
||||
ocppStationIdDetected(): string | undefined {
|
||||
if (!this.isOcpp) {
|
||||
return undefined;
|
||||
}
|
||||
return null;
|
||||
const stations = this.ocpp.status.stations;
|
||||
return stations.find((station) => station.status === "unknown")?.id;
|
||||
},
|
||||
hideTemplateFields(): boolean {
|
||||
return this.isOcpp && !this.ocppNextStepConfirmed;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -244,6 +311,16 @@ export default defineComponent({
|
|||
// For template types, use the standard logic (deviceProduct or templateName)
|
||||
return values.deviceProduct || templateName || "";
|
||||
},
|
||||
confirmOcppNextStep() {
|
||||
if (window.confirm(this.$t("config.charger.ocppConfirmContinue"))) {
|
||||
this.ocppNextStepConfirmed = true;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.ocppNextStepConfirmed = false;
|
||||
this.currentTemplate = null;
|
||||
this.currentValues = {} as DeviceValues;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@
|
|||
<div v-else>
|
||||
<p v-if="loadingTemplate">{{ $t("config.general.templateLoading") }}</p>
|
||||
<SponsorTokenRequired v-if="sponsorTokenRequired" />
|
||||
<Markdown v-if="description" :markdown="description" class="my-4" />
|
||||
<slot name="template-description">
|
||||
<Markdown v-if="description" :markdown="description" class="my-4" />
|
||||
</slot>
|
||||
|
||||
<div v-if="authRequired">
|
||||
<PropertyEntry
|
||||
|
|
@ -99,47 +101,49 @@
|
|||
<div v-else>
|
||||
<slot name="after-template-info" :values="values"></slot>
|
||||
|
||||
<Modbus
|
||||
v-if="modbus"
|
||||
v-model:modbus="values['modbus']"
|
||||
v-model:id="values['id']"
|
||||
v-model:host="values['host']"
|
||||
v-model:port="values['port']"
|
||||
v-model:device="values['device']"
|
||||
v-model:baudrate="values['baudrate']"
|
||||
v-model:comset="values['comset']"
|
||||
component-id="device"
|
||||
:defaultId="modbus.ID ? Number(modbus.ID) : undefined"
|
||||
:defaultComset="modbus.Comset"
|
||||
:defaultBaudrate="modbus.Baudrate"
|
||||
:defaultPort="modbus.Port"
|
||||
:capabilities="modbusCapabilities"
|
||||
/>
|
||||
<div v-if="!hideTemplateFields">
|
||||
<Modbus
|
||||
v-if="modbus"
|
||||
v-model:modbus="values['modbus']"
|
||||
v-model:id="values['id']"
|
||||
v-model:host="values['host']"
|
||||
v-model:port="values['port']"
|
||||
v-model:device="values['device']"
|
||||
v-model:baudrate="values['baudrate']"
|
||||
v-model:comset="values['comset']"
|
||||
component-id="device"
|
||||
:defaultId="modbus.ID ? Number(modbus.ID) : undefined"
|
||||
:defaultComset="modbus.Comset"
|
||||
:defaultBaudrate="modbus.Baudrate"
|
||||
:defaultPort="modbus.Port"
|
||||
:capabilities="modbusCapabilities"
|
||||
/>
|
||||
|
||||
<PropertyEntry
|
||||
v-for="param in normalParams"
|
||||
:id="`${deviceType}Param${param.Name}`"
|
||||
:key="param.Name"
|
||||
v-bind="param"
|
||||
v-model="values[param.Name]"
|
||||
:service-values="serviceValues[param.Name]"
|
||||
/>
|
||||
<PropertyEntry
|
||||
v-for="param in normalParams"
|
||||
:id="`${deviceType}Param${param.Name}`"
|
||||
:key="param.Name"
|
||||
v-bind="param"
|
||||
v-model="values[param.Name]"
|
||||
:service-values="serviceValues[param.Name]"
|
||||
/>
|
||||
|
||||
<PropertyCollapsible>
|
||||
<template v-if="advancedParams.length" #advanced>
|
||||
<PropertyEntry
|
||||
v-for="param in advancedParams"
|
||||
:id="`${deviceType}Param${param.Name}`"
|
||||
:key="param.Name"
|
||||
v-bind="param"
|
||||
v-model="values[param.Name]"
|
||||
:service-values="serviceValues[param.Name]"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="$slots['collapsible-more']" #more>
|
||||
<slot name="collapsible-more" :values="values"></slot>
|
||||
</template>
|
||||
</PropertyCollapsible>
|
||||
<PropertyCollapsible>
|
||||
<template v-if="advancedParams.length" #advanced>
|
||||
<PropertyEntry
|
||||
v-for="param in advancedParams"
|
||||
:id="`${deviceType}Param${param.Name}`"
|
||||
:key="param.Name"
|
||||
v-bind="param"
|
||||
v-model="values[param.Name]"
|
||||
:service-values="serviceValues[param.Name]"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="$slots['collapsible-more']" #more>
|
||||
<slot name="collapsible-more" :values="values"></slot>
|
||||
</template>
|
||||
</PropertyCollapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -252,8 +256,18 @@ export default defineComponent({
|
|||
onConfigurationLoaded: Function as PropType<(values: DeviceValues) => void>,
|
||||
// Optional: external template selection control (for parent to reset template)
|
||||
externalTemplate: String as PropType<string | null>,
|
||||
// Optional: hide template fields, e.g. because ocpp step was not completed
|
||||
hideTemplateFields: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["added", "updated", "removed", "close", "template-changed", "update:externalTemplate"],
|
||||
emits: [
|
||||
"added",
|
||||
"updated",
|
||||
"removed",
|
||||
"close",
|
||||
"template-changed",
|
||||
"update:externalTemplate",
|
||||
"reset",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
|
|
@ -367,7 +381,19 @@ export default defineComponent({
|
|||
return !this.isNew;
|
||||
},
|
||||
showActions() {
|
||||
return (this.templateName && !this.authRequired) || this.showYamlInput;
|
||||
// explicitly hide template fields (ocpp step 1)
|
||||
if (this.hideTemplateFields) {
|
||||
return false;
|
||||
}
|
||||
// yaml input type
|
||||
if (this.showYamlInput) {
|
||||
return true;
|
||||
}
|
||||
// template selected and no auth prerequisit
|
||||
if (this.templateName && !this.authRequired) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showYamlInput() {
|
||||
return this.isYamlInputTypeByValue(this.values.type);
|
||||
|
|
@ -499,6 +525,7 @@ export default defineComponent({
|
|||
this.values = { ...this.initialValues } as DeviceValues;
|
||||
this.test = initialTestState();
|
||||
this.resetAuthStatus();
|
||||
this.$emit("reset");
|
||||
},
|
||||
async loadConfiguration() {
|
||||
try {
|
||||
|
|
|
|||
143
assets/js/components/Config/OcppModal.vue
Normal file
143
assets/js/components/Config/OcppModal.vue
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<GenericModal id="ocppModal" :title="$t('config.ocpp.title')" data-testid="ocpp-modal">
|
||||
<div class="container">
|
||||
<!-- OCPP URL -->
|
||||
<div class="mb-3">
|
||||
<Markdown :markdown="$t('config.ocpp.urlHelp', { url: ocppUrlWithStationId })" />
|
||||
</div>
|
||||
<FormRow id="ocppModalServerUrl" :label="$t('config.ocpp.url')">
|
||||
<input
|
||||
id="ocppModalServerUrl"
|
||||
type="text"
|
||||
class="form-control border"
|
||||
:value="ocppUrl"
|
||||
readonly
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<!-- No chargers message -->
|
||||
<div
|
||||
v-if="connectedStations.length === 0 && detectedStations.length === 0"
|
||||
class="text-muted"
|
||||
>
|
||||
{{ $t("config.ocpp.noChargers") }}
|
||||
</div>
|
||||
|
||||
<!-- Connection status -->
|
||||
<div v-if="connectedStations.length > 0">
|
||||
<h6 class="mb-3">{{ $t("config.ocpp.connectionStatus") }}</h6>
|
||||
<p class="text-muted small mb-3">{{ $t("config.ocpp.connectionStatusHelp") }}</p>
|
||||
|
||||
<ul class="list-group stations-list mb-4">
|
||||
<li
|
||||
v-for="station in connectedStations"
|
||||
:key="station.id"
|
||||
class="list-group-item d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<code>{{ station.id }}</code>
|
||||
<span
|
||||
class="badge"
|
||||
:class="statusBadgeClass(station.status)"
|
||||
:title="$t(`config.ocpp.status.${station.status}`)"
|
||||
data-bs-toggle="tooltip"
|
||||
>
|
||||
{{ $t(`config.ocpp.status.${station.status}`) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Detected chargers -->
|
||||
<div v-if="detectedStations.length > 0">
|
||||
<h6 class="mb-3">{{ $t("config.ocpp.detectedChargers") }}</h6>
|
||||
<p class="text-muted small mb-3">{{ $t("config.ocpp.detectedHelp") }}</p>
|
||||
|
||||
<ul class="list-group stations-list">
|
||||
<li
|
||||
v-for="station in detectedStations"
|
||||
:key="station.id"
|
||||
class="list-group-item d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<code>{{ station.id }}</code>
|
||||
<span class="badge bg-secondary">
|
||||
{{ $t(`config.ocpp.status.${station.status}`) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import Markdown from "./Markdown.vue";
|
||||
import type { Ocpp, OcppStationStatus } from "@/types/evcc";
|
||||
import { getOcppUrl, getOcppUrlWithStationId } from "@/utils/ocpp";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OcppModal",
|
||||
components: {
|
||||
GenericModal,
|
||||
FormRow,
|
||||
Markdown,
|
||||
},
|
||||
props: {
|
||||
ocpp: {
|
||||
type: Object as PropType<Ocpp>,
|
||||
default: () => ({ config: { port: 0 }, status: { stations: [] } }),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
status() {
|
||||
return this.ocpp.status;
|
||||
},
|
||||
stations() {
|
||||
return this.status.stations;
|
||||
},
|
||||
ocppUrl(): string {
|
||||
return getOcppUrl(this.ocpp);
|
||||
},
|
||||
ocppUrlWithStationId(): string {
|
||||
return getOcppUrlWithStationId(this.ocpp);
|
||||
},
|
||||
connectedStations(): OcppStationStatus[] {
|
||||
return this.stations.filter(
|
||||
(s) => s.status === "connected" || s.status === "configured"
|
||||
);
|
||||
},
|
||||
detectedStations(): OcppStationStatus[] {
|
||||
return this.stations.filter((s) => s.status === "unknown");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
statusBadgeClass(status: string): string {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return "bg-success";
|
||||
case "configured":
|
||||
return "bg-warning";
|
||||
case "unknown":
|
||||
return "bg-secondary";
|
||||
default:
|
||||
return "bg-secondary";
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.list-group-item code {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
18
assets/js/components/MaterialIcon/Ocpp.vue
Normal file
18
assets/js/components/MaterialIcon/Ocpp.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 16v-3H2q-.425 0-.712-.288T1 12t.288-.712T2 11h1V8q0-1.25.875-2.125T6 5h1q0-.425.288-.712T8 4t.713.288T9 5v14q0 .425-.288.713T8 20t-.712-.288T7 19H6q-1.25 0-2.125-.875T3 16m3 1h1V7H6q-.425 0-.712.288T5 8v8q0 .425.288.713T6 17m15-1q0 1.25-.875 2.125T18 19h-1q0 .425-.288.713T16 20t-.712-.288T15 19v-3h-3q-.425 0-.712-.288T11 15t.288-.712T12 14h3v-4h-3q-.425 0-.712-.288T11 9t.288-.712T12 8h3V5q0-.425.288-.712T16 4t.713.288T17 5h1q1.25 0 2.125.875T21 8v3h1q.425 0 .713.288T23 12t-.288.713T22 13h-1zm-4 1h1q.425 0 .713-.288T19 16V8q0-.425-.288-.712T18 7h-1zm0-5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import icon from "@/mixins/icon";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Ocpp",
|
||||
mixins: [icon],
|
||||
});
|
||||
</script>
|
||||
|
|
@ -92,6 +92,32 @@ export interface State {
|
|||
authDisabled?: boolean;
|
||||
config?: string;
|
||||
database?: string;
|
||||
ocpp?: Ocpp;
|
||||
}
|
||||
|
||||
export interface OcppConfig {
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface OcppStatus {
|
||||
externalUrl?: string;
|
||||
stations: OcppStationStatus[];
|
||||
}
|
||||
|
||||
export interface Ocpp {
|
||||
config: OcppConfig;
|
||||
status: OcppStatus;
|
||||
}
|
||||
|
||||
export interface OcppStationStatus {
|
||||
id: string;
|
||||
status: OCPP_STATION_STATUS;
|
||||
}
|
||||
|
||||
export enum OCPP_STATION_STATUS {
|
||||
UNKNOWN = "unknown",
|
||||
CONFIGURED = "configured",
|
||||
CONNECTED = "connected",
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
|
|
|
|||
13
assets/js/utils/ocpp.ts
Normal file
13
assets/js/utils/ocpp.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Ocpp } from "@/types/evcc";
|
||||
|
||||
export function getOcppUrl(ocpp: Ocpp): string {
|
||||
// User specified url, e.g., for reverse proxy setups
|
||||
if (ocpp.status.externalUrl) {
|
||||
return ocpp.status.externalUrl;
|
||||
}
|
||||
return `ws://${window.location.hostname}:${ocpp.config.port}/`;
|
||||
}
|
||||
|
||||
export function getOcppUrlWithStationId(ocpp: Ocpp): string {
|
||||
return `${getOcppUrl(ocpp)}<stationId>`;
|
||||
}
|
||||
|
|
@ -234,6 +234,18 @@
|
|||
<DeviceTags :tags="eebusTags" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
:title="$t('config.ocpp.title')"
|
||||
editable
|
||||
:error="hasClassError('ocpp')"
|
||||
data-testid="ocpp"
|
||||
@edit="openModal('ocppModal')"
|
||||
>
|
||||
<template #icon><OcppIcon /></template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="ocppTags" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
|
||||
<DeviceCard
|
||||
:title="`${$t('config.circuits.title')} 🧪`"
|
||||
|
|
@ -359,6 +371,7 @@
|
|||
:loadpointType="selectedLoadpointType"
|
||||
:fade="loadpointSubModalOpen ? 'right' : undefined"
|
||||
:is-sponsor="isSponsor"
|
||||
:ocpp="ocpp"
|
||||
@added="chargerAdded"
|
||||
@updated="chargerChanged"
|
||||
@removed="chargerRemoved"
|
||||
|
|
@ -380,6 +393,7 @@
|
|||
@changed="yamlChanged"
|
||||
/>
|
||||
<EebusModal @changed="yamlChanged" />
|
||||
<OcppModal :ocpp="ocpp" />
|
||||
<BackupRestoreModal v-bind="backupRestoreProps" />
|
||||
<PasswordModal update-mode />
|
||||
<SponsorModal :error="hasClassError('sponsorship')" @changed="loadDirty" />
|
||||
|
|
@ -404,6 +418,8 @@ import DeviceCard from "../components/Config/DeviceCard.vue";
|
|||
import DeviceTags from "../components/Config/DeviceTags.vue";
|
||||
import EebusIcon from "../components/MaterialIcon/Eebus.vue";
|
||||
import EebusModal from "../components/Config/EebusModal.vue";
|
||||
import OcppIcon from "../components/MaterialIcon/Ocpp.vue";
|
||||
import OcppModal from "../components/Config/OcppModal.vue";
|
||||
import formatter from "../mixins/formatter";
|
||||
import GeneralConfig from "../components/Config/GeneralConfig.vue";
|
||||
import HemsIcon from "../components/MaterialIcon/Hems.vue";
|
||||
|
|
@ -469,6 +485,8 @@ export default defineComponent({
|
|||
DeviceTags,
|
||||
EebusIcon,
|
||||
EebusModal,
|
||||
OcppIcon,
|
||||
OcppModal,
|
||||
ExperimentalBanner,
|
||||
AuthSuccessBanner,
|
||||
GeneralConfig,
|
||||
|
|
@ -665,6 +683,9 @@ export default defineComponent({
|
|||
isSponsor(): boolean {
|
||||
return !!this.sponsor?.status.name;
|
||||
},
|
||||
ocpp() {
|
||||
return store.state?.ocpp;
|
||||
},
|
||||
telemetry() {
|
||||
// @ts-expect-error: telemetry property exists but not in TypeScript definitions
|
||||
return store.state?.telemetry === true;
|
||||
|
|
@ -672,6 +693,28 @@ export default defineComponent({
|
|||
eebusTags() {
|
||||
return { configured: { value: store.state?.eebus || false } };
|
||||
},
|
||||
ocppTags() {
|
||||
const ocpp = store.state?.ocpp;
|
||||
const stations = ocpp?.status?.stations || [];
|
||||
if (stations.length === 0) {
|
||||
return { configured: { value: false } };
|
||||
}
|
||||
|
||||
const connected = stations.filter((s) => s.status === "connected").length;
|
||||
const configured = stations.filter((s) => s.status === "configured").length;
|
||||
const detected = stations.filter((s) => s.status === "unknown").length;
|
||||
const total = connected + configured;
|
||||
|
||||
const tags: Record<string, any> = {
|
||||
connections: { value: `${connected}/${total}` },
|
||||
};
|
||||
|
||||
if (detected > 0) {
|
||||
tags["detected"] = { value: detected };
|
||||
}
|
||||
|
||||
return tags;
|
||||
},
|
||||
modbusproxyTags() {
|
||||
const config = store.state?.modbusproxy || [];
|
||||
if (config.length > 0) {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,62 @@ func newRegistration() *registration {
|
|||
|
||||
type CS struct {
|
||||
ocpp16.CentralSystem
|
||||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
regs map[string]*registration // guarded by mu mutex
|
||||
txnId atomic.Int64
|
||||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
regs map[string]*registration // guarded by mu mutex
|
||||
txnId atomic.Int64
|
||||
publishFunc func()
|
||||
}
|
||||
|
||||
type stationStatus struct {
|
||||
ID string `json:"id"`
|
||||
Status StationStatus `json:"status"`
|
||||
}
|
||||
|
||||
// Status represents the runtime OCPP status
|
||||
type Status struct {
|
||||
ExternalUrl string `json:"externalUrl,omitempty"`
|
||||
Stations []stationStatus `json:"stations"`
|
||||
}
|
||||
|
||||
// status returns the OCPP runtime status
|
||||
func (cs *CS) status() Status {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
stations := []stationStatus{}
|
||||
|
||||
for id, reg := range cs.regs {
|
||||
if id == "" {
|
||||
continue // skip anonymous registrations
|
||||
}
|
||||
|
||||
state := StationStatusUnknown
|
||||
if cp := reg.cp; cp != nil {
|
||||
if cp.Connected() {
|
||||
state = StationStatusConnected
|
||||
} else {
|
||||
state = StationStatusConfigured
|
||||
}
|
||||
}
|
||||
|
||||
stations = append(stations, stationStatus{
|
||||
ID: id,
|
||||
Status: state,
|
||||
})
|
||||
}
|
||||
|
||||
return Status{
|
||||
ExternalUrl: ExternalUrl(),
|
||||
Stations: stations,
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpdated sets a callback function that is called when the status changes
|
||||
func (cs *CS) SetUpdated(f func()) {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
cs.publishFunc = f
|
||||
}
|
||||
|
||||
// errorHandler logs error channel
|
||||
|
|
@ -37,6 +89,13 @@ func (cs *CS) errorHandler(errC <-chan error) {
|
|||
}
|
||||
}
|
||||
|
||||
// publish triggers the publish callback if set
|
||||
func (cs *CS) publish() {
|
||||
if cs.publishFunc != nil {
|
||||
cs.publishFunc()
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *CS) ChargepointByID(id string) (*CP, error) {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
|
@ -76,6 +135,7 @@ func (cs *CS) RegisterChargepoint(id string, newfun func() *CP, init func(*CP) e
|
|||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
cs.publish()
|
||||
|
||||
// serialise on chargepoint id
|
||||
reg.setup.Lock()
|
||||
|
|
@ -112,23 +172,22 @@ func (cs *CS) RegisterChargepoint(id string, newfun func() *CP, init func(*CP) e
|
|||
// NewChargePoint implements ocpp16.ChargePointConnectionHandler
|
||||
func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// check for configured charge point
|
||||
reg, ok := cs.regs[chargePoint.ID()]
|
||||
if ok {
|
||||
cs.log.DEBUG.Printf("charge point connected: %s", chargePoint.ID())
|
||||
|
||||
// trigger initial connection if charge point is already setup
|
||||
// trigger initial connection
|
||||
if cp := reg.cp; cp != nil {
|
||||
cp.connect(true)
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
cs.publish()
|
||||
return
|
||||
}
|
||||
|
||||
cs.log.WARN.Printf("unknown charge point connected: %s", chargePoint.ID())
|
||||
|
||||
// check for configured anonymous charge point
|
||||
reg, ok = cs.regs[""]
|
||||
if ok && reg.cp != nil {
|
||||
|
|
@ -142,12 +201,17 @@ func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) {
|
|||
|
||||
cp.connect(true)
|
||||
|
||||
cs.mu.Unlock()
|
||||
cs.publish()
|
||||
return
|
||||
}
|
||||
|
||||
// register unknown charge point
|
||||
// when charge point setup is complete, it will eventually be associated with the connected id
|
||||
cs.regs[chargePoint.ID()] = newRegistration()
|
||||
cs.log.WARN.Printf("unknown charge point connected: %s", chargePoint.ID())
|
||||
|
||||
cs.mu.Unlock()
|
||||
cs.publish()
|
||||
}
|
||||
|
||||
// ChargePointDisconnected implements ocpp16.ChargePointConnectionHandler
|
||||
|
|
@ -157,4 +221,6 @@ func (cs *CS) ChargePointDisconnected(chargePoint ocpp16.ChargePointConnection)
|
|||
if cp, err := cs.ChargepointByID(chargePoint.ID()); err == nil {
|
||||
cp.connect(false)
|
||||
}
|
||||
|
||||
cs.publish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package ocpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -16,11 +19,49 @@ import (
|
|||
"github.com/lorenzodonini/ocpp-go/ws"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
instance *CS
|
||||
once sync.Once
|
||||
instance *CS
|
||||
port = 8887
|
||||
externalUrl string
|
||||
)
|
||||
|
||||
// GetStatus returns the OCPP runtime status
|
||||
func GetStatus() Status {
|
||||
if instance == nil {
|
||||
return Status{}
|
||||
}
|
||||
return instance.status()
|
||||
}
|
||||
|
||||
// ExternalUrl returns the auto-generated OCPP external URL based on network external URL
|
||||
func ExternalUrl() string {
|
||||
if externalUrl == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, err := url.Parse(externalUrl)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Replace protocol: http -> ws, https -> wss
|
||||
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
|
||||
u.Host = fmt.Sprintf("%s:%d", strings.Split(u.Host, ":")[0], 8887) // deliberately fixed, port configurability only for testing
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Init initializes the OCPP server
|
||||
func Init(cfg Config, networkExternalUrl string) {
|
||||
port = cfg.Port
|
||||
externalUrl = networkExternalUrl
|
||||
}
|
||||
|
||||
func Instance() *CS {
|
||||
once.Do(func() {
|
||||
log := util.NewLogger("ocpp")
|
||||
|
|
@ -55,7 +96,7 @@ func Instance() *CS {
|
|||
cs.SetChargePointDisconnectedHandler(instance.ChargePointDisconnected)
|
||||
|
||||
go instance.errorHandler(cs.Errors())
|
||||
go cs.Start(8887, "/{ws}")
|
||||
go cs.Start(port, "/{ws}")
|
||||
|
||||
// wait for server to start
|
||||
for range time.Tick(10 * time.Millisecond) {
|
||||
|
|
|
|||
27
charger/ocpp/instance_test.go
Normal file
27
charger/ocpp/instance_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package ocpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExternalUrl(t *testing.T) {
|
||||
tests := []struct{ input, expected string }{
|
||||
{"", ""},
|
||||
{"http://example.com:7070", "ws://example.com:8887"},
|
||||
{"https://example.com:443", "wss://example.com:8887"},
|
||||
{"http://example.com", "ws://example.com:8887"},
|
||||
{"https://example.com", "wss://example.com:8887"},
|
||||
{"http://10.20.30.40:7070/path", "ws://10.20.30.40:8887/path"},
|
||||
{"https://example.com/path", "wss://example.com:8887/path"},
|
||||
{"ws://example.com", "ws://example.com:8887"},
|
||||
{"wss://example.com:9000", "wss://example.com:8887"},
|
||||
{"strange://example.com", "strange://example.com:8887"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
externalUrl = tt.input
|
||||
if result := ExternalUrl(); result != tt.expected {
|
||||
t.Errorf("ExternalUrl(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
charger/ocpp/stationstatus.go
Normal file
10
charger/ocpp/stationstatus.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package ocpp
|
||||
|
||||
type StationStatus int
|
||||
|
||||
//go:generate go tool enumer -type StationStatus -trimprefix StationStatus -transform=lower -json
|
||||
const (
|
||||
StationStatusUnknown StationStatus = iota
|
||||
StationStatusConfigured
|
||||
StationStatusConnected
|
||||
)
|
||||
100
charger/ocpp/stationstatus_enumer.go
Normal file
100
charger/ocpp/stationstatus_enumer.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Code generated by "enumer -type StationStatus -trimprefix StationStatus -transform=lower -json"; DO NOT EDIT.
|
||||
|
||||
package ocpp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _StationStatusName = "unknownconfiguredconnected"
|
||||
|
||||
var _StationStatusIndex = [...]uint8{0, 7, 17, 26}
|
||||
|
||||
const _StationStatusLowerName = "unknownconfiguredconnected"
|
||||
|
||||
func (i StationStatus) String() string {
|
||||
if i < 0 || i >= StationStatus(len(_StationStatusIndex)-1) {
|
||||
return fmt.Sprintf("StationStatus(%d)", i)
|
||||
}
|
||||
return _StationStatusName[_StationStatusIndex[i]:_StationStatusIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _StationStatusNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[StationStatusUnknown-(0)]
|
||||
_ = x[StationStatusConfigured-(1)]
|
||||
_ = x[StationStatusConnected-(2)]
|
||||
}
|
||||
|
||||
var _StationStatusValues = []StationStatus{StationStatusUnknown, StationStatusConfigured, StationStatusConnected}
|
||||
|
||||
var _StationStatusNameToValueMap = map[string]StationStatus{
|
||||
_StationStatusName[0:7]: StationStatusUnknown,
|
||||
_StationStatusLowerName[0:7]: StationStatusUnknown,
|
||||
_StationStatusName[7:17]: StationStatusConfigured,
|
||||
_StationStatusLowerName[7:17]: StationStatusConfigured,
|
||||
_StationStatusName[17:26]: StationStatusConnected,
|
||||
_StationStatusLowerName[17:26]: StationStatusConnected,
|
||||
}
|
||||
|
||||
var _StationStatusNames = []string{
|
||||
_StationStatusName[0:7],
|
||||
_StationStatusName[7:17],
|
||||
_StationStatusName[17:26],
|
||||
}
|
||||
|
||||
// StationStatusString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func StationStatusString(s string) (StationStatus, error) {
|
||||
if val, ok := _StationStatusNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _StationStatusNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to StationStatus values", s)
|
||||
}
|
||||
|
||||
// StationStatusValues returns all values of the enum
|
||||
func StationStatusValues() []StationStatus {
|
||||
return _StationStatusValues
|
||||
}
|
||||
|
||||
// StationStatusStrings returns a slice of all String values of the enum
|
||||
func StationStatusStrings() []string {
|
||||
strs := make([]string, len(_StationStatusNames))
|
||||
copy(strs, _StationStatusNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAStationStatus returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i StationStatus) IsAStationStatus() bool {
|
||||
for _, v := range _StationStatusValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for StationStatus
|
||||
func (i StationStatus) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for StationStatus
|
||||
func (i *StationStatus) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("StationStatus should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = StationStatusString(s)
|
||||
return err
|
||||
}
|
||||
30
cmd/root.go
30
cmd/root.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/charger/ocpp"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
||||
|
|
@ -171,6 +172,23 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
valueChan := make(chan util.Param, 64)
|
||||
go tee.Run(valueChan)
|
||||
|
||||
// start OCPP server
|
||||
ocppCS := ocpp.Instance()
|
||||
ocppCS.SetUpdated(func() {
|
||||
// republish when OCPP state updates
|
||||
valueChan <- util.Param{Key: keys.Ocpp, Val: struct {
|
||||
Config ocpp.Config `json:"config"`
|
||||
Status ocpp.Status `json:"status"`
|
||||
}{
|
||||
Config: conf.Ocpp,
|
||||
Status: ocpp.GetStatus(),
|
||||
}}
|
||||
})
|
||||
log.INFO.Printf("OCPP local: ws://127.0.0.1:%d/<stationId>", conf.Ocpp.Port)
|
||||
if ocpp.ExternalUrl() != "" {
|
||||
log.INFO.Printf("OCPP external: %s/<stationId>", ocpp.ExternalUrl())
|
||||
}
|
||||
|
||||
// value cache
|
||||
cache := util.NewParamCache()
|
||||
go cache.Run(pipe.NewDropper(ignoreLogs...).Pipe(tee.Attach()))
|
||||
|
|
@ -186,7 +204,10 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
log.INFO.Printf("UI listening at :%d", conf.Network.Port)
|
||||
log.INFO.Printf("UI local: http://127.0.0.1:%d", conf.Network.Port)
|
||||
if conf.Network.ExternalUrl != "" {
|
||||
log.INFO.Printf("UI external: %s", conf.Network.ExternalURL())
|
||||
}
|
||||
|
||||
// publish to UI
|
||||
go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache)
|
||||
|
|
@ -316,6 +337,13 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy}
|
||||
valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
|
||||
valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
|
||||
valueChan <- util.Param{Key: keys.Ocpp, Val: struct {
|
||||
Config ocpp.Config `json:"config"`
|
||||
Status ocpp.Status `json:"status"`
|
||||
}{
|
||||
Config: conf.Ocpp,
|
||||
Status: ocpp.GetStatus(),
|
||||
}}
|
||||
valueChan <- util.Param{Key: keys.Sponsor, Val: struct {
|
||||
Status sponsor.Status `json:"status"`
|
||||
FromYaml bool `json:"fromYaml"`
|
||||
|
|
|
|||
14
cmd/setup.go
14
cmd/setup.go
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/charger"
|
||||
"github.com/evcc-io/evcc/charger/ocpp"
|
||||
"github.com/evcc-io/evcc/cmd/shutdown"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/core/circuit"
|
||||
|
|
@ -65,6 +66,9 @@ var conf = globalconfig.All{
|
|||
Host: "",
|
||||
Port: 7070,
|
||||
},
|
||||
Ocpp: ocpp.Config{
|
||||
Port: 8887,
|
||||
},
|
||||
Mqtt: globalconfig.Mqtt{
|
||||
Topic: "evcc",
|
||||
},
|
||||
|
|
@ -547,6 +551,11 @@ func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) error {
|
|||
err = wrapErrorWithClass(ClassMqtt, configureMqtt(&conf.Mqtt))
|
||||
}
|
||||
|
||||
// setup OCPP server
|
||||
if err == nil {
|
||||
configureOCPP(&conf.Ocpp, conf.Network.ExternalUrl)
|
||||
}
|
||||
|
||||
// setup EEBus server
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassEEBus, configureEEBus(&conf.EEBus))
|
||||
|
|
@ -758,6 +767,11 @@ func configureMDNS(conf globalconfig.Network) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// setup OCPP
|
||||
func configureOCPP(cfg *ocpp.Config, externalUrl string) {
|
||||
ocpp.Init(*cfg, externalUrl)
|
||||
}
|
||||
|
||||
// setup EEBus
|
||||
func configureEEBus(conf *eebus.Config) error {
|
||||
// migrate settings
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const (
|
|||
Shm = "shm"
|
||||
Messaging = "messaging"
|
||||
ModbusProxy = "modbusproxy"
|
||||
Ocpp = "ocpp"
|
||||
Tariffs = "tariffs"
|
||||
Version = "version"
|
||||
Config = "config"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Зарядни станции",
|
||||
"generic": "Общи интеграции",
|
||||
"heatingdevices": "Загревателни устройства",
|
||||
"ocppHelp": "Копирай този адрес в конфигурацията на твоите зарядни станции.",
|
||||
"ocppHelp": "Копирайте този URL адрес в конфигурацията на вашето зарядно устройство. Проверете ръководството на производителя за подробности. Зарядното устройство автоматично ще добави своя уникален идентификатор (ID на станцията) към URL адреса. В редки случаи може да се наложи ръчно да посочите идентификатора. Пример: `{url}`",
|
||||
"ocppLabel": "Адрес на OCPP сървъра",
|
||||
"switchsockets": "Контакти с прекъсвач",
|
||||
"template": "Производител",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "EV nabíječky",
|
||||
"generic": "Obecné integrace",
|
||||
"heatingdevices": "Zařízení pro vytápění",
|
||||
"ocppHelp": "Zkopíruj tuto adresu do konfiguračního nástroje nabíječky.",
|
||||
"ocppHelp": "Zkopírujte tuto URL adresu do konfigurace vaší nabíječky. Podrobnosti naleznete v manuálu výrobce. Nabíječka automaticky připojí svůj unikátní identifikátor (ID stanice) k URL. Ve vzácných případech může být nutné identifikátor zadat ručně. Příklad: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "Přepínatelné zásuvky",
|
||||
"template": "Výrobce",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "EV opladere",
|
||||
"generic": "Generisk integration",
|
||||
"heatingdevices": "Varmeenheder",
|
||||
"ocppHelp": "Kopier denne adresse til opladerens konfiguration.",
|
||||
"ocppHelp": "Kopier denne URL til konfigurationen af din oplader. Se producentens manual for detaljer. Opladeren vil automatisk tilføje sin unikke identifikator (stations-ID) til URL'en. I sjældne tilfælde skal du muligvis angive identifikatoren manuelt. Eksempel: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "Omskiftelige stikkontakter",
|
||||
"template": "Fabrikant",
|
||||
|
|
|
|||
29
i18n/de.json
29
i18n/de.json
|
|
@ -58,8 +58,16 @@
|
|||
"chargers": "Wallboxen",
|
||||
"generic": "Generische Integrationen",
|
||||
"heatingdevices": "Wärmeerzeuger",
|
||||
"ocppHelp": "Kopiere diese Adresse in die Konfiguration der Wallbox.",
|
||||
"ocppConfirmContinue": "Deine Wallbox hat sich noch nicht mit evcc verbunden. Möchtest du wirklich fortfahren?",
|
||||
"ocppConnected": "Verbunden!",
|
||||
"ocppDescription": "evcc hat einen eingebauten OCPP-Server. Folge diesen Schritten:",
|
||||
"ocppHelp": "Kopiere diese URL in die Konfiguration deiner Wallbox. Details findest du im Handbuch des Herstellers. Die Wallbox sollte automatisch ihre eindeutige Kennung (Station-ID) an die URL anhängen. In seltenen Fällen musst du die Kennung manuell angeben. Beispiel: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"ocppNextStep": "Nächster Schritt",
|
||||
"ocppStep1": "Konfiguriere deine Wallbox, um evcc als OCPP-Server zu verwenden.",
|
||||
"ocppStep2": "Warte darauf, dass deine Wallbox sich mit evcc verbindet.",
|
||||
"ocppStep3": "Fahre fort und schließe die Konfiguration ab.",
|
||||
"ocppWaiting": "Warte auf Verbindung",
|
||||
"switchsockets": "Schaltbare Steckdosen",
|
||||
"template": "Hersteller",
|
||||
"titleAdd": {
|
||||
|
|
@ -109,10 +117,12 @@
|
|||
"chargedEnergy": "Geladen",
|
||||
"co2": "Netz-CO₂",
|
||||
"configured": "Konfiguriert",
|
||||
"connections": "Verbindungen",
|
||||
"controllable": "Steuerbar",
|
||||
"currency": "Währung",
|
||||
"current": "Strom",
|
||||
"currentRange": "Strom",
|
||||
"detected": "Erkannt",
|
||||
"dimmed": "Gedimmt",
|
||||
"enabled": "Ladebereit",
|
||||
"energy": "Energie",
|
||||
|
|
@ -472,6 +482,23 @@
|
|||
"labelPort": "Port",
|
||||
"title": "Netzwerk"
|
||||
},
|
||||
"ocpp": {
|
||||
"connectedChargers": "Verbundene Wallboxen",
|
||||
"connectionStatus": "Konfigurierte Station-IDs",
|
||||
"connectionStatusHelp": "Verbindungsstatus der konfigurierten Wallboxen.",
|
||||
"detectedChargers": "Erkannte Station-IDs",
|
||||
"detectedHelp": "Diese Wallboxen haben versucht, sich mit evcc zu verbinden. Um eine Wallbox zu verwenden, erstelle einen Ladepunkt mit ihrer Station-ID.",
|
||||
"noChargers": "Keine OCPP-Wallboxen erkannt.",
|
||||
"noStations": "Keine Stationen verbunden",
|
||||
"status": {
|
||||
"configured": "Nicht verbunden",
|
||||
"connected": "Verbunden",
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"title": "OCPP Server",
|
||||
"url": "Server-URL",
|
||||
"urlHelp": "Kopiere diese URL in die Konfiguration deiner Wallbox. Details findest du im Handbuch des Herstellers. Die Wallbox sollte automatisch ihre eindeutige Kennung (Station-ID) an die URL anhängen. In seltenen Fällen musst du die Kennung manuell angeben. Beispiel: `{url}`"
|
||||
},
|
||||
"options": {
|
||||
"boolean": {
|
||||
"no": "nein",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Φορτιστές οχημάτων",
|
||||
"generic": "Γενικές ενσωματώσεις",
|
||||
"heatingdevices": "Συσκευές θέρμανσης",
|
||||
"ocppHelp": "Αντιγράψτε αυτή τη διεύθυνση στη διαμόρφωση των φορτιστών σας.",
|
||||
"ocppHelp": "Αντιγράψτε αυτό το URL στη διαμόρφωση του φορτιστή σας. Ελέγξτε το εγχειρίδιο του κατασκευαστή για λεπτομέρειες. Ο φορτιστής θα προσθέσει αυτόματα το μοναδικό αναγνωριστικό του (ID σταθμού) στο URL. Σε σπάνιες περιπτώσεις, ίσως χρειαστεί να καθορίσετε το αναγνωριστικό χειροκίνητα. Παράδειγμα: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "Πρίζες με δυνατότητα εναλλαγής",
|
||||
"template": "Κατασκευαστής",
|
||||
|
|
|
|||
29
i18n/en.json
29
i18n/en.json
|
|
@ -58,8 +58,16 @@
|
|||
"chargers": "EV chargers",
|
||||
"generic": "Generic integrations",
|
||||
"heatingdevices": "Heating devices",
|
||||
"ocppHelp": "Copy this address into your chargers configuration.",
|
||||
"ocppConfirmContinue": "Your charger has not connected to evcc yet. Are you sure you want to continue?",
|
||||
"ocppConnected": "Connected!",
|
||||
"ocppDescription": "evcc has a built-in OCPP server. Follow these steps:",
|
||||
"ocppHelp": "Copy this URL into your charger's configuration. Check the manufacturer's manual for details. The charger is expected to automatically append its unique identifier (station ID) to the url. In rare cases, you may need to manually specify the identifier. Example: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"ocppNextStep": "Next step",
|
||||
"ocppStep1": "Configure your charger to use evcc as OCPP server.",
|
||||
"ocppStep2": "Wait for your charger to connect to evcc.",
|
||||
"ocppStep3": "Proceed and complete the configuration.",
|
||||
"ocppWaiting": "Waiting for connection",
|
||||
"switchsockets": "Switchable sockets",
|
||||
"template": "Manufacturer",
|
||||
"titleAdd": {
|
||||
|
|
@ -109,10 +117,12 @@
|
|||
"chargedEnergy": "Charged",
|
||||
"co2": "Grid CO₂",
|
||||
"configured": "Configured",
|
||||
"connections": "Connections",
|
||||
"controllable": "Controllable",
|
||||
"currency": "Currency",
|
||||
"current": "Current",
|
||||
"currentRange": "Current",
|
||||
"detected": "Detected",
|
||||
"dimmed": "Dimmed",
|
||||
"enabled": "Enabled",
|
||||
"energy": "Energy",
|
||||
|
|
@ -472,6 +482,23 @@
|
|||
"labelPort": "Port",
|
||||
"title": "Network"
|
||||
},
|
||||
"ocpp": {
|
||||
"connectedChargers": "Connected chargers",
|
||||
"connectionStatus": "Configured station IDs",
|
||||
"connectionStatusHelp": "Connection status of configured chargers.",
|
||||
"detectedChargers": "Detected station IDs",
|
||||
"detectedHelp": "These chargers have tried to connect to evcc. To use a charger, create a loadpoint with its station ID.",
|
||||
"noChargers": "No OCPP chargers detected.",
|
||||
"noStations": "No stations connected",
|
||||
"status": {
|
||||
"configured": "Not connected",
|
||||
"connected": "Connected",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"title": "OCPP Server",
|
||||
"url": "Server URL",
|
||||
"urlHelp": "Copy this URL into your charger's configuration. Check the manufacturer's manual for details. The charger is expected to automatically append its unique identifier (station ID) to the url. In rare cases, you may need to manually specify the identifier. Example: `{url}`"
|
||||
},
|
||||
"options": {
|
||||
"boolean": {
|
||||
"no": "no",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"charger": {
|
||||
"generic": "Integraciones genéricas",
|
||||
"heatingdevices": "Dispositivos de calefacción",
|
||||
"ocppHelp": "Copia esta dirección en la configuración de cargadores.",
|
||||
"ocppHelp": "Copia esta URL en la configuración de tu cargador. Consulta el manual del fabricante para más detalles. El cargador añadirá automáticamente su identificador único (ID de estación) a la URL. En casos excepcionales, es posible que debas especificar el identificador manualmente. Ejemplo: `{url}`",
|
||||
"ocppLabel": "URL del servidor OCPP",
|
||||
"switchsockets": "Tomas conmutables",
|
||||
"template": "Fabricante",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Sähköauton latausasemat",
|
||||
"generic": "Yleiset integraatiot",
|
||||
"heatingdevices": "Lämmityslaitteet",
|
||||
"ocppHelp": "Kopioi tämä osoite laturien määrityksiin.",
|
||||
"ocppHelp": "Kopioi tämä URL-osoite laturisi asetuksiin. Katso lisätietoja valmistajan käsikirjasta. Laturi lisää automaattisesti yksilöllisen tunnisteensa (aseman tunniste) URL-osoitteeseen. Harvinaisissa tapauksissa saatat joutua määrittämään tunnisteen manuaalisesti. Esimerkki: `{url}`",
|
||||
"ocppLabel": "OCPP-palvelimen URL",
|
||||
"switchsockets": "Kytkettävät pistorasiat",
|
||||
"template": "Valmistaja",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Chargeurs VE",
|
||||
"generic": "Intégrations génériques",
|
||||
"heatingdevices": "Dispositif de chauffage",
|
||||
"ocppHelp": "Copier cette adresse dans votre configuration de chargeurs.",
|
||||
"ocppHelp": "Copiez cette URL dans la configuration de votre chargeur. Consultez le manuel du fabricant pour plus de détails. Le chargeur ajoutera automatiquement son identifiant unique (ID de station) à l'URL. Dans de rares cas, vous devrez peut-être spécifier l'identifiant manuellement. Exemple : `{url}`",
|
||||
"ocppLabel": "URL du serveur OCPP",
|
||||
"switchsockets": "Prises commutables",
|
||||
"template": "Fabricant",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "EV punjači",
|
||||
"generic": "Opće integracije",
|
||||
"heatingdevices": "Uređaji za grijanje",
|
||||
"ocppHelp": "Unesite ovu adresu u konfiguraciju punjača.",
|
||||
"ocppHelp": "Kopirajte ovaj URL u konfiguraciju svog punjača. Provjerite upute proizvođača za detalje. Punjač će automatski dodati svoj jedinstveni identifikator (ID stanice) na URL. U rijetkim slučajevima možda ćete morati ručno odrediti identifikator. Primjer: `{url}`",
|
||||
"ocppLabel": "URL OCPP poslužitelja",
|
||||
"switchsockets": "Upravljive utičnice",
|
||||
"template": "Proizvođač",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "EV töltők",
|
||||
"generic": "Általános integrációk",
|
||||
"heatingdevices": "Fűtőberendezések",
|
||||
"ocppHelp": "Másolja be ezt a címet a töltő konfigurációjába.",
|
||||
"ocppHelp": "Másolja ezt az URL-t a töltő konfigurációjába. A részletekért tekintse meg a gyártó kézikönyvét. A töltő automatikusan hozzáfűzi egyedi azonosítóját (állomás azonosító) az URL-hez. Ritka esetekben előfordulhat, hogy az azonosítót manuálisan kell megadnia. Példa: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "Kapcsolható dugaljak",
|
||||
"template": "Gyártó",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Caricatori EV",
|
||||
"generic": "Integrazioni generiche",
|
||||
"heatingdevices": "Dispositivi di riscaldamento",
|
||||
"ocppHelp": "Copia questo indirizzo nella configurazione dei tuoi caricatori",
|
||||
"ocppHelp": "Copia questo URL nella configurazione del tuo caricatore. Consulta il manuale del produttore per i dettagli. Il caricatore aggiungerà automaticamente il suo identificatore univoco (ID stazione) all'URL. In rari casi, potrebbe essere necessario specificare manualmente l'identificatore. Esempio: `{url}`",
|
||||
"ocppLabel": "URL Server OCPP",
|
||||
"switchsockets": "Prese commutabili",
|
||||
"template": "Produttore",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Wallboxen",
|
||||
"generic": "Generesch Integratiounen",
|
||||
"heatingdevices": "Heizungsgerät",
|
||||
"ocppHelp": "Kopéier dës Adress an d'Konfiguratioun vun der Wallbox.",
|
||||
"ocppHelp": "Kopéier dës URL an d'Konfiguratioun vun dengem Luedapparat. Kuck am Handbuch vum Hiersteller fir Detailer. De Luedapparat setzt automatesch säin eenzegaartegen Identifikator (Statiounen-ID) un d'URL. A seltenen Fäll muss de vläicht den Identifikator manuell uginn. Beispill: `{url}`",
|
||||
"ocppLabel": "URL vum OCPP-Server",
|
||||
"switchsockets": "Schaltbar Steckdousen",
|
||||
"template": "Hiersteller",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Elektromobilių įkrovikliai",
|
||||
"generic": "Tipinės integracijos",
|
||||
"heatingdevices": "Šildymo prietaisai",
|
||||
"ocppHelp": "Nukopijuokite šį adresą į jūsų įkroviklio nustatymus.",
|
||||
"ocppHelp": "Nukopijuokite šį URL į savo įkroviklio konfigūraciją. Išsamesnės informacijos ieškokite gamintojo vadove. Įkroviklis automatiškai pridės savo unikalų identifikatorių (stoties ID) prie URL. Retais atvejais gali tekti rankiniu būdu nurodyti identifikatorių. Pavyzdys: `{url}`",
|
||||
"ocppLabel": "OCPP-Serverio URL",
|
||||
"switchsockets": "Išmanios rozetės",
|
||||
"template": "Gamintojas",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "EV laders",
|
||||
"generic": "Algemene integraties",
|
||||
"heatingdevices": "Warmtepompen",
|
||||
"ocppHelp": "Kopieer dit adres in de configuratie van jouw lader.",
|
||||
"ocppHelp": "Kopieer deze URL naar de configuratie van je lader. Raadpleeg de handleiding van de fabrikant voor details. De lader voegt automatisch zijn unieke identificatie (station-ID) toe aan de URL. In zeldzame gevallen moet je de identificatie mogelijk handmatig opgeven. Voorbeeld: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "schakelbare stopcontacten",
|
||||
"template": "Fabrikant",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Ładowarki EV",
|
||||
"generic": "Ogólne integracje",
|
||||
"heatingdevices": "Urządzenia grzewcze",
|
||||
"ocppHelp": "Skopiuj ten adres do konfiguracji twojej ładowarki.",
|
||||
"ocppHelp": "Skopiuj ten adres URL do konfiguracji swojej ładowarki. Sprawdź instrukcję producenta, aby uzyskać szczegóły. Ładowarka automatycznie doda swój unikalny identyfikator (ID stacji) do adresu URL. W rzadkich przypadkach może być konieczne ręczne określenie identyfikatora. Przykład: `{url}`",
|
||||
"ocppLabel": "Adres URL serwera OCPP",
|
||||
"switchsockets": "Gniazdka przełączalne",
|
||||
"template": "Producent",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Carregadores de Veículos Eléctricos",
|
||||
"generic": "Integrações genéricas",
|
||||
"heatingdevices": "Equipamentos de aquecimento",
|
||||
"ocppHelp": "Copie este endereço para a configuração do seu carregador.",
|
||||
"ocppHelp": "Copie este URL para a configuração do seu carregador. Consulte o manual do fabricante para obter detalhes. O carregador adicionará automaticamente seu identificador único (ID da estação) ao URL. Em casos raros, pode ser necessário especificar o identificador manualmente. Exemplo: `{url}`",
|
||||
"ocppLabel": "URL do servidor OCPP",
|
||||
"switchsockets": "Tomadas comandadas",
|
||||
"template": "Fabricante",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "polnilnice za EV",
|
||||
"generic": "Splošne integracije",
|
||||
"heatingdevices": "Ogrevalne naprave",
|
||||
"ocppHelp": "Kopirajte ta naslov v konfiguracijo vaše polnilnice.",
|
||||
"ocppHelp": "Kopirajte ta URL v konfiguracijo vašega polnilnika. Za podrobnosti preverite priročnik proizvajalca. Polnilnik bo samodejno dodal svoj edinstveni identifikator (ID postaje) k URL-ju. V redkih primerih boste morda morali identifikator določiti ročno. Primer: `{url}`",
|
||||
"ocppLabel": "URL strežnika OCPP",
|
||||
"switchsockets": "Preklopne vtičnice",
|
||||
"template": "Proizvajalec",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Elbilsladdare",
|
||||
"generic": "Generisk integration",
|
||||
"heatingdevices": "Värmeenheter",
|
||||
"ocppHelp": "Kopiera in denna adress i din laddkonfiguration.",
|
||||
"ocppHelp": "Kopiera denna URL till din laddares konfiguration. Kontrollera tillverkarens manual för detaljer. Laddaren kommer automatiskt att lägga till sin unika identifierare (stations-ID) till URL:en. I sällsynta fall kan du behöva ange identifieraren manuellt. Exempel: `{url}`",
|
||||
"ocppLabel": "OCPP-Server URL",
|
||||
"switchsockets": "Byt kontakt",
|
||||
"template": "Tillverkare",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "ஈ.வி. சார்சர்ச்",
|
||||
"generic": "பொதுவான ஒருங்கிணைப்புகள்",
|
||||
"heatingdevices": "வெப்ப சாதனங்கள்",
|
||||
"ocppHelp": "இந்த முகவரியை உங்கள் சார்சர்ச் உள்ளமைவில் நகலெடுக்கவும்.",
|
||||
"ocppHelp": "இந்த URL-ஐ உங்கள் சார்ஜரின் உள்ளமைவில் நகலெடுக்கவும். விவரங்களுக்கு உற்பத்தியாளரின் கையேட்டைப் பார்க்கவும். சார்ஜர் தானாகவே அதன் தனித்துவமான அடையாளங்காட்டியை (நிலைய ID) URL-க்கு இணைக்கும். அரிதான சந்தர்ப்பங்களில், நீங்கள் அடையாளங்காட்டியை கைமுறையாக குறிப்பிட வேண்டியிருக்கலாம். எடுத்துக்காட்டு: `{url}`",
|
||||
"ocppLabel": "OCPP-SERVER முகவரி",
|
||||
"switchsockets": "மாறக்கூடிய சாக்கெட்டுகள்",
|
||||
"template": "உற்பத்தியாளர்",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Doldurma cihazları",
|
||||
"generic": "Genel entegrasyon",
|
||||
"heatingdevices": "Isıtma cihazları",
|
||||
"ocppHelp": "Bu adresi doldurma cihazının yapılandırmasına kopyala.",
|
||||
"ocppHelp": "Bu URL'yi şarj cihazınızın yapılandırmasına kopyalayın. Ayrıntılar için üreticinin kılavuzuna bakın. Şarj cihazı, benzersiz tanımlayıcısını (istasyon kimliği) URL'ye otomatik olarak ekleyecektir. Nadir durumlarda tanımlayıcıyı manuel olarak belirtmeniz gerekebilir. Örnek: `{url}`",
|
||||
"ocppLabel": "OCPP sunucusu URL'si",
|
||||
"switchsockets": "“Açılıp kapanabilen prizler”",
|
||||
"template": "Üretici",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "Зарядні пристрої для електромобілів",
|
||||
"generic": "Загальні інтеграції",
|
||||
"heatingdevices": "Нагрівальні прилади",
|
||||
"ocppHelp": "Скопіюйте цю адресу в конфігурацію зарядних пристроїв.",
|
||||
"ocppHelp": "Скопіюйте цей URL у конфігурацію вашого зарядного пристрою. Перевірте посібник виробника для отримання деталей. Зарядний пристрій автоматично додасть свій унікальний ідентифікатор (ID станції) до URL. У рідкісних випадках може знадобитися вказати ідентифікатор вручну. Приклад: `{url}`",
|
||||
"ocppLabel": "URL-адреса OCPP-сервера",
|
||||
"switchsockets": "Перемикаються розетки",
|
||||
"template": "Виробник",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"chargers": "电动汽车充电桩",
|
||||
"generic": "通用集成",
|
||||
"heatingdevices": "加热设备",
|
||||
"ocppHelp": "将此地址复制到您的充电桩配置中。",
|
||||
"ocppHelp": "将此URL复制到您的充电器配置中。详细信息请查看制造商的手册。充电器将自动将其唯一标识符(站点ID)附加到URL。在极少数情况下,您可能需要手动指定标识符。示例:`{url}`",
|
||||
"ocppLabel": "OCPP 服务器 URL",
|
||||
"switchsockets": "可切换插座",
|
||||
"template": "制造商",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -42,6 +42,7 @@
|
|||
"@types/kill-port": "^2.0.3",
|
||||
"@types/smoothscroll-polyfill": "^0.3.4",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.30",
|
||||
|
|
@ -78,7 +79,8 @@
|
|||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-tsc": "^3.1.5",
|
||||
"wait-on": "^9.0.1"
|
||||
"wait-on": "^9.0.1",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"@types/kill-port": "^2.0.3",
|
||||
"@types/smoothscroll-polyfill": "^0.3.4",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/compiler-sfc": "^3.2.30",
|
||||
|
|
@ -99,6 +100,7 @@
|
|||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-tsc": "^3.1.5",
|
||||
"wait-on": "^9.0.1"
|
||||
"wait-on": "^9.0.1",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -664,12 +664,12 @@ test.describe("sponsor token", () => {
|
|||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint with OCPP charger
|
||||
await newLoadpoint(page, "OCPP Test Charger");
|
||||
// add loadpoint with Peblar charger
|
||||
await newLoadpoint(page, "Peblar Test Charger");
|
||||
await page.getByTestId("loadpoint-modal").getByRole("button", { name: "Add charger" }).click();
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption({ label: "OCPP 1.6J compatible" });
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption({ label: "Peblar Business" });
|
||||
|
||||
// verify disabled save button
|
||||
await expect(chargerModal.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
|
|
|
|||
97
tests/config-ocpp.spec.ts
Normal file
97
tests/config-ocpp.spec.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorUrl } from "./simulator";
|
||||
import { expectModalVisible, enableExperimental } from "./utils";
|
||||
import axios from "axios";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const OCPP_STATION_ID = "test-station-001";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await startSimulator();
|
||||
await start();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
await stopSimulator();
|
||||
});
|
||||
|
||||
test.describe("ocpp", () => {
|
||||
test("ocpp modal", async ({ page }) => {
|
||||
// Navigate to config page
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, true);
|
||||
|
||||
// Open OCPP card
|
||||
const ocppCard = page.getByTestId("ocpp");
|
||||
await expect(ocppCard).toContainText(["Configured", "no"].join(""));
|
||||
await ocppCard.getByRole("button", { name: "edit" }).click();
|
||||
|
||||
const ocppModal = page.getByTestId("ocpp-modal");
|
||||
await expectModalVisible(ocppModal);
|
||||
const serverUrl = await ocppModal.getByLabel("Server URL").inputValue();
|
||||
await expect(ocppModal).toContainText("No OCPP chargers detected.");
|
||||
|
||||
// Connect OCPP client via simulator UI
|
||||
await page.goto(simulatorUrl());
|
||||
|
||||
// Fill in OCPP connection details
|
||||
const addClientCard = page.getByTestId("ocpp-add-client");
|
||||
await addClientCard.getByLabel("Server URL").fill(serverUrl);
|
||||
await addClientCard.getByLabel("Station ID").fill(OCPP_STATION_ID);
|
||||
await addClientCard.getByRole("button", { name: "Connect" }).click();
|
||||
|
||||
// Navigate back to evcc config page
|
||||
await page.goto("/#/config");
|
||||
await expect(ocppCard).toContainText(["Connections", "0/0", "Detected", "1"].join(""));
|
||||
await ocppCard.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(ocppModal);
|
||||
await expect(ocppModal).toContainText("Detected station IDs");
|
||||
await expect(ocppModal).toContainText([OCPP_STATION_ID, "Unknown"].join(""));
|
||||
await expect(ocppModal).not.toContainText("No OCPP chargers detected.");
|
||||
});
|
||||
|
||||
test("create ocpp charger", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, true);
|
||||
|
||||
// Open loadpoint modal and select charging point type
|
||||
await page.getByRole("button", { name: "Add charger or heater" }).click();
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Charging point" }).click();
|
||||
await lpModal.getByLabel("Title").fill("OCPP Test Charger");
|
||||
|
||||
// Open charger modal and select OCPP 1.6J
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("OCPP 1.6J compatible");
|
||||
|
||||
// Verify waiting for connection state and extract server URL
|
||||
const serverUrl = await chargerModal.getByLabel("OCPP-Server URL").inputValue();
|
||||
const waitingButton = chargerModal.getByRole("button", { name: "Waiting for connection" });
|
||||
await expect(waitingButton).toBeVisible();
|
||||
|
||||
// Connect OCPP client via simulator REST API
|
||||
await axios.post(`${simulatorUrl()}/api/ocpp/connect`, {
|
||||
stationId: OCPP_STATION_ID,
|
||||
serverUrl: serverUrl,
|
||||
});
|
||||
|
||||
// Verify connection successful
|
||||
await expect(chargerModal).toContainText("Connected!");
|
||||
await expect(waitingButton).not.toBeVisible();
|
||||
|
||||
// Proceed to validation step
|
||||
await chargerModal.getByRole("button", { name: "Next step" }).click();
|
||||
await expect(chargerModal.getByLabel("Station ID")).toHaveValue(OCPP_STATION_ID);
|
||||
|
||||
// Validate and verify sponsor token error
|
||||
const testResult = chargerModal.getByTestId("test-result");
|
||||
await testResult.getByRole("link", { name: "Validate" }).click();
|
||||
await expect(testResult).toContainText("No sponsor token configured.");
|
||||
});
|
||||
});
|
||||
|
|
@ -20,6 +20,11 @@ function workerPort() {
|
|||
return 11000 + index;
|
||||
}
|
||||
|
||||
function ocppPort() {
|
||||
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
||||
return 12000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
}
|
||||
|
|
@ -99,14 +104,20 @@ async function _restoreDatabase(sqlDumps: string) {
|
|||
async function _start(config?: string, flags: string | string[] = []) {
|
||||
const configArgs = config ? ["--config", config.includes("/") ? config : `tests/${config}`] : [];
|
||||
const port = workerPort();
|
||||
const ocpp = ocppPort();
|
||||
log(`wait until port ${port} is available`);
|
||||
// wait for port to be available
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED });
|
||||
log(`starting evcc on ports ${port} (HTTP) and ${ocpp} (OCPP)`);
|
||||
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
||||
additionalFlags.push("--log", "debug,httpd:trace");
|
||||
log("starting evcc", { config, port, additionalFlags });
|
||||
log("starting evcc", { config, port, ocpp, additionalFlags });
|
||||
const instance = spawn(BINARY, [...configArgs, ...additionalFlags], {
|
||||
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
|
||||
env: {
|
||||
EVCC_NETWORK_PORT: port.toString(),
|
||||
EVCC_OCPP_PORT: ocpp.toString(),
|
||||
EVCC_DATABASE_DSN: dbPath(),
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
|
|
@ -137,7 +148,7 @@ async function _stop(instance?: ChildProcess) {
|
|||
if (instance) {
|
||||
log("shutting down evcc hard", { port });
|
||||
instance.kill("SIGKILL");
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED });
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED, timeout: 5000 });
|
||||
log("evcc is down", { port });
|
||||
return;
|
||||
}
|
||||
|
|
@ -165,7 +176,7 @@ async function _stop(instance?: ChildProcess) {
|
|||
}
|
||||
}
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED });
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED, timeout: 5000 });
|
||||
log("evcc is down", { port });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ test.describe("opening logs", async () => {
|
|||
test.describe("features", async () => {
|
||||
test("content", async ({ page }) => {
|
||||
await page.goto("/#/log");
|
||||
await page.getByTestId("log-search").fill("listening at");
|
||||
await expect(page.getByTestId("log-content")).toContainText("listening at");
|
||||
await page.getByTestId("log-search").fill("UI local");
|
||||
await expect(page.getByTestId("log-content")).toContainText("UI local");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const LOG_ENABLED = !process.env["GITHUB_ACTIONS"];
|
|||
|
||||
function workerPort() {
|
||||
const index = parseInt(process.env["TEST_WORKER_INDEX"] ?? "-1");
|
||||
return 12000 + index;
|
||||
return 13000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import bodyParser from "body-parser";
|
||||
import type { Connect, ViteDevServer } from "vite";
|
||||
import type { ServerResponse } from "http";
|
||||
import { OcppClient } from "./ocppClient";
|
||||
|
||||
const ocppClients = new Map<string, OcppClient>();
|
||||
|
||||
let state = {
|
||||
site: {
|
||||
|
|
@ -11,6 +14,9 @@ let state = {
|
|||
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
|
||||
vehicles: [{ soc: 0, range: 0 }],
|
||||
hems: { relay: false },
|
||||
ocpp: {
|
||||
clients: [] as { stationId: string; serverUrl: string; connected: boolean }[],
|
||||
},
|
||||
};
|
||||
|
||||
const loggingMiddleware = (
|
||||
|
|
@ -34,6 +40,10 @@ const stateApiMiddleware = (
|
|||
res.end();
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
|
||||
console.log("[simulator] POST /api/shutdown");
|
||||
for (const client of ocppClients.values()) {
|
||||
client.disconnect();
|
||||
}
|
||||
ocppClients.clear();
|
||||
res.end();
|
||||
process.exit();
|
||||
} else if (req.originalUrl === "/api/state") {
|
||||
|
|
@ -117,6 +127,80 @@ const shellyMiddleware = (
|
|||
}
|
||||
};
|
||||
|
||||
const updateOcppState = () => {
|
||||
state.ocpp.clients = Array.from(ocppClients.entries()).map(([stationId, client]) => ({
|
||||
stationId,
|
||||
serverUrl: client.getServerUrl(),
|
||||
connected: client.isConnected(),
|
||||
}));
|
||||
};
|
||||
|
||||
const ocppMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
if (req.method === "POST" && req.originalUrl === "/api/ocpp/connect") {
|
||||
console.log("[simulator] POST /api/ocpp/connect");
|
||||
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
|
||||
const { stationId, serverUrl } = req.body;
|
||||
|
||||
if (!stationId || !serverUrl) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: "stationId and serverUrl required" }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ocppClients.has(stationId)) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: `Client ${stationId} already exists` }));
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new OcppClient(stationId, serverUrl);
|
||||
ocppClients.set(stationId, client);
|
||||
|
||||
client
|
||||
.connect()
|
||||
.then(() => client.bootNotification())
|
||||
.then((response) => {
|
||||
console.log("[simulator] OCPP BootNotification response:", response);
|
||||
updateOcppState();
|
||||
res.end(JSON.stringify({ status: "connected", stationId, response }));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[simulator] OCPP connection error:", error);
|
||||
ocppClients.delete(stationId);
|
||||
updateOcppState();
|
||||
res.statusCode = 500;
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
});
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/ocpp/disconnect") {
|
||||
console.log("[simulator] POST /api/ocpp/disconnect");
|
||||
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
|
||||
const { stationId } = req.body;
|
||||
|
||||
if (!stationId) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: "stationId required" }));
|
||||
return;
|
||||
}
|
||||
|
||||
const client = ocppClients.get(stationId);
|
||||
if (client) {
|
||||
client.disconnect();
|
||||
ocppClients.delete(stationId);
|
||||
updateOcppState();
|
||||
res.end(JSON.stringify({ status: "disconnected", stationId }));
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: `Client ${stationId} not found` }));
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export default () => ({
|
||||
name: "api",
|
||||
enforce: "pre",
|
||||
|
|
@ -129,6 +213,7 @@ export default () => ({
|
|||
server.middlewares.use(openemsMiddleware);
|
||||
server.middlewares.use(teslaloggerMiddleware);
|
||||
server.middlewares.use(shellyMiddleware);
|
||||
server.middlewares.use(ocppMiddleware);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
221
tests/simulator/ocppClient.ts
Normal file
221
tests/simulator/ocppClient.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import WebSocket from "ws";
|
||||
|
||||
export class OcppClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageId = 0;
|
||||
private pendingCallbacks = new Map<string, (response: any) => void>();
|
||||
private connected = false;
|
||||
private stationId: string;
|
||||
private serverUrl: string;
|
||||
|
||||
constructor(stationId: string, serverUrl: string) {
|
||||
this.stationId = stationId;
|
||||
this.serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.serverUrl}${this.stationId}`;
|
||||
console.log(`[OCPP Client] Connecting to ${url}`);
|
||||
|
||||
this.ws = new WebSocket(url, ["ocpp1.6"]);
|
||||
|
||||
this.ws.on("open", () => {
|
||||
console.log(`[OCPP Client] Connected as ${this.stationId}`);
|
||||
this.connected = true;
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ws.on("message", (data: WebSocket.Data) => {
|
||||
const message = JSON.parse(data.toString());
|
||||
this.handleMessage(message);
|
||||
});
|
||||
|
||||
this.ws.on("error", (error) => {
|
||||
console.error(`[OCPP Client] Error:`, error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.ws.on("close", () => {
|
||||
console.log(`[OCPP Client] Disconnected`);
|
||||
this.connected = false;
|
||||
});
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!this.connected) {
|
||||
this.disconnect();
|
||||
reject(new Error("Connection timeout"));
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage(message: any[]) {
|
||||
const messageType = message[0];
|
||||
|
||||
// Handle CallResult (3) - response to our calls
|
||||
if (messageType === 3) {
|
||||
const messageId = message[1];
|
||||
const payload = message[2];
|
||||
const callback = this.pendingCallbacks.get(messageId);
|
||||
if (callback) {
|
||||
callback(payload);
|
||||
this.pendingCallbacks.delete(messageId);
|
||||
}
|
||||
}
|
||||
// Handle Call (2) - requests from server
|
||||
else if (messageType === 2) {
|
||||
const messageId = message[1];
|
||||
const action = message[2];
|
||||
const payload = message[3];
|
||||
console.log(`[OCPP Client] Received ${action}:`, payload);
|
||||
|
||||
// Auto-respond to common server requests
|
||||
this.handleServerRequest(messageId, action, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private handleServerRequest(messageId: string, action: string, payload: any) {
|
||||
let response: any = {};
|
||||
|
||||
switch (action) {
|
||||
case "GetConfiguration":
|
||||
response = {
|
||||
configurationKey: [
|
||||
{ key: "NumberOfConnectors", readonly: true, value: "1" },
|
||||
{
|
||||
key: "MeterValuesSampledData",
|
||||
readonly: false,
|
||||
value: "Energy.Active.Import.Register,Power.Active.Import",
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
case "ChangeAvailability":
|
||||
response = { status: "Accepted" };
|
||||
break;
|
||||
case "RemoteStartTransaction":
|
||||
response = { status: "Accepted" };
|
||||
break;
|
||||
case "RemoteStopTransaction":
|
||||
response = { status: "Accepted" };
|
||||
break;
|
||||
case "TriggerMessage":
|
||||
// Handle trigger and send the requested message
|
||||
if (payload.requestedMessage === "StatusNotification") {
|
||||
this.statusNotification(1, "Available");
|
||||
}
|
||||
response = { status: "Accepted" };
|
||||
break;
|
||||
default:
|
||||
console.log(`[OCPP Client] Unknown action ${action}, sending empty response`);
|
||||
}
|
||||
|
||||
// Send CallResult
|
||||
const responseMessage = [3, messageId, response];
|
||||
this.send(responseMessage);
|
||||
}
|
||||
|
||||
private send(message: any): void {
|
||||
if (this.ws && this.connected) {
|
||||
const data = JSON.stringify(message);
|
||||
console.log(`[OCPP Client] Sending:`, data);
|
||||
this.ws.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
private async call(action: string, payload: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = (++this.messageId).toString();
|
||||
const message = [2, messageId, action, payload];
|
||||
|
||||
// Setup callback for response
|
||||
this.pendingCallbacks.set(messageId, resolve);
|
||||
|
||||
// Send message
|
||||
this.send(message);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (this.pendingCallbacks.has(messageId)) {
|
||||
this.pendingCallbacks.delete(messageId);
|
||||
reject(new Error(`Timeout waiting for response to ${action}`));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async bootNotification(model = "Simulator", vendor = "evcc-test"): Promise<any> {
|
||||
return this.call("BootNotification", {
|
||||
chargePointModel: model,
|
||||
chargePointVendor: vendor,
|
||||
chargePointSerialNumber: this.stationId,
|
||||
firmwareVersion: "1.0.0",
|
||||
});
|
||||
}
|
||||
|
||||
async statusNotification(
|
||||
connectorId: number,
|
||||
status: string,
|
||||
errorCode = "NoError"
|
||||
): Promise<any> {
|
||||
return this.call("StatusNotification", {
|
||||
connectorId,
|
||||
status,
|
||||
errorCode,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async meterValues(connectorId: number, power: number, energy: number): Promise<any> {
|
||||
return this.call("MeterValues", {
|
||||
connectorId,
|
||||
meterValue: [
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
sampledValue: [
|
||||
{
|
||||
value: power.toString(),
|
||||
context: "Sample.Periodic",
|
||||
format: "Raw",
|
||||
measurand: "Power.Active.Import",
|
||||
phase: null,
|
||||
location: "Outlet",
|
||||
unit: "W",
|
||||
},
|
||||
{
|
||||
value: energy.toString(),
|
||||
context: "Sample.Periodic",
|
||||
format: "Raw",
|
||||
measurand: "Energy.Active.Import.Register",
|
||||
phase: null,
|
||||
location: "Outlet",
|
||||
unit: "Wh",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
getStationId(): string {
|
||||
return this.stationId;
|
||||
}
|
||||
|
||||
getServerUrl(): string {
|
||||
return this.serverUrl;
|
||||
}
|
||||
}
|
||||
|
|
@ -250,6 +250,80 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="my-4">OCPP <small>work in progress, connect only</small></h4>
|
||||
|
||||
<!-- Connected Clients -->
|
||||
<div
|
||||
v-for="client in state.ocpp.clients"
|
||||
:key="client.stationId"
|
||||
class="card mb-3"
|
||||
:data-testid="`ocpp-client-${client.stationId}`"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="card-title mb-0">{{ client.stationId }}</h5>
|
||||
<span class="badge" :class="client.connected ? 'bg-success' : 'bg-secondary'">
|
||||
{{ client.connected ? "Connected" : "Disconnected" }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text text-muted small mb-2">{{ client.serverUrl }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="disconnectOcpp(client.stationId)"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add OCPP Client Card -->
|
||||
<div class="card mb-3" data-testid="ocpp-add-client">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">OCPP Client</h5>
|
||||
<div class="row">
|
||||
<label for="ocppServerUrl" class="col-sm-6 col-form-label">Server URL</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
id="ocppServerUrl"
|
||||
v-model="ocppServerUrl"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="ocppStationId" class="col-sm-6 col-form-label">Station ID</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
id="ocppStationId"
|
||||
v-model="ocppStationId"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6"></div>
|
||||
<div class="col-sm-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary w-100"
|
||||
:disabled="!ocppServerUrl || !ocppStationId || connecting"
|
||||
@click="connectOcpp"
|
||||
>
|
||||
{{ connecting ? "Connecting..." : "Connect" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 text-center fixed-bottom bg-light text-dark bg-opacity-75">
|
||||
<button type="submit" class="btn btn-primary">Apply changes</button>
|
||||
</div>
|
||||
|
|
@ -278,7 +352,13 @@ export default defineComponent({
|
|||
}[];
|
||||
vehicles: { soc: number; range: number }[];
|
||||
hems: { relay: boolean };
|
||||
ocpp: {
|
||||
clients: { stationId: string; serverUrl: string; connected: boolean }[];
|
||||
};
|
||||
} | null,
|
||||
ocppServerUrl: "ws://127.0.0.1:8887/",
|
||||
ocppStationId: "",
|
||||
connecting: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -292,6 +372,33 @@ export default defineComponent({
|
|||
const response = await axios.get("/api/state");
|
||||
this.state = response.data;
|
||||
},
|
||||
async connectOcpp() {
|
||||
this.connecting = true;
|
||||
try {
|
||||
await axios.post("/api/ocpp/connect", {
|
||||
stationId: this.ocppStationId,
|
||||
serverUrl: this.ocppServerUrl,
|
||||
});
|
||||
// Reload state to get updated ocpp status
|
||||
await this.load();
|
||||
this.ocppStationId = "";
|
||||
} catch (error) {
|
||||
console.error("Failed to connect OCPP client:", error);
|
||||
alert("Failed to connect OCPP client");
|
||||
} finally {
|
||||
this.connecting = false;
|
||||
}
|
||||
},
|
||||
async disconnectOcpp(stationId: string) {
|
||||
try {
|
||||
await axios.post("/api/ocpp/disconnect", { stationId });
|
||||
// Reload state to get updated ocpp status
|
||||
await this.load();
|
||||
} catch (error) {
|
||||
console.error("Failed to disconnect OCPP client:", error);
|
||||
alert("Failed to disconnect OCPP client");
|
||||
}
|
||||
},
|
||||
addVehicle() {
|
||||
// push a duplacate of the last entry
|
||||
const vehicles = this.state?.vehicles;
|
||||
|
|
@ -327,6 +434,7 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||
margin: 0;
|
||||
}
|
||||
input[type="number"] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
input[type="number"] {
|
||||
|
|
|
|||
|
|
@ -596,8 +596,8 @@ presets:
|
|||
description:
|
||||
generic: Station ID
|
||||
help:
|
||||
en: "Station ID of the charging point. Note: In exceptional cases, it may be necessary to manually append this ID to the OCPP URL of the charging station in the form `ws://<evcc-address>:8887/<stationid>`. Most charging stations automatically add the ID internally."
|
||||
de: "Station ID des Ladepunktes. Hinweis: In Ausnahmefällen kann es erforderlich sein, diese ID manuell an die OCPP-URL der Ladestation in der Form `ws://<evcc-adresse>:8887/<stationid>` anzuhängen. Die meisten Ladestationen fügen die ID intern automatisch hinzu."
|
||||
en: Unique identifier of the charger. evcc auto-detects this once the charger connects.
|
||||
de: Eindeutige Wallbox-Kennung. evcc erkennt diese automatisch, sobald sich die Wallbox verbindet.
|
||||
example: EVB-P12354
|
||||
- name: connector
|
||||
description:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue