857 lines
25 KiB
Vue
857 lines
25 KiB
Vue
<template>
|
|
<div class="root safe-area-inset">
|
|
<div class="container px-4">
|
|
<TopHeader :title="$t('config.main.title')" />
|
|
<div class="wrapper pb-5">
|
|
<div v-if="$hiddenFeatures()" class="alert alert-danger my-4 pb-0" role="alert">
|
|
<p>
|
|
<strong>Experimental! 🧪</strong>
|
|
Only use these features if you are in the mood for adventure and not afraid
|
|
of debugging. Unexpected things and data loss may happen.
|
|
</p>
|
|
<p>
|
|
We are in the progress of replacing <code>evcc.yaml</code> with UI-based
|
|
configuration. Any changes made here will be written to the database. After
|
|
that, the corresponding <code>evcc.yaml</code>-values (e.g. network
|
|
settings) will be ignored.
|
|
</p>
|
|
<p class="mb-1"><strong>Missing features</strong></p>
|
|
<ul>
|
|
<li>aux meters</li>
|
|
<li>custom/plugin meters and vehicles</li>
|
|
<li>migration for vehicles, chargers, meters, loadpoints</li>
|
|
<li>remove mixed mode (evcc.yaml + db) for meters and vehicles</li>
|
|
</ul>
|
|
<p>
|
|
<strong>Migration and repair.</strong> Run <code>evcc migrate</code> to copy
|
|
configuration from <code>evcc.yaml</code> to the database. Existing database
|
|
configurations will be overwritten. Session and statistics data will not be
|
|
touched. Run <code>evcc migrate --reset</code> to remove all database
|
|
configurations.
|
|
</p>
|
|
</div>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2>
|
|
<GeneralConfig @site-changed="siteChanged" />
|
|
|
|
<div v-if="$hiddenFeatures()">
|
|
<h2 class="my-4">{{ $t("config.section.loadpoints") }} 🧪</h2>
|
|
<ul class="p-0 config-list">
|
|
<DeviceCard
|
|
v-for="loadpoint in loadpoints"
|
|
:key="loadpoint.name"
|
|
:name="loadpoint.title"
|
|
:editable="!!loadpoint.id"
|
|
data-testid="loadpoint"
|
|
@edit="editLoadpoint(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="newLoadpoint"
|
|
/>
|
|
</ul>
|
|
|
|
<h2 class="my-4">{{ $t("config.section.vehicles") }} 🧪</h2>
|
|
<ul class="p-0 config-list">
|
|
<DeviceCard
|
|
v-for="vehicle in vehicles"
|
|
:key="vehicle.name"
|
|
:name="vehicle.config?.title || vehicle.name"
|
|
:editable="vehicle.id >= 0"
|
|
:error="deviceError('vehicle', vehicle.name)"
|
|
data-testid="vehicle"
|
|
@edit="editVehicle(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="newVehicle"
|
|
/>
|
|
</ul>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.grid") }} 🧪</h2>
|
|
<ul class="p-0 config-list">
|
|
<DeviceCard
|
|
v-if="gridMeter?.id"
|
|
:name="$t('config.grid.title')"
|
|
editable
|
|
:error="deviceError('meter', gridMeter.name)"
|
|
data-testid="grid"
|
|
@edit="editMeter(gridMeter.id, 'grid')"
|
|
>
|
|
<template #icon>
|
|
<shopicon-regular-powersupply></shopicon-regular-powersupply>
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="deviceTags('meter', gridMeter.name)" />
|
|
</template>
|
|
</DeviceCard>
|
|
<NewDeviceButton
|
|
v-else
|
|
:title="$t('config.main.addGrid')"
|
|
data-testid="add-grid"
|
|
@click="newMeter('grid')"
|
|
/>
|
|
<DeviceCard
|
|
v-if="tariffTags"
|
|
:name="$t('config.tariffs.title')"
|
|
editable
|
|
:error="fatalClass === 'tariff'"
|
|
data-testid="tariffs"
|
|
@edit="openModal('tariffsModal')"
|
|
>
|
|
<template #icon>
|
|
<shopicon-regular-receivepayment></shopicon-regular-receivepayment>
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="tariffTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<NewDeviceButton
|
|
v-else
|
|
:title="$t('config.main.addTariffs')"
|
|
data-testid="add-tariffs"
|
|
@click="openModal('tariffsModal')"
|
|
/>
|
|
</ul>
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.meter") }} 🧪</h2>
|
|
<ul class="p-0 config-list">
|
|
<DeviceCard
|
|
v-for="meter in pvMeters"
|
|
:key="meter.name"
|
|
:name="meter.config?.template || 'Solar system'"
|
|
:editable="!!meter.id"
|
|
:error="deviceError('meter', meter.name)"
|
|
data-testid="pv"
|
|
@edit="editMeter(meter.id, 'pv')"
|
|
>
|
|
<template #icon>
|
|
<shopicon-regular-sun></shopicon-regular-sun>
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="deviceTags('meter', meter.name)" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
v-for="meter in batteryMeters"
|
|
:key="meter.name"
|
|
:name="meter.config?.template || 'Battery storage'"
|
|
:editable="!!meter.id"
|
|
:error="deviceError('meter', meter.name)"
|
|
data-testid="battery"
|
|
@edit="editMeter(meter.id, 'battery')"
|
|
>
|
|
<template #icon>
|
|
<shopicon-regular-batterythreequarters></shopicon-regular-batterythreequarters>
|
|
</template>
|
|
<template #tags>
|
|
<DeviceTags :tags="deviceTags('meter', meter.name)" />
|
|
</template>
|
|
</DeviceCard>
|
|
<NewDeviceButton
|
|
:title="$t('config.main.addPvBattery')"
|
|
@click="newMeter"
|
|
/>
|
|
</ul>
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.integrations") }} 🧪</h2>
|
|
|
|
<ul class="p-0 config-list">
|
|
<DeviceCard
|
|
:name="$t('config.mqtt.title')"
|
|
editable
|
|
:error="fatalClass === 'mqtt'"
|
|
data-testid="mqtt"
|
|
@edit="openModal('mqttModal')"
|
|
>
|
|
<template #icon><MqttIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="mqttTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:name="$t('config.messaging.title')"
|
|
editable
|
|
:error="fatalClass === 'messenger'"
|
|
data-testid="messaging"
|
|
@edit="openModal('messagingModal')"
|
|
>
|
|
<template #icon><NotificationIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="messagingTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:name="$t('config.influx.title')"
|
|
editable
|
|
:error="fatalClass === 'influx'"
|
|
data-testid="influx"
|
|
@edit="openModal('influxModal')"
|
|
>
|
|
<template #icon><InfluxIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="influxTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:name="`${$t('config.eebus.title')} 🧪`"
|
|
editable
|
|
:error="fatalClass === 'eebus'"
|
|
data-testid="eebus"
|
|
@edit="openModal('eebusModal')"
|
|
>
|
|
<template #icon><EebusIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="eebusTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
|
|
<DeviceCard
|
|
:name="`${$t('config.circuits.title')} 🧪`"
|
|
editable
|
|
:error="fatalClass === 'circuit'"
|
|
data-testid="circuits"
|
|
@edit="openModal('circuitsModal')"
|
|
>
|
|
<template #icon><CircuitsIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags
|
|
v-if="circuits.length == 0"
|
|
:tags="{ configured: { value: false } }"
|
|
/>
|
|
<template
|
|
v-for="(circuit, idx) in circuits"
|
|
v-else
|
|
:key="circuit.name"
|
|
>
|
|
<hr v-if="idx > 0" />
|
|
<p class="my-2 fw-bold">
|
|
{{ circuit.config?.title }}
|
|
<code>({{ circuit.name }})</code>
|
|
</p>
|
|
<DeviceTags :tags="circuitTags(circuit)" />
|
|
</template>
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:name="$t('config.modbusproxy.title')"
|
|
editable
|
|
:error="fatalClass === 'modbusproxy'"
|
|
data-testid="modbusproxy"
|
|
@edit="openModal('modbusProxyModal')"
|
|
>
|
|
<template #icon><ModbusProxyIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="yamlTags('modbusproxy')" />
|
|
</template>
|
|
</DeviceCard>
|
|
<DeviceCard
|
|
:name="$t('config.hems.title')"
|
|
editable
|
|
:error="fatalClass === 'hems'"
|
|
data-testid="hems"
|
|
@edit="openModal('hemsModal')"
|
|
>
|
|
<template #icon><HemsIcon /></template>
|
|
<template #tags>
|
|
<DeviceTags :tags="hemsTags" />
|
|
</template>
|
|
</DeviceCard>
|
|
</ul>
|
|
</div>
|
|
|
|
<hr class="my-5" />
|
|
|
|
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
|
|
<div class="round-box p-4 d-flex gap-4 mb-5">
|
|
<router-link to="/log" class="btn btn-outline-secondary">
|
|
{{ $t("config.system.logs") }}
|
|
</router-link>
|
|
<button class="btn btn-outline-danger" @click="restart">
|
|
{{ $t("config.system.restart") }}
|
|
</button>
|
|
</div>
|
|
|
|
<LoadpointModal
|
|
:id="selectedLoadpointId"
|
|
ref="loadpointModal"
|
|
:vehicleOptions="vehicleOptions"
|
|
:loadpointCount="loadpoints.length"
|
|
:chargers="chargers"
|
|
:chargerValues="deviceValues.charger"
|
|
:meters="meters"
|
|
:circuits="circuits"
|
|
:fade="loadpointSubModalOpen ? 'left' : ''"
|
|
@updated="loadpointChanged"
|
|
@open-charger-modal="editLoadpointCharger"
|
|
@open-meter-modal="editLoadpointMeter"
|
|
@opened="loadpointSubModalOpen = false"
|
|
/>
|
|
<VehicleModal :id="selectedVehicleId" @vehicle-changed="vehicleChanged" />
|
|
<MeterModal
|
|
:id="selectedMeterId"
|
|
:name="selectedMeterName"
|
|
:type="selectedMeterType"
|
|
:fade="loadpointSubModalOpen ? 'right' : ''"
|
|
@added="addMeter"
|
|
@updated="meterChanged"
|
|
@removed="removeMeter"
|
|
@close="meterModalClosed"
|
|
/>
|
|
<ChargerModal
|
|
:id="selectedChargerId"
|
|
:name="selectedChargerName"
|
|
:fade="loadpointSubModalOpen ? 'right' : ''"
|
|
:isSponsor="isSponsor"
|
|
@added="addCharger"
|
|
@updated="chargerChanged"
|
|
@removed="removeCharger"
|
|
@close="chargerModalClosed"
|
|
/>
|
|
<InfluxModal @changed="loadDirty" />
|
|
<MqttModal @changed="loadDirty" />
|
|
<NetworkModal @changed="loadDirty" />
|
|
<ControlModal @changed="loadDirty" />
|
|
<SponsorModal @changed="loadDirty" />
|
|
<HemsModal @changed="yamlChanged" />
|
|
<MessagingModal @changed="yamlChanged" />
|
|
<TariffsModal @changed="yamlChanged" />
|
|
<ModbusProxyModal @changed="yamlChanged" />
|
|
<CircuitsModal @changed="yamlChanged" />
|
|
<EebusModal @changed="yamlChanged" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
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 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 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 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/MessagingModal.vue";
|
|
import MeterModal from "../components/Config/MeterModal.vue";
|
|
import Modal from "bootstrap/js/dist/modal";
|
|
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 NetworkModal from "../components/Config/NetworkModal.vue";
|
|
import NotificationIcon from "../components/MaterialIcon/Notification.vue";
|
|
import restart, { performRestart } from "../restart";
|
|
import SponsorModal from "../components/Config/SponsorModal.vue";
|
|
import store from "../store";
|
|
import TariffsModal from "../components/Config/TariffsModal.vue";
|
|
import TopHeader from "../components/TopHeader.vue";
|
|
import VehicleIcon from "../components/VehicleIcon";
|
|
import VehicleModal from "../components/Config/VehicleModal.vue";
|
|
|
|
export default {
|
|
name: "Config",
|
|
components: {
|
|
NewDeviceButton,
|
|
ChargerModal,
|
|
CircuitsIcon,
|
|
CircuitsModal,
|
|
ControlModal,
|
|
DeviceCard,
|
|
DeviceTags,
|
|
EebusIcon,
|
|
EebusModal,
|
|
GeneralConfig,
|
|
HemsIcon,
|
|
HemsModal,
|
|
InfluxIcon,
|
|
InfluxModal,
|
|
MessagingModal,
|
|
MeterModal,
|
|
LoadpointModal,
|
|
LoadpointIcon,
|
|
ModbusProxyIcon,
|
|
ModbusProxyModal,
|
|
MqttIcon,
|
|
MqttModal,
|
|
NetworkModal,
|
|
NotificationIcon,
|
|
SponsorModal,
|
|
TariffsModal,
|
|
TopHeader,
|
|
VehicleIcon,
|
|
VehicleModal,
|
|
},
|
|
mixins: [formatter, collector],
|
|
props: {
|
|
offline: Boolean,
|
|
notifications: Array,
|
|
},
|
|
data() {
|
|
return {
|
|
vehicles: [],
|
|
meters: [],
|
|
loadpoints: [],
|
|
chargers: [],
|
|
circuits: [],
|
|
selectedVehicleId: undefined,
|
|
selectedMeterId: undefined,
|
|
selectedMeterType: undefined,
|
|
selectedChargerId: undefined,
|
|
selectedLoadpointId: undefined,
|
|
loadpointSubModalOpen: false,
|
|
site: { grid: "", pv: [], battery: [], title: "" },
|
|
deviceValueTimeout: undefined,
|
|
deviceValues: {},
|
|
};
|
|
},
|
|
computed: {
|
|
fatalClass() {
|
|
return store.state?.fatal?.class;
|
|
},
|
|
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);
|
|
},
|
|
selectedMeterName() {
|
|
return this.getMeterById(this.selectedMeterId)?.name;
|
|
},
|
|
selectedChargerName() {
|
|
return this.getChargerById(this.selectedChargerId)?.name;
|
|
},
|
|
tariffTags() {
|
|
const { currency, tariffGrid, tariffFeedIn, tariffCo2 } = store.state;
|
|
if (tariffGrid === undefined && tariffFeedIn === undefined && tariffCo2 === undefined) {
|
|
return null;
|
|
}
|
|
const tags = {};
|
|
if (currency) {
|
|
tags.currency = { value: currency };
|
|
}
|
|
if (tariffGrid) {
|
|
tags.gridPrice = { value: tariffGrid, options: { currency } };
|
|
}
|
|
if (tariffFeedIn) {
|
|
tags.feedinPrice = { value: tariffFeedIn * -1, options: { currency } };
|
|
}
|
|
if (tariffCo2) {
|
|
tags.co2 = { value: tariffCo2 };
|
|
}
|
|
return tags;
|
|
},
|
|
mqttTags() {
|
|
const { broker, topic } = store.state?.mqtt || {};
|
|
if (!broker) return { configured: { value: false } };
|
|
return {
|
|
broker: { value: broker },
|
|
topic: { value: topic },
|
|
};
|
|
},
|
|
influxTags() {
|
|
const { url, database, org } = store.state?.influx || {};
|
|
if (!url) return { configured: { value: false } };
|
|
const result = { url: { value: url } };
|
|
if (database) result.bucket = { value: database };
|
|
if (org) result.org = { value: org };
|
|
return result;
|
|
},
|
|
vehicleOptions() {
|
|
return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name }));
|
|
},
|
|
hemsTags() {
|
|
const result = { configured: { value: false } };
|
|
const { type } = store.state?.hems || {};
|
|
if (type) {
|
|
result.configured.value = true;
|
|
result.hemsType = { value: type };
|
|
}
|
|
return result;
|
|
},
|
|
isSponsor() {
|
|
const { name } = store.state?.sponsor || {};
|
|
return !!name;
|
|
},
|
|
eebusTags() {
|
|
return { configured: { value: store.state?.eebus || false } };
|
|
},
|
|
modbusproxyTags() {
|
|
const config = store.state?.modbusproxy || [];
|
|
if (config.length > 0) {
|
|
return { amount: { value: config.length } };
|
|
}
|
|
return { configured: { value: false } };
|
|
},
|
|
messagingTags() {
|
|
return { configured: { value: store.state?.messaging || false } };
|
|
},
|
|
},
|
|
watch: {
|
|
offline() {
|
|
if (!this.offline) {
|
|
this.loadAll();
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.loadAll();
|
|
},
|
|
unmounted() {
|
|
clearTimeout(this.deviceValueTimeout);
|
|
},
|
|
methods: {
|
|
async loadAll() {
|
|
await this.loadVehicles();
|
|
await this.loadMeters();
|
|
await this.loadSite();
|
|
await this.loadChargers();
|
|
await this.loadLoadpoints();
|
|
await this.loadCircuits();
|
|
await this.loadDirty();
|
|
await this.updateValues();
|
|
},
|
|
async loadDirty() {
|
|
const response = await api.get("/config/dirty");
|
|
if (response.data?.result) {
|
|
restart.restartNeeded = true;
|
|
}
|
|
},
|
|
async loadVehicles() {
|
|
const response = await api.get("/config/devices/vehicle");
|
|
this.vehicles = response.data?.result || [];
|
|
},
|
|
async loadChargers() {
|
|
const response = await api.get("/config/devices/charger");
|
|
this.chargers = response.data?.result || [];
|
|
},
|
|
async loadMeters() {
|
|
const response = await api.get("/config/devices/meter");
|
|
this.meters = response.data?.result || [];
|
|
},
|
|
async loadCircuits() {
|
|
const response = await api.get("/config/devices/circuit");
|
|
this.circuits = response.data?.result || [];
|
|
},
|
|
async loadSite() {
|
|
const response = await api.get("/config/site", {
|
|
validateStatus: (status) => status < 500,
|
|
});
|
|
if (response.status === 200) {
|
|
this.site = response.data?.result;
|
|
}
|
|
},
|
|
async loadLoadpoints() {
|
|
const response = await api.get("/config/loadpoints");
|
|
this.loadpoints = response.data?.result || [];
|
|
},
|
|
getMetersByNames(names) {
|
|
if (!names || !this.meters) {
|
|
return [];
|
|
}
|
|
return this.meters.filter((m) => names.includes(m.name));
|
|
},
|
|
getMeterById(id) {
|
|
if (!id || !this.meters) {
|
|
return undefined;
|
|
}
|
|
return this.meters.find((m) => m.id === id);
|
|
},
|
|
getChargerById(id) {
|
|
if (!id || !this.chargers) {
|
|
return undefined;
|
|
}
|
|
return this.chargers.find((c) => c.id === id);
|
|
},
|
|
vehicleModal() {
|
|
return Modal.getOrCreateInstance(document.getElementById("vehicleModal"));
|
|
},
|
|
meterModal() {
|
|
return Modal.getOrCreateInstance(document.getElementById("meterModal"));
|
|
},
|
|
loadpointModal() {
|
|
return Modal.getOrCreateInstance(document.getElementById("loadpointModal"));
|
|
},
|
|
chargerModal() {
|
|
return Modal.getOrCreateInstance(document.getElementById("chargerModal"));
|
|
},
|
|
editLoadpointCharger(name) {
|
|
this.loadpointSubModalOpen = true;
|
|
const charger = this.chargers.find((c) => c.name === name);
|
|
if (charger && charger.id === undefined) {
|
|
alert(
|
|
"yaml configured chargers can not be edited. Remove charger from yaml first."
|
|
);
|
|
return;
|
|
}
|
|
this.loadpointModal().hide();
|
|
this.$nextTick(() => this.editCharger(charger?.id));
|
|
},
|
|
editLoadpointMeter(name) {
|
|
this.loadpointSubModalOpen = true;
|
|
const meter = this.meters.find((m) => m.name === name);
|
|
if (meter && meter.id === undefined) {
|
|
alert("yaml configured meters can not be edited. Remove meter from yaml first.");
|
|
return;
|
|
}
|
|
this.loadpointModal().hide();
|
|
this.$nextTick(() => this.editMeter(meter?.id, "charge"));
|
|
},
|
|
editMeter(id, type) {
|
|
this.selectedMeterId = id;
|
|
this.selectedMeterType = type;
|
|
this.$nextTick(() => this.meterModal().show());
|
|
},
|
|
newMeter(type) {
|
|
this.selectedMeterId = undefined;
|
|
this.selectedMeterType = type;
|
|
this.$nextTick(() => this.meterModal().show());
|
|
},
|
|
editCharger(id) {
|
|
this.selectedChargerId = id;
|
|
this.$nextTick(() => this.chargerModal().show());
|
|
},
|
|
newCharger() {
|
|
this.selectedChargerId = undefined;
|
|
this.$nextTick(() => this.chargerModal().show());
|
|
},
|
|
async meterChanged() {
|
|
await this.loadMeters();
|
|
this.meterModal().hide();
|
|
await this.loadDirty();
|
|
await this.updateValues();
|
|
},
|
|
async chargerChanged() {
|
|
await this.loadChargers();
|
|
this.chargerModal().hide();
|
|
await this.loadDirty();
|
|
await this.updateValues();
|
|
},
|
|
editLoadpoint(id) {
|
|
this.selectedLoadpointId = id;
|
|
this.$nextTick(() => this.loadpointModal().show());
|
|
},
|
|
newLoadpoint() {
|
|
this.selectedLoadpointId = undefined;
|
|
this.$nextTick(() => this.loadpointModal().show());
|
|
},
|
|
async loadpointChanged() {
|
|
this.selectedLoadpointId = undefined;
|
|
await this.loadLoadpoints();
|
|
this.loadpointModal().hide();
|
|
this.loadDirty();
|
|
},
|
|
editVehicle(id) {
|
|
this.selectedVehicleId = id;
|
|
this.$nextTick(() => this.vehicleModal().show());
|
|
},
|
|
newVehicle() {
|
|
this.selectedVehicleId = undefined;
|
|
this.$nextTick(() => this.vehicleModal().show());
|
|
},
|
|
vehicleChanged() {
|
|
this.selectedVehicleId = undefined;
|
|
this.vehicleModal().hide();
|
|
this.loadVehicles();
|
|
this.loadDirty();
|
|
},
|
|
siteChanged() {
|
|
this.loadDirty();
|
|
},
|
|
yamlChanged() {
|
|
this.loadDirty();
|
|
},
|
|
addMeter(type, name) {
|
|
if (type === "charge") {
|
|
// update loadpoint
|
|
this.$refs.loadpointModal?.setMeter(name);
|
|
} else if (type === "grid") {
|
|
// update site grid
|
|
this.site.grid = name;
|
|
this.saveSite(type);
|
|
} else {
|
|
// update site pv, battery, aux
|
|
if (!this.site[type]) {
|
|
this.site[type] = [];
|
|
}
|
|
this.site[type].push(name);
|
|
this.saveSite(type);
|
|
}
|
|
},
|
|
removeMeter(type, name) {
|
|
if (type === "charge") {
|
|
// update loadpoint
|
|
this.$refs.loadpointModal?.setMeter(undefined);
|
|
} else if (type === "grid") {
|
|
// update site grid
|
|
this.site.grid = "";
|
|
this.saveSite(type);
|
|
} else {
|
|
// update site pv, battery, aux
|
|
this.site[type] = this.site[type].filter((i) => i !== name);
|
|
this.saveSite(type);
|
|
}
|
|
},
|
|
addCharger(name) {
|
|
this.$refs.loadpointModal?.setCharger(name);
|
|
},
|
|
removeCharger() {
|
|
this.$refs.loadpointModal?.setCharger(undefined);
|
|
},
|
|
meterModalClosed() {
|
|
if (this.selectedMeterType === "charge") {
|
|
// reopen loadpoint modal
|
|
this.loadpointModal().show();
|
|
}
|
|
},
|
|
chargerModalClosed() {
|
|
// reopen loadpoint modal
|
|
this.loadpointModal().show();
|
|
},
|
|
async saveSite(key) {
|
|
const body = key ? { [key]: this.site[key] } : this.site;
|
|
await api.put("/config/site", body);
|
|
await this.loadSite();
|
|
await this.loadDirty();
|
|
await this.updateValues();
|
|
},
|
|
todo() {
|
|
alert("not implemented yet");
|
|
},
|
|
async restart() {
|
|
await performRestart();
|
|
},
|
|
async updateDeviceValue(type, name) {
|
|
try {
|
|
const response = await api.get(`/config/devices/${type}/${name}/status`);
|
|
if (!this.deviceValues[type]) this.deviceValues[type] = {};
|
|
this.deviceValues[type][name] = response.data.result;
|
|
} catch (error) {
|
|
console.error("Error fetching device values for", type, name, error);
|
|
return null;
|
|
}
|
|
},
|
|
async updateValues() {
|
|
clearTimeout(this.deviceValueTimeout);
|
|
if (!this.offline) {
|
|
const devices = {
|
|
meter: this.meters,
|
|
vehicle: this.vehicles,
|
|
charger: this.chargers,
|
|
};
|
|
for (const type in devices) {
|
|
for (const device of devices[type]) {
|
|
await this.updateDeviceValue(type, device.name);
|
|
}
|
|
}
|
|
}
|
|
// ensure that component is still mounted
|
|
if (!this.$el) return;
|
|
const interval = (store.state?.interval || 30) * 1000;
|
|
this.deviceValueTimeout = setTimeout(this.updateValues, interval);
|
|
},
|
|
deviceTags(type, id) {
|
|
return this.deviceValues[type]?.[id] || {};
|
|
},
|
|
loadpointTags(loadpoint) {
|
|
const { charger, meter } = loadpoint;
|
|
const chargerTags = charger ? this.deviceTags("charger", charger) : {};
|
|
const meterTags = meter ? this.deviceTags("meter", meter) : {};
|
|
return { ...chargerTags, ...meterTags };
|
|
},
|
|
openModal(id) {
|
|
const $el = document.getElementById(id);
|
|
if ($el) {
|
|
Modal.getOrCreateInstance($el).show();
|
|
} else {
|
|
console.error(`modal ${id} not found`);
|
|
}
|
|
},
|
|
circuitTags(circuit) {
|
|
const circuits = store.state?.circuits || {};
|
|
const data = circuits[circuit.name] || {};
|
|
const result = {};
|
|
if (data.maxPower) {
|
|
result.powerRange = {
|
|
value: [data.power || 0, data.maxPower],
|
|
warning: data.power >= data.maxPower,
|
|
};
|
|
} else {
|
|
result.power = { value: data.power || 0, muted: true };
|
|
}
|
|
if (data.maxCurrent) {
|
|
result.currentRange = {
|
|
value: [data.current || 0, data.maxCurrent],
|
|
warning: data.current >= data.maxCurrent,
|
|
};
|
|
}
|
|
return result;
|
|
},
|
|
deviceError(type, name) {
|
|
const fatal = store.state?.fatal || {};
|
|
return fatal.class === type && fatal.device === name;
|
|
},
|
|
chargerIcon(chargerName) {
|
|
const charger = this.chargers.find((c) => c.name === chargerName);
|
|
return charger?.config?.icon;
|
|
},
|
|
},
|
|
};
|
|
</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>
|