1194 lines
36 KiB
Vue
1194 lines
36 KiB
Vue
<template>
|
|
<div class="root safe-area-inset">
|
|
<div class="container px-4">
|
|
<TopHeader
|
|
ref="header"
|
|
:title="$t('config.main.title')"
|
|
:notifications="notifications"
|
|
/>
|
|
<div class="wrapper mb-3">
|
|
<AuthSuccessBanner
|
|
v-if="callbackCompleted || callbackError"
|
|
:provider-id="callbackCompleted"
|
|
:error="callbackError"
|
|
:auth-providers="authProviders"
|
|
/>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2>
|
|
<GeneralConfig
|
|
:experimental="experimental"
|
|
:sponsor-error="hasClassError('sponsorship')"
|
|
@site-changed="siteChanged"
|
|
/>
|
|
|
|
<WelcomeBanner v-if="setupRequired" />
|
|
<h2 class="my-4">{{ $t("config.section.loadpoints") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<DeviceCard
|
|
v-for="loadpoint in loadpoints"
|
|
:id="`loadpoint_${loadpoint.name}`"
|
|
:key="loadpoint.name"
|
|
:title="loadpoint.title"
|
|
:name="loadpoint.name"
|
|
:editable="!!loadpoint.id"
|
|
:error="hasDeviceError('loadpoint', loadpoint.name)"
|
|
data-testid="loadpoint"
|
|
@edit="openModal('loadpoint', { id: loadpoint.id })"
|
|
>
|
|
<template #tags>
|
|
<DeviceTags :tags="loadpointTags(loadpoint)" />
|
|
</template>
|
|
<template #icon>
|
|
<VehicleIcon
|
|
v-if="chargerIcon(loadpoint.charger)"
|
|
:name="chargerIcon(loadpoint.charger)"
|
|
/>
|
|
<LoadpointIcon v-else />
|
|
</template>
|
|
</DeviceCard>
|
|
|
|
<NewDeviceButton
|
|
data-testid="add-loadpoint"
|
|
:title="$t('config.main.addLoadpoint')"
|
|
@click="openModal('loadpoint')"
|
|
/>
|
|
</div>
|
|
|
|
<h2 class="my-4">{{ $t("config.section.vehicles") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<DeviceCard
|
|
v-for="vehicle in vehicles"
|
|
:id="`vehicle_${vehicle.name}`"
|
|
:key="vehicle.name"
|
|
:title="vehicle.config?.title || vehicle.name"
|
|
:name="vehicle.name"
|
|
:editable="vehicle.id >= 0"
|
|
:error="hasDeviceError('vehicle', vehicle.name)"
|
|
data-testid="vehicle"
|
|
@edit="openModal('vehicle', { id: vehicle.id })"
|
|
>
|
|
<template #icon>
|
|
<VehicleIcon :name="vehicle.config?.icon" />
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="deviceTags('vehicle', vehicle.name)" />
|
|
</template>
|
|
</DeviceCard>
|
|
<NewDeviceButton
|
|
data-testid="add-vehicle"
|
|
:title="$t('config.main.addVehicle')"
|
|
@click="openModal('vehicle')"
|
|
/>
|
|
</div>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.grid") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<MeterCard
|
|
v-if="gridMeter"
|
|
:meter="gridMeter"
|
|
:title="$t('config.grid.title')"
|
|
meter-type="grid"
|
|
:has-error="hasDeviceError('meter', gridMeter.name)"
|
|
:tags="deviceTags('meter', gridMeter.name)"
|
|
@edit="(type, id) => openModal('meter', { type, id })"
|
|
/>
|
|
<NewDeviceButton
|
|
v-else
|
|
:title="$t('config.main.addGrid')"
|
|
data-testid="add-grid"
|
|
@click="openModal('meter', { type: 'grid' })"
|
|
/>
|
|
</div>
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.meter") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<MeterCard
|
|
v-for="meter in pvMeters"
|
|
:key="meter.name"
|
|
:meter="meter"
|
|
meter-type="pv"
|
|
:has-error="hasDeviceError('meter', meter.name)"
|
|
:tags="deviceTags('meter', meter.name)"
|
|
@edit="(type, id) => openModal('meter', { type, id })"
|
|
/>
|
|
<MeterCard
|
|
v-for="meter in batteryMeters"
|
|
:key="meter.name"
|
|
:meter="meter"
|
|
meter-type="battery"
|
|
:has-error="hasDeviceError('meter', meter.name)"
|
|
:tags="deviceTags('meter', meter.name)"
|
|
@edit="(type, id) => openModal('meter', { type, id })"
|
|
/>
|
|
<NewDeviceButton
|
|
:title="$t('config.main.addPvBattery')"
|
|
@click="openModal('meter', { choices: ['pv', 'battery'] })"
|
|
/>
|
|
</div>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.additionalMeter") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<MeterCard
|
|
v-for="meter in auxMeters"
|
|
:key="meter.name"
|
|
:meter="meter"
|
|
meter-type="aux"
|
|
:has-error="hasDeviceError('meter', meter.name)"
|
|
:tags="deviceTags('meter', meter.name)"
|
|
@edit="(type, id) => openModal('meter', { type, id })"
|
|
/>
|
|
<MeterCard
|
|
v-for="meter in extMeters"
|
|
:key="meter.name"
|
|
:meter="meter"
|
|
meter-type="ext"
|
|
:has-error="hasDeviceError('meter', meter.name)"
|
|
:tags="deviceTags('meter', meter.name)"
|
|
@edit="(type, id) => openModal('meter', { type, id })"
|
|
/>
|
|
<NewDeviceButton
|
|
:title="$t('config.main.addAdditional')"
|
|
@click="openModal('meter', { choices: ['aux', 'ext'] })"
|
|
/>
|
|
</div>
|
|
|
|
<h2 id="tariffs" class="my-4 mt-5">{{ $t("config.tariff.title") }}</h2>
|
|
<div v-if="!!tariffsYamlSource" class="p-0 config-list">
|
|
<DeviceCard
|
|
:title="$t('config.tariff.title')"
|
|
:editable="tariffsYamlSource === 'db'"
|
|
:unconfigured="isUnconfigured(tariffTags)"
|
|
:error="hasClassError('tariff')"
|
|
:badge="tariffsYamlSource === 'db'"
|
|
data-testid="tariffs-legacy"
|
|
:currency="currency"
|
|
@edit="openModal('tariffsLegacy')"
|
|
>
|
|
<template #icon>
|
|
<shopicon-regular-receivepayment></shopicon-regular-receivepayment>
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="tariffTags" :currency="currency" />
|
|
</template>
|
|
</DeviceCard>
|
|
</div>
|
|
<div v-else class="p-0 config-list">
|
|
<TariffCard
|
|
v-if="gridTariff"
|
|
:tariff="gridTariff"
|
|
tariff-type="grid"
|
|
:has-error="hasDeviceError('tariff', gridTariff.name)"
|
|
:tags="deviceTags('tariff', gridTariff.name)"
|
|
:currency="currency"
|
|
@edit="openModal('tariff', { type: 'grid', id: gridTariff.id })"
|
|
/>
|
|
<TariffCard
|
|
v-if="feedInTariff"
|
|
:tariff="feedInTariff"
|
|
tariff-type="feedIn"
|
|
:has-error="hasDeviceError('tariff', feedInTariff.name)"
|
|
:tags="deviceTags('tariff', feedInTariff.name)"
|
|
:currency="currency"
|
|
@edit="openModal('tariff', { type: 'feedIn', id: feedInTariff.id })"
|
|
/>
|
|
<NewDeviceButton
|
|
v-if="possibleTariffTypes.length"
|
|
:title="$t('config.tariff.addTariff')"
|
|
@click="openModal('tariff', { choices: possibleTariffTypes })"
|
|
/>
|
|
<TariffCard
|
|
v-if="co2Tariff"
|
|
:tariff="co2Tariff"
|
|
tariff-type="co2"
|
|
:has-error="hasDeviceError('tariff', co2Tariff.name)"
|
|
:tags="deviceTags('tariff', co2Tariff.name)"
|
|
:currency="currency"
|
|
@edit="openModal('tariff', { type: 'co2', id: co2Tariff.id })"
|
|
/>
|
|
<TariffCard
|
|
v-for="tariff in solarTariffs"
|
|
:key="tariff.name"
|
|
:tariff="tariff"
|
|
tariff-type="solar"
|
|
:has-error="hasDeviceError('tariff', tariff.name)"
|
|
:tags="deviceTags('tariff', tariff.name)"
|
|
:currency="currency"
|
|
@edit="openModal('tariff', { type: 'solar', id: tariff.id })"
|
|
/>
|
|
<TariffCard
|
|
v-if="plannerTariff"
|
|
:tariff="plannerTariff"
|
|
tariff-type="planner"
|
|
:has-error="hasDeviceError('tariff', plannerTariff.name)"
|
|
:tags="deviceTags('tariff', plannerTariff.name)"
|
|
:currency="currency"
|
|
@edit="openModal('tariff', { type: 'planner', id: plannerTariff.id })"
|
|
/>
|
|
<NewDeviceButton
|
|
v-if="possibleForecastTypes.length"
|
|
:title="$t('config.tariff.addForecast')"
|
|
@click="openModal('tariff', { choices: possibleForecastTypes })"
|
|
/>
|
|
</div>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.integrations") }}</h2>
|
|
|
|
<div class="p-0 config-list">
|
|
<AuthProvidersCard
|
|
:providers="authProviders"
|
|
data-testid="auth-providers"
|
|
@auth-request="handleProviderAuthRequest"
|
|
/>
|
|
<DeviceCard
|
|
:title="$t('config.mqtt.title')"
|
|
editable
|
|
:error="hasClassError('mqtt')"
|
|
:unconfigured="isUnconfigured(mqttTags)"
|
|
data-testid="mqtt"
|
|
@edit="openModal('mqtt')"
|
|
>
|
|
<template #icon><MqttIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="mqttTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.messaging.title')"
|
|
:editable="messagingYamlSource !== 'file'"
|
|
:error="hasClassError('messenger')"
|
|
:unconfigured="isUnconfigured(messagingTags)"
|
|
:badge="messagingYamlSource === 'db'"
|
|
data-testid="messaging"
|
|
@edit="openMessagingModal"
|
|
>
|
|
<template #icon><NotificationIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="messagingTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.influx.title')"
|
|
editable
|
|
:error="hasClassError('influx')"
|
|
:unconfigured="isUnconfigured(influxTags)"
|
|
data-testid="influx"
|
|
@edit="openModal('influx')"
|
|
>
|
|
<template #icon><InfluxIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="influxTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="`${$t('config.circuits.title')}`"
|
|
editable
|
|
:error="hasClassError('circuit')"
|
|
:unconfigured="!circuitsRoot"
|
|
data-testid="circuits"
|
|
@edit="openModal('circuits')"
|
|
>
|
|
<template #icon><CircuitsIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags
|
|
v-if="!circuitsRoot"
|
|
:tags="{ configured: { value: false } }"
|
|
/>
|
|
<CircuitTags v-else :nodes="[circuitsRoot]" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.modbusproxy.title')"
|
|
editable
|
|
:error="hasClassError('modbusproxy')"
|
|
:unconfigured="isUnconfigured(modbusproxyTags)"
|
|
data-testid="modbusproxy"
|
|
@edit="openModal('modbusproxy')"
|
|
>
|
|
<template #icon><ModbusProxyIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="modbusproxyTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.hems.title')"
|
|
editable
|
|
:error="hasClassError('hems')"
|
|
:unconfigured="isUnconfigured(hemsTags)"
|
|
data-testid="hems"
|
|
@edit="openModal('hems')"
|
|
>
|
|
<template #icon><HemsIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="hemsTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
v-if="experimental"
|
|
:title="`${$t('config.remote.title')} 🧪`"
|
|
editable
|
|
:unconfigured="isUnconfigured(remoteTags)"
|
|
data-testid="remote-access"
|
|
@edit="openModal('remote')"
|
|
>
|
|
<template #icon><RemoteAccessIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="remoteTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
v-if="experimental"
|
|
:title="`${$t('config.optimizer.title')} 🧪`"
|
|
editable
|
|
:unconfigured="isUnconfigured(optimizerTags)"
|
|
data-testid="optimizer"
|
|
@edit="openModal('optimizer')"
|
|
>
|
|
<template #icon><OptimizerIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="optimizerTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
</div>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.services") }}</h2>
|
|
<div class="p-0 config-list">
|
|
<DeviceCard
|
|
:title="$t('config.ocpp.title')"
|
|
editable
|
|
:error="hasClassError('ocpp')"
|
|
data-testid="ocpp"
|
|
@edit="openModal('ocpp')"
|
|
>
|
|
<template #icon><OcppIcon /></template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.shm.cardTitle')"
|
|
editable
|
|
:error="hasClassError('shm')"
|
|
data-testid="shm"
|
|
@edit="openModal('shm')"
|
|
>
|
|
<template #icon><ShmIcon /></template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:title="$t('config.eebus.title')"
|
|
editable
|
|
:error="hasClassError('eebus')"
|
|
data-testid="eebus"
|
|
@edit="openModal('eebus')"
|
|
>
|
|
<template #icon><EebusIcon /></template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
v-if="experimental"
|
|
:title="`${$t('config.mcp.title')} 🧪`"
|
|
editable
|
|
data-testid="mcp"
|
|
@edit="openModal('mcp')"
|
|
>
|
|
<template #icon><McpIcon /></template>
|
|
</DeviceCard>
|
|
</div>
|
|
|
|
<hr class="my-5" />
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
|
|
<div data-testid="config-system" class="round-box p-4 d-flex gap-4 flex-wrap">
|
|
<router-link to="/log" class="btn btn-outline-secondary">
|
|
{{ $t("config.system.logs") }}
|
|
</router-link>
|
|
<router-link to="/issue" class="btn btn-outline-secondary">
|
|
{{ $t("help.issueButton") }}
|
|
</router-link>
|
|
<button
|
|
data-testid="backup-restore"
|
|
class="btn btn-outline-secondary text-truncate"
|
|
@click="openModal('backuprestore')"
|
|
>
|
|
{{ $t("config.system.backupRestore.title") }}
|
|
</button>
|
|
<button class="btn btn-outline-danger" @click="restart">
|
|
{{ $t("config.system.restart") }}
|
|
</button>
|
|
</div>
|
|
|
|
<LoadpointModal
|
|
:vehicleOptions="vehicleOptions"
|
|
:loadpointCount="loadpoints.length"
|
|
:chargers="chargers"
|
|
:chargerValues="deviceValues['charger']"
|
|
:meters="meters"
|
|
:circuits="circuits"
|
|
:hasDeviceError="hasDeviceError"
|
|
@changed="loadpointChanged"
|
|
@dismissed="loadpointDismissed"
|
|
/>
|
|
<VehicleModal :is-sponsor="isSponsor" @vehicle-changed="vehicleChanged" />
|
|
<MeterModal :is-sponsor="isSponsor" @changed="meterChanged" />
|
|
<ChargerModal :is-sponsor="isSponsor" :ocpp="ocpp" @changed="chargerChanged" />
|
|
<InfluxModal @changed="loadDirty" />
|
|
<MqttModal @changed="loadDirty" />
|
|
<NetworkModal @changed="loadDirty" />
|
|
<ControlModal @changed="loadDirty" />
|
|
<HemsModal :yamlSource="hems?.yamlSource" @changed="loadDirty" />
|
|
<ShmModal @changed="loadDirty" />
|
|
<MessagingLegacyModal @changed="loadDirty" />
|
|
<MessagingModal :messengers="messengers" @changed="loadDirty" />
|
|
<MessengerModal @changed="messengerChanged" />
|
|
<TariffsLegacyModal @changed="loadDirty" />
|
|
<TariffModal :currency="currency" @changed="tariffChanged" />
|
|
<TelemetryModal :is-sponsor="isSponsor" :telemetry="telemetry" />
|
|
<OptimizerModal :is-sponsor="isSponsor" />
|
|
<McpModal />
|
|
<ExperimentalModal :experimental="experimental" />
|
|
<RemoteModal :remote="remote" :is-sponsor="isSponsor" />
|
|
<TitleModal @changed="loadDirty" />
|
|
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
|
|
<CircuitsModal :gridMeter="gridMeter" :extMeters="extMeters" @changed="loadDirty" />
|
|
<EebusModal
|
|
:status="eebus?.status"
|
|
:yamlSource="eebus?.yamlSource"
|
|
@changed="loadDirty"
|
|
/>
|
|
<OcppModal :ocpp="ocpp" />
|
|
<BackupRestoreModal v-bind="backupRestoreProps" />
|
|
<SecurityModal :auth-disabled="authDisabled" />
|
|
<ApiKeyModal :auth-disabled="authDisabled" />
|
|
<PasswordModal update-mode />
|
|
<SponsorModal :error="hasClassError('sponsorship')" @changed="loadDirty" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import "@h2d2/shopicons/es/regular/sun";
|
|
import "@h2d2/shopicons/es/regular/batterythreequarters";
|
|
import "@h2d2/shopicons/es/regular/powersupply";
|
|
import "@h2d2/shopicons/es/regular/receivepayment";
|
|
import NewDeviceButton from "../components/Config/NewDeviceButton.vue";
|
|
import api from "../api";
|
|
import ChargerModal from "../components/Config/ChargerModal.vue";
|
|
import CircuitsIcon from "../components/MaterialIcon/Circuits.vue";
|
|
import CircuitsModal from "../components/Config/CircuitsModal.vue";
|
|
import CircuitTags from "../components/Config/CircuitTags.vue";
|
|
import collector from "../mixins/collector";
|
|
import ControlModal from "../components/Config/ControlModal.vue";
|
|
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";
|
|
import HemsModal from "../components/Config/HemsModal.vue";
|
|
import ShmIcon from "../components/MaterialIcon/Shm.vue";
|
|
import ShmModal from "@/components/Config/ShmModal.vue";
|
|
import InfluxIcon from "../components/MaterialIcon/Influx.vue";
|
|
import InfluxModal from "../components/Config/InfluxModal.vue";
|
|
import LoadpointModal from "../components/Config/LoadpointModal.vue";
|
|
import LoadpointIcon from "../components/MaterialIcon/Loadpoint.vue";
|
|
import MessagingModal from "../components/Config/Messaging/MessagingModal.vue";
|
|
import MessengerModal from "@/components/Config/Messaging/MessengerModal.vue";
|
|
import MessagingLegacyModal from "@/components/Config/Messaging/MessagingLegacyModal.vue";
|
|
import MeterModal from "../components/Config/MeterModal.vue";
|
|
import MeterCard from "../components/Config/MeterCard.vue";
|
|
import { openModal, type ModalResult } from "@/configModal";
|
|
import ModbusProxyIcon from "../components/MaterialIcon/ModbusProxy.vue";
|
|
import ModbusProxyModal from "../components/Config/ModbusProxyModal.vue";
|
|
import MqttIcon from "../components/MaterialIcon/Mqtt.vue";
|
|
import MqttModal from "../components/Config/MqttModal.vue";
|
|
import RemoteAccessIcon from "../components/MaterialIcon/RemoteAccess.vue";
|
|
import RemoteModal from "../components/Config/Remote/RemoteModal.vue";
|
|
import { isRemoteClientActive } from "@/utils/remote";
|
|
import NetworkModal from "../components/Config/NetworkModal.vue";
|
|
import NotificationIcon from "../components/MaterialIcon/Notification.vue";
|
|
import OptimizerIcon from "../components/MaterialIcon/Optimizer.vue";
|
|
import OptimizerModal from "../components/Config/OptimizerModal.vue";
|
|
import McpIcon from "../components/MaterialIcon/Mcp.vue";
|
|
import McpModal from "../components/Config/McpModal.vue";
|
|
import restart, { performRestart } from "../restart";
|
|
import SponsorModal from "../components/Config/SponsorModal.vue";
|
|
import store from "../store";
|
|
import TariffsLegacyModal from "../components/Config/TariffsLegacyModal.vue";
|
|
import TariffCard from "../components/Config/TariffCard.vue";
|
|
import TariffModal from "../components/Config/TariffModal.vue";
|
|
import TelemetryModal from "../components/Config/TelemetryModal.vue";
|
|
import ExperimentalModal from "../components/Config/ExperimentalModal.vue";
|
|
import TitleModal from "../components/Config/TitleModal.vue";
|
|
import Header from "../components/Top/Header.vue";
|
|
import VehicleIcon from "../components/VehicleIcon";
|
|
import VehicleModal from "../components/Config/VehicleModal.vue";
|
|
import { defineComponent, type PropType } from "vue";
|
|
import type {
|
|
ConfigCharger,
|
|
ConfigVehicle,
|
|
ConfigCircuit,
|
|
ConfigMessenger,
|
|
ConfigLoadpoint,
|
|
ConfigMeter,
|
|
Timeout,
|
|
VehicleOption,
|
|
MeterType,
|
|
TariffType,
|
|
SiteConfig,
|
|
DeviceType,
|
|
Notification,
|
|
Remote,
|
|
} from "@/types/evcc";
|
|
import { CURRENCY, GRID_CONTROL } from "@/types/evcc";
|
|
import { circuitTree } from "@/utils/circuits";
|
|
|
|
type DeviceValuesMap = Record<DeviceType, Record<string, any>>;
|
|
|
|
type DeviceTags = Record<
|
|
string,
|
|
{ value?: any; error?: boolean; warning?: boolean; muted?: boolean; options?: any }
|
|
>;
|
|
|
|
import BackupRestoreModal from "@/components/Config/BackupRestoreModal.vue";
|
|
import WelcomeBanner from "../components/Config/WelcomeBanner.vue";
|
|
import AuthSuccessBanner from "../components/Config/AuthSuccessBanner.vue";
|
|
import PasswordModal from "../components/Auth/PasswordModal.vue";
|
|
import SecurityModal from "../components/Config/Security/SecurityModal.vue";
|
|
import ApiKeyModal from "../components/Config/Security/ApiKeyModal.vue";
|
|
import AuthProvidersCard from "../components/Config/AuthProvidersCard.vue";
|
|
|
|
export default defineComponent({
|
|
name: "Config",
|
|
components: {
|
|
NewDeviceButton,
|
|
BackupRestoreModal,
|
|
ChargerModal,
|
|
CircuitsIcon,
|
|
CircuitsModal,
|
|
CircuitTags,
|
|
ControlModal,
|
|
DeviceCard,
|
|
DeviceTags,
|
|
EebusIcon,
|
|
EebusModal,
|
|
OcppIcon,
|
|
OcppModal,
|
|
GeneralConfig,
|
|
HemsIcon,
|
|
HemsModal,
|
|
ShmModal,
|
|
ShmIcon,
|
|
InfluxIcon,
|
|
InfluxModal,
|
|
MessagingLegacyModal,
|
|
MessagingModal,
|
|
MessengerModal,
|
|
MeterModal,
|
|
MeterCard,
|
|
LoadpointModal,
|
|
LoadpointIcon,
|
|
ModbusProxyIcon,
|
|
ModbusProxyModal,
|
|
MqttIcon,
|
|
MqttModal,
|
|
RemoteAccessIcon,
|
|
RemoteModal,
|
|
NetworkModal,
|
|
NotificationIcon,
|
|
OptimizerIcon,
|
|
OptimizerModal,
|
|
McpIcon,
|
|
McpModal,
|
|
SponsorModal,
|
|
TariffsLegacyModal,
|
|
TariffCard,
|
|
TariffModal,
|
|
TelemetryModal,
|
|
ExperimentalModal,
|
|
TitleModal,
|
|
TopHeader: Header,
|
|
VehicleIcon,
|
|
VehicleModal,
|
|
WelcomeBanner,
|
|
AuthSuccessBanner,
|
|
PasswordModal,
|
|
SecurityModal,
|
|
ApiKeyModal,
|
|
AuthProvidersCard,
|
|
},
|
|
mixins: [formatter, collector],
|
|
props: {
|
|
offline: Boolean,
|
|
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
|
},
|
|
data() {
|
|
return {
|
|
messengers: [] as ConfigMessenger[],
|
|
vehicles: [] as ConfigVehicle[],
|
|
meters: [] as ConfigMeter[],
|
|
loadpoints: [] as ConfigLoadpoint[],
|
|
chargers: [] as ConfigCharger[],
|
|
circuits: [] as ConfigCircuit[],
|
|
tariffs: [] as any[], // ConfigTariff[] - tariff device entities
|
|
tariffRefs: {
|
|
grid: "",
|
|
feedIn: "",
|
|
co2: "",
|
|
planner: "",
|
|
solar: [] as string[],
|
|
},
|
|
site: {
|
|
grid: "",
|
|
pv: [] as string[],
|
|
battery: [] as string[],
|
|
title: "",
|
|
aux: null as string[] | null,
|
|
ext: null as string[] | null,
|
|
} as SiteConfig,
|
|
deviceValueTimeout: null as Timeout,
|
|
deviceValues: {
|
|
meter: {},
|
|
vehicle: {},
|
|
charger: {},
|
|
loadpoint: {},
|
|
messenger: {},
|
|
tariff: {},
|
|
} as DeviceValuesMap,
|
|
isComponentMounted: true,
|
|
isPageVisible: true,
|
|
};
|
|
},
|
|
head() {
|
|
return { title: this.$t("config.main.title") };
|
|
},
|
|
computed: {
|
|
callbackCompleted() {
|
|
return this.$route.query["callbackCompleted"] as string | undefined;
|
|
},
|
|
callbackError() {
|
|
return this.$route.query["callbackError"] as string | undefined;
|
|
},
|
|
authProviders() {
|
|
return store.state?.authProviders;
|
|
},
|
|
setupRequired() {
|
|
return store.state?.setupRequired;
|
|
},
|
|
currency(): CURRENCY {
|
|
return store.state?.currency ?? CURRENCY.EUR;
|
|
},
|
|
siteTitle() {
|
|
return this.site?.title;
|
|
},
|
|
gridMeter() {
|
|
const name = this.site?.grid;
|
|
return this.getMetersByNames([name])[0];
|
|
},
|
|
pvMeters() {
|
|
const names = this.site?.pv;
|
|
return this.getMetersByNames(names);
|
|
},
|
|
batteryMeters() {
|
|
const names = this.site?.battery;
|
|
return this.getMetersByNames(names);
|
|
},
|
|
auxMeters() {
|
|
const names = this.site?.aux;
|
|
return this.getMetersByNames(names);
|
|
},
|
|
extMeters() {
|
|
const names = this.site?.ext;
|
|
return this.getMetersByNames(names);
|
|
},
|
|
gridTariff() {
|
|
const name = this.tariffRefs?.grid;
|
|
return name ? this.tariffs.find((t) => t.name === name) : null;
|
|
},
|
|
feedInTariff() {
|
|
const name = this.tariffRefs?.feedIn;
|
|
return name ? this.tariffs.find((t) => t.name === name) : null;
|
|
},
|
|
co2Tariff() {
|
|
const name = this.tariffRefs?.co2;
|
|
return name ? this.tariffs.find((t) => t.name === name) : null;
|
|
},
|
|
plannerTariff() {
|
|
const name = this.tariffRefs?.planner;
|
|
return name ? this.tariffs.find((t) => t.name === name) : null;
|
|
},
|
|
solarTariffs() {
|
|
const names = this.tariffRefs?.solar || [];
|
|
return names.map((name) => this.tariffs.find((t) => t.name === name)).filter(Boolean);
|
|
},
|
|
possibleTariffTypes(): TariffType[] {
|
|
const types: TariffType[] = [];
|
|
if (!this.gridTariff) types.push("grid");
|
|
if (!this.feedInTariff) types.push("feedIn");
|
|
return types;
|
|
},
|
|
possibleForecastTypes(): TariffType[] {
|
|
const types: TariffType[] = [];
|
|
if (!this.co2Tariff) types.push("co2");
|
|
types.push("solar"); // Solar can have multiple
|
|
if (!this.plannerTariff) types.push("planner");
|
|
return types;
|
|
},
|
|
tariffTags(): DeviceTags {
|
|
const { tariffGrid, tariffFeedIn, tariffCo2, tariffSolar } = store.state;
|
|
if (
|
|
tariffGrid === undefined &&
|
|
tariffFeedIn === undefined &&
|
|
tariffCo2 === undefined &&
|
|
tariffSolar === undefined
|
|
) {
|
|
return { configured: { value: false } };
|
|
}
|
|
const tags = {
|
|
gridPrice: {},
|
|
feedinPrice: {},
|
|
co2: {},
|
|
solarForecast: {},
|
|
};
|
|
if (tariffGrid) {
|
|
tags.gridPrice = { value: tariffGrid };
|
|
}
|
|
if (tariffFeedIn) {
|
|
tags.feedinPrice = { value: tariffFeedIn * -1 };
|
|
}
|
|
if (tariffCo2) {
|
|
tags.co2 = { value: tariffCo2 };
|
|
}
|
|
if (tariffSolar) {
|
|
tags.solarForecast = { value: tariffSolar };
|
|
}
|
|
return tags;
|
|
},
|
|
mqttTags(): DeviceTags {
|
|
const { broker, topic } = store.state?.mqtt || {};
|
|
if (!broker) return { configured: { value: false } };
|
|
return {
|
|
broker: { value: broker },
|
|
topic: { value: topic },
|
|
};
|
|
},
|
|
influxTags(): DeviceTags {
|
|
const { url, database, org } = store.state?.influx || {};
|
|
if (!url) return { configured: { value: false } };
|
|
const result = { url: { value: url }, bucket: {}, org: {} };
|
|
if (database) result.bucket = { value: database };
|
|
if (org) result.org = { value: org };
|
|
return result;
|
|
},
|
|
vehicleOptions(): VehicleOption[] {
|
|
return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name }));
|
|
},
|
|
hems() {
|
|
return store.state?.hems;
|
|
},
|
|
hemsTags(): DeviceTags {
|
|
const type = this.hems?.config?.type;
|
|
if (!type) {
|
|
return { configured: { value: false } };
|
|
}
|
|
const result = {
|
|
hemsType: {},
|
|
hemsActiveLimit: { value: null as number | null },
|
|
};
|
|
if (["relay", "eebus"].includes(type)) {
|
|
result.hemsType = { value: type };
|
|
}
|
|
const gc = store.state?.circuits?.[GRID_CONTROL];
|
|
if (gc) {
|
|
const value = gc.maxPower || null;
|
|
result.hemsActiveLimit = { value };
|
|
}
|
|
|
|
return result;
|
|
},
|
|
remote(): Remote | undefined {
|
|
return store.state?.remote;
|
|
},
|
|
remoteTags(): DeviceTags {
|
|
const remote = this.remote;
|
|
if (!remote?.status?.url) {
|
|
return { configured: { value: false } };
|
|
}
|
|
const tags: DeviceTags = {
|
|
enabled: { value: remote.config?.enabled },
|
|
connected: { value: remote.status?.connected },
|
|
};
|
|
if (remote.status?.loginBlocked) {
|
|
tags["loginBlocked"] = { value: true, error: true };
|
|
}
|
|
if (remote.status?.connected) {
|
|
const lastSeen = remote.status?.lastSeen;
|
|
const count = lastSeen
|
|
? Object.keys(lastSeen).filter((u) => isRemoteClientActive(lastSeen, u)).length
|
|
: 0;
|
|
tags["activeClients"] = { value: count };
|
|
}
|
|
return tags;
|
|
},
|
|
sponsor() {
|
|
return store.state?.sponsor;
|
|
},
|
|
isSponsor(): boolean {
|
|
return !!this.sponsor?.status?.name;
|
|
},
|
|
ocpp() {
|
|
return store.state?.ocpp;
|
|
},
|
|
telemetry() {
|
|
return store.state?.telemetry;
|
|
},
|
|
experimental() {
|
|
return store.state?.experimental;
|
|
},
|
|
eebus() {
|
|
return store.state?.eebus;
|
|
},
|
|
optimizerTags(): DeviceTags {
|
|
if (!store.state?.optimizer) return { configured: { value: false } };
|
|
return { configured: { value: true } };
|
|
},
|
|
modbusproxyTags(): DeviceTags {
|
|
const config = store.state?.modbusproxy || [];
|
|
if (config.length > 0) {
|
|
return { amount: { value: config.length } };
|
|
}
|
|
return { configured: { value: false } };
|
|
},
|
|
messagingTags(): DeviceTags {
|
|
if (this.messagingUiConfigured) {
|
|
const events = store.state?.messagingEvents || [];
|
|
const enabledEvents = Object.values(events).filter((e: any) => !e.disabled).length;
|
|
return {
|
|
events: { value: enabledEvents },
|
|
messengers: { value: this.messengers.length },
|
|
};
|
|
}
|
|
return { configured: { value: this.messagingYamlConfigured } };
|
|
},
|
|
messagingYamlSource() {
|
|
return store.state.messaging?.yamlSource;
|
|
},
|
|
messagingYamlConfigured() {
|
|
return this.messagingYamlSource === "file" || this.messagingYamlSource === "db";
|
|
},
|
|
messagingUiConfigured() {
|
|
return (
|
|
this.messengers.length > 0 ||
|
|
Object.values(store.state.messagingEvents ?? {}).some((e) => !e.disabled)
|
|
);
|
|
},
|
|
authDisabled() {
|
|
return store.state?.authDisabled || false;
|
|
},
|
|
backupRestoreProps() {
|
|
return {
|
|
authDisabled: this.authDisabled,
|
|
};
|
|
},
|
|
circuitsRoot() {
|
|
return circuitTree(store.state?.circuits || {});
|
|
},
|
|
tariffsYamlSource() {
|
|
return store.state?.tariffs?.yamlSource;
|
|
},
|
|
tariffsUiVisible() {
|
|
return this.tariffsYamlSource === undefined;
|
|
},
|
|
tariffsYamlVisible() {
|
|
return !this.tariffsUiVisible;
|
|
},
|
|
tariffsYamlDisabled() {
|
|
return this.tariffsYamlSource === "file";
|
|
},
|
|
},
|
|
watch: {
|
|
offline() {
|
|
if (!this.offline) {
|
|
this.loadAll();
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.isComponentMounted = true;
|
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
this.isPageVisible = document.visibilityState === "visible";
|
|
this.loadAll();
|
|
},
|
|
unmounted() {
|
|
this.isComponentMounted = false;
|
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
if (this.deviceValueTimeout) {
|
|
clearTimeout(this.deviceValueTimeout);
|
|
}
|
|
},
|
|
methods: {
|
|
isUnconfigured(tags: DeviceTags): boolean {
|
|
return tags["configured"]?.value === false;
|
|
},
|
|
handleVisibilityChange() {
|
|
this.isPageVisible = document.visibilityState === "visible";
|
|
if (this.isPageVisible) {
|
|
this.updateValues();
|
|
} else if (this.deviceValueTimeout) {
|
|
clearTimeout(this.deviceValueTimeout);
|
|
}
|
|
},
|
|
async loadAll() {
|
|
await this.loadVehicles();
|
|
await this.loadMeters();
|
|
await this.loadSite();
|
|
await this.loadChargers();
|
|
await this.loadLoadpoints();
|
|
await this.loadCircuits();
|
|
await this.loadMessengers();
|
|
await this.loadTariffs();
|
|
await this.loadTariffRefs();
|
|
await this.loadDirty();
|
|
this.updateValues();
|
|
},
|
|
async loadDirty() {
|
|
const data = await this.loadConfig("dirty");
|
|
if (data) {
|
|
restart.restartNeeded = true;
|
|
}
|
|
},
|
|
async loadConfig(path: string) {
|
|
const validateStatus = (code: number) => [200, 404].includes(code);
|
|
const response = await api.get(`/config/${path}`, { validateStatus });
|
|
return response.status === 200 ? response.data : undefined;
|
|
},
|
|
async loadMessengers() {
|
|
this.messengers = (await this.loadConfig("devices/messenger")) || [];
|
|
},
|
|
async loadVehicles() {
|
|
this.vehicles = (await this.loadConfig("devices/vehicle")) || [];
|
|
},
|
|
async loadChargers() {
|
|
this.chargers = (await this.loadConfig("devices/charger")) || [];
|
|
},
|
|
async loadMeters() {
|
|
this.meters = (await this.loadConfig("devices/meter")) || [];
|
|
},
|
|
async loadCircuits() {
|
|
const circuits = (await this.loadConfig("devices/circuit")) || [];
|
|
// gridcontrol is auto-created for hems and must not be user-assignable to loadpoints
|
|
this.circuits = circuits.filter((c: ConfigCircuit) => c.name !== GRID_CONTROL);
|
|
},
|
|
async loadTariffs() {
|
|
this.tariffs = (await this.loadConfig("devices/tariff")) || [];
|
|
},
|
|
async loadTariffRefs() {
|
|
const response = await api.get("/config/tariff", {
|
|
validateStatus: (code: number) => [200, 404].includes(code),
|
|
});
|
|
if (response.status === 200) {
|
|
this.tariffRefs = response.data;
|
|
if (!this.tariffRefs.solar) this.tariffRefs.solar = [];
|
|
}
|
|
},
|
|
async loadSite() {
|
|
const data = await this.loadConfig("site");
|
|
if (data) {
|
|
this.site = data;
|
|
}
|
|
},
|
|
async loadLoadpoints() {
|
|
this.loadpoints = (await this.loadConfig("loadpoints")) || [];
|
|
},
|
|
getMetersByNames(names: string[] | null): ConfigMeter[] {
|
|
if (!names || !this.meters) {
|
|
return [];
|
|
}
|
|
return names
|
|
.map((name) => this.meters.find((m) => m.name === name))
|
|
.filter((m): m is ConfigMeter => m !== undefined);
|
|
},
|
|
async meterChanged(result: ModalResult) {
|
|
// Added: update site config
|
|
if (result.action === "added") {
|
|
const type = result.type as MeterType;
|
|
const name = result.name!;
|
|
|
|
switch (type) {
|
|
case "grid":
|
|
this.site.grid = name;
|
|
this.saveSite(type);
|
|
break;
|
|
case "pv":
|
|
if (!this.site.pv) this.site.pv = [];
|
|
this.site.pv.push(name);
|
|
this.saveSite(type);
|
|
break;
|
|
case "battery":
|
|
if (!this.site.battery) this.site.battery = [];
|
|
this.site.battery.push(name);
|
|
this.saveSite(type);
|
|
break;
|
|
case "aux":
|
|
if (!this.site.aux) this.site.aux = [];
|
|
this.site.aux.push(name);
|
|
this.saveSite(type);
|
|
break;
|
|
case "ext":
|
|
if (!this.site.ext) this.site.ext = [];
|
|
this.site.ext.push(name);
|
|
this.saveSite(type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Removed: reload site config
|
|
if (result.action === "removed") {
|
|
await this.loadSite();
|
|
}
|
|
|
|
// Reload meters and update UI
|
|
await this.loadMeters();
|
|
await this.loadDirty();
|
|
this.updateValues();
|
|
},
|
|
async chargerChanged() {
|
|
await this.loadChargers();
|
|
await this.loadDirty();
|
|
this.updateValues();
|
|
},
|
|
async loadpointChanged() {
|
|
await this.loadLoadpoints();
|
|
this.loadDirty();
|
|
},
|
|
async loadpointDismissed() {
|
|
await this.loadChargers();
|
|
await this.loadMeters();
|
|
this.updateValues();
|
|
},
|
|
vehicleChanged() {
|
|
this.loadVehicles();
|
|
this.loadDirty();
|
|
},
|
|
async tariffChanged(result: ModalResult) {
|
|
if (result.action === "added") {
|
|
const usage = result.type as TariffType;
|
|
const name = result.name!;
|
|
if (usage === "solar") {
|
|
this.tariffRefs.solar.push(name);
|
|
} else {
|
|
this.tariffRefs[usage] = name;
|
|
}
|
|
await api.put("/config/tariff", this.tariffRefs);
|
|
}
|
|
if (result.action === "removed") {
|
|
await this.loadTariffRefs();
|
|
}
|
|
await this.loadTariffs();
|
|
await this.loadTariffRefs();
|
|
await this.loadDirty();
|
|
this.updateValues();
|
|
},
|
|
openMessagingModal() {
|
|
const modalName = this.messagingYamlSource === "db" ? "messaginglegacy" : "messaging";
|
|
openModal(modalName);
|
|
},
|
|
async messengerChanged() {
|
|
this.loadMessengers();
|
|
this.loadDirty();
|
|
},
|
|
siteChanged() {
|
|
this.loadDirty();
|
|
},
|
|
async saveSite(key: keyof SiteConfig) {
|
|
const body = key ? { [key]: this.site[key] } : this.site;
|
|
await api.put("/config/site", body);
|
|
await this.loadSite();
|
|
await this.loadDirty();
|
|
this.updateValues();
|
|
},
|
|
todo() {
|
|
alert("not implemented yet");
|
|
},
|
|
async restart() {
|
|
await performRestart();
|
|
},
|
|
async updateDeviceValue(type: DeviceType, name: string) {
|
|
try {
|
|
const validateStatus = (status: number) => [200, 404].includes(status);
|
|
const response = await api.get(`/config/devices/${type}/${name}/status`, {
|
|
validateStatus,
|
|
});
|
|
if (response.status === 200) {
|
|
if (!this.deviceValues[type]) this.deviceValues[type] = {};
|
|
this.deviceValues[type][name] = response.data;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching device values for", type, name, error);
|
|
}
|
|
},
|
|
async updateValues() {
|
|
if (this.deviceValueTimeout) {
|
|
clearTimeout(this.deviceValueTimeout);
|
|
}
|
|
if (!this.offline) {
|
|
const devices = {
|
|
meter: this.meters,
|
|
vehicle: this.vehicles,
|
|
charger: this.chargers,
|
|
tariff: this.tariffs,
|
|
} as Record<DeviceType, any[]>;
|
|
for (const type in devices) {
|
|
for (const device of devices[type as DeviceType]) {
|
|
if (this.isComponentMounted && this.isPageVisible) {
|
|
await this.updateDeviceValue(type as DeviceType, device.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isComponentMounted && this.isPageVisible) {
|
|
const interval = (store.state?.interval || 30) * 1000;
|
|
this.deviceValueTimeout = setTimeout(this.updateValues, interval);
|
|
}
|
|
},
|
|
deviceTags(type: DeviceType, id: string) {
|
|
return this.deviceValues[type][id] || {};
|
|
},
|
|
loadpointTags(loadpoint: ConfigLoadpoint) {
|
|
const { charger, meter } = loadpoint;
|
|
const chargerTags = charger ? this.deviceTags("charger", charger) : {};
|
|
const meterTags = meter ? this.deviceTags("meter", meter) : {};
|
|
return { ...chargerTags, ...meterTags };
|
|
},
|
|
openModal,
|
|
hasDeviceError(type: DeviceType, name?: string) {
|
|
if (!name) return false;
|
|
const fatals = store.state?.fatal || [];
|
|
return fatals.some((fatal) => fatal.class === type && fatal.device === name);
|
|
},
|
|
hasClassError(className: string) {
|
|
const fatals = store.state?.fatal || [];
|
|
return fatals.some((fatal) => fatal.class === className);
|
|
},
|
|
chargerIcon(chargerName: string) {
|
|
const charger = this.chargers.find((c) => c.name === chargerName);
|
|
|
|
return charger?.config?.icon || this.deviceValues["charger"][chargerName]?.icon?.value;
|
|
},
|
|
handleProviderAuthRequest(providerId: string) {
|
|
const header = this.$refs["header"] as InstanceType<typeof Header> | undefined;
|
|
header?.requestAuthProvider(providerId);
|
|
},
|
|
},
|
|
}) as any;
|
|
</script>
|
|
<style scoped>
|
|
.config-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
grid-gap: 2rem;
|
|
margin-bottom: 5rem;
|
|
}
|
|
.wip {
|
|
opacity: 0.2 !important;
|
|
display: none !important;
|
|
}
|
|
</style>
|