Config UI: add missing config sections / partial yaml inputs (BC) (#13319)
This commit is contained in:
parent
62a1e223b6
commit
554ce1db60
133 changed files with 4405 additions and 1572 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ __debug_bin*
|
|||
!templates/**/*.yaml
|
||||
!templates/**/*-schema.json
|
||||
!util/templates/**/*.yaml
|
||||
!assets/js/**/*.yaml
|
||||
!package*.json
|
||||
!evcc.dist.yaml
|
||||
!tests/**/*.evcc.yaml
|
||||
|
|
|
|||
142
api/globalconfig/types.go
Normal file
142
api/globalconfig/types.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package globalconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/charger/eebus"
|
||||
"github.com/evcc-io/evcc/provider/mqtt"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
)
|
||||
|
||||
type All struct {
|
||||
Network Network
|
||||
Log string
|
||||
SponsorToken string
|
||||
Plant string // telemetry plant id
|
||||
Telemetry bool
|
||||
Metrics bool
|
||||
Profile bool
|
||||
Levels map[string]string
|
||||
Interval time.Duration
|
||||
Database DB
|
||||
Mqtt Mqtt
|
||||
ModbusProxy []ModbusProxy
|
||||
Javascript []Javascript
|
||||
Go []Go
|
||||
Influx Influx
|
||||
EEBus eebus.Config
|
||||
HEMS config.Typed
|
||||
Messaging Messaging
|
||||
Meters []config.Named
|
||||
Chargers []config.Named
|
||||
Vehicles []config.Named
|
||||
Tariffs Tariffs
|
||||
Site map[string]interface{}
|
||||
Loadpoints []map[string]interface{}
|
||||
Circuits []config.Named
|
||||
}
|
||||
|
||||
type Javascript struct {
|
||||
VM string
|
||||
Script string
|
||||
}
|
||||
|
||||
type Go struct {
|
||||
VM string
|
||||
Script string
|
||||
}
|
||||
|
||||
type ModbusProxy struct {
|
||||
Port int
|
||||
ReadOnly string
|
||||
modbus.Settings `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type Mqtt struct {
|
||||
mqtt.Config `mapstructure:",squash"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// Redacted implements the redactor interface used by the tee publisher
|
||||
func (m Mqtt) Redacted() any {
|
||||
// TODO add masked password
|
||||
return struct {
|
||||
Broker string `json:"broker"`
|
||||
Topic string `json:"topic"`
|
||||
User string `json:"user"`
|
||||
ClientID string `json:"clientID"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}{
|
||||
Broker: m.Broker,
|
||||
Topic: m.Topic,
|
||||
User: m.User,
|
||||
ClientID: m.ClientID,
|
||||
Insecure: m.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
// Influx is the influx db configuration
|
||||
type Influx struct {
|
||||
URL string `json:"url"`
|
||||
Database string `json:"database"`
|
||||
Token string `json:"token"`
|
||||
Org string `json:"org"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Redacted implements the redactor interface used by the tee publisher
|
||||
func (c Influx) Redacted() any {
|
||||
// TODO add masked password
|
||||
return struct {
|
||||
URL string `json:"url"`
|
||||
Database string `json:"database"`
|
||||
Org string `json:"org"`
|
||||
User string `json:"user"`
|
||||
}{
|
||||
URL: c.URL,
|
||||
Database: c.Database,
|
||||
Org: c.Org,
|
||||
User: c.User,
|
||||
}
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
Type string
|
||||
Dsn string
|
||||
}
|
||||
|
||||
type Messaging struct {
|
||||
Events map[string]push.EventTemplateConfig
|
||||
Services []config.Typed
|
||||
}
|
||||
|
||||
type Tariffs struct {
|
||||
Currency string
|
||||
Grid config.Typed
|
||||
FeedIn config.Typed
|
||||
Co2 config.Typed
|
||||
Planner config.Typed
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
Schema string `json:"schema"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (c Network) HostPort() string {
|
||||
if c.Schema == "http" && c.Port == 80 || c.Schema == "https" && c.Port == 443 {
|
||||
return c.Host
|
||||
}
|
||||
return net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
||||
}
|
||||
|
||||
func (c Network) URI() string {
|
||||
return fmt.Sprintf("%s://%s", c.Schema, c.HostPort())
|
||||
}
|
||||
|
|
@ -35,9 +35,11 @@
|
|||
--evcc-red-rgb: 252, 68, 15;
|
||||
--bs-gray-deep: #010322;
|
||||
--bs-gray-dark: #28293e;
|
||||
--bs-gray-darker: #151630;
|
||||
--bs-gray-medium: #93949e;
|
||||
--bs-gray-light: #b5b6c3;
|
||||
--bs-gray-bright: #f3f3f7;
|
||||
--bs-gray-brighter: #f9f9fb;
|
||||
|
||||
--evcc-grid: var(--bs-gray-dark);
|
||||
--evcc-self: var(--evcc-dark-green);
|
||||
|
|
@ -46,6 +48,7 @@
|
|||
--evcc-export: var(--evcc-yellow);
|
||||
--evcc-background: var(--bs-gray-bright);
|
||||
--evcc-box: var(--bs-white);
|
||||
--evcc-box-border: var(--bs-gray-brighter);
|
||||
--evcc-default-text: var(--bs-gray-dark);
|
||||
--evcc-gray: var(--bs-gray-medium);
|
||||
--evcc-gray-50: #93949e80;
|
||||
|
|
@ -65,6 +68,10 @@
|
|||
|
||||
--bs-warning: var(--evcc-orange);
|
||||
--bs-warning-rgb: var(--evcc-orange-rgb);
|
||||
--bs-danger-50: #dc354580;
|
||||
|
||||
--bs-danger: var(--evcc-red);
|
||||
--bs-danger-rgb: var(--evcc-red-rgb);
|
||||
|
||||
--bs-danger: var(--evcc-red);
|
||||
--bs-danger-rgb: var(--evcc-red-rgb);
|
||||
|
|
@ -76,12 +83,14 @@
|
|||
--bs-success: var(--evcc-primary);
|
||||
--bs-success-rgb: var(--bs-primary-rgb);
|
||||
--bs-code-color: var(--evcc-darkest-green);
|
||||
--evcc-backdrop: #93949eaa; /* --bs-gray-medium + transparent */
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--evcc-grid: var(--bs-gray-medium);
|
||||
--evcc-background: var(--bs-gray-deep);
|
||||
--evcc-box: var(--bs-gray-dark);
|
||||
--evcc-box-border: var(--vs-gray-darker);
|
||||
--evcc-default-text: var(--bs-white);
|
||||
--evcc-gray: var(--bs-gray-light);
|
||||
--evcc-accent1: var(--evcc-yellow);
|
||||
|
|
@ -90,15 +99,10 @@
|
|||
--bs-primary: var(--evcc-dark-green);
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.175);
|
||||
--bs-code-color: var(--evcc-dark-green);
|
||||
--evcc-backdrop: #000000aa; /* black + transparent */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html {
|
||||
/* prevents content jump when scrollbar is activate */
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html.no-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
|
@ -322,7 +326,19 @@ a:hover {
|
|||
}
|
||||
|
||||
.modal-backdrop.show {
|
||||
opacity: 0.8;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
--bs-backdrop-bg: var(--evcc-backdrop);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.modal-backdrop.fade {
|
||||
transition-property: opacity, backdrop-filter, background-color;
|
||||
transition-duration: var(--evcc-transition-fast);
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
|
@ -342,6 +358,13 @@ a:hover {
|
|||
color: var(--evcc-default-text);
|
||||
}
|
||||
|
||||
.modal-content h6 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* breakpoint sm */
|
||||
@media (min-width: 576px) {
|
||||
.modal-content {
|
||||
|
|
@ -362,6 +385,10 @@ a:hover {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
html:has(.modal.show) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -514,6 +541,9 @@ input[type="time"]::-webkit-calendar-picker-indicator {
|
|||
.w-sm-50 {
|
||||
width: 50% !important;
|
||||
}
|
||||
.w-sm-25 {
|
||||
width: 25% !important;
|
||||
}
|
||||
}
|
||||
|
||||
html.app .hide-in-app {
|
||||
|
|
@ -553,13 +583,20 @@ html.app .modal-dialog {
|
|||
|
||||
.round-box {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 0 0 var(--evcc-gray-50);
|
||||
color: var(--evcc-default-text);
|
||||
background: var(--evcc-box);
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--evcc-gray-50);
|
||||
transition: box-shadow var(--evcc-transition-fast) linear;
|
||||
}
|
||||
.round-box:hover {
|
||||
border-color: var(--evcc-gray);
|
||||
|
||||
.round-box--error {
|
||||
box-shadow: 0 0 0.5rem var(--bs-danger);
|
||||
border: 1px solid var(--bs-danger);
|
||||
}
|
||||
|
||||
.round-box--error .tags * {
|
||||
color: var(--bs-danger-text-emphasis) !important;
|
||||
}
|
||||
|
||||
.alert-danger code {
|
||||
color: var(--evcc-darkest-green);
|
||||
}
|
||||
|
|
|
|||
26
assets/js/components/Config/CircuitsModal.vue
Normal file
26
assets/js/components/Config/CircuitsModal.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="circuitsModal"
|
||||
:title="$t('config.circuits.title')"
|
||||
:description="$t('config.circuits.description')"
|
||||
docs="/docs/reference/configuration/circuits"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/circuits"
|
||||
size="md"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/circuits.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "CircuitsModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
194
assets/js/components/Config/ControlModal.vue
Normal file
194
assets/js/components/Config/ControlModal.vue
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
id="controlModal"
|
||||
ref="modal"
|
||||
:title="$t('config.control.title')"
|
||||
data-testid="control-modal"
|
||||
@open="open"
|
||||
>
|
||||
<p>{{ $t("config.control.description") }}</p>
|
||||
<p class="text-danger" v-if="error">{{ error }}</p>
|
||||
<form ref="form" class="container mx-0 px-0" @submit.prevent="save">
|
||||
<FormRow
|
||||
id="controlInterval"
|
||||
:label="$t('config.control.labelInterval')"
|
||||
:help="$t('config.control.descriptionInterval')"
|
||||
example="30 s"
|
||||
docsLink="/docs/reference/configuration/interval"
|
||||
>
|
||||
<div class="input-group w-50 w-sm-25">
|
||||
<input
|
||||
id="controlInterval"
|
||||
v-model="values.interval"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
required
|
||||
aria-describedby="controlIntervalUnit"
|
||||
class="form-control text-end"
|
||||
/>
|
||||
<span id="controlIntervalUnit" class="input-group-text">s</span>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="controlResidualPower"
|
||||
:label="$t('config.control.labelResidualPower')"
|
||||
:help="$t('config.control.descriptionResidualPower')"
|
||||
example="100 W"
|
||||
docsLink="/docs/reference/configuration/site#residualpower"
|
||||
>
|
||||
<div class="input-group w-50 w-sm-25">
|
||||
<input
|
||||
id="controlResidualPower"
|
||||
v-model="values.residualPower"
|
||||
type="number"
|
||||
step="1"
|
||||
required
|
||||
aria-describedby="controlResidualPowerUnit"
|
||||
class="form-control text-end"
|
||||
/>
|
||||
<span id="controlResidualPowerUnit" class="input-group-text">W</span>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="controlMaxGridSupply"
|
||||
:label="$t('config.control.labelMaxGridSupply')"
|
||||
:help="$t('config.control.descriptionMaxGridSupply')"
|
||||
docsLink="/docs/reference/configuration/site#maxgridsupplywhilebatterycharging"
|
||||
>
|
||||
<div class="input-group w-50 w-sm-25">
|
||||
<input
|
||||
id="controlMaxGridSupply"
|
||||
v-model="values.maxGridSupply"
|
||||
type="number"
|
||||
step="1"
|
||||
required
|
||||
aria-describedby="controlMaxGridSupplyUnit"
|
||||
class="form-control text-end"
|
||||
/>
|
||||
<span id="controlMaxGridSupplyUnit" class="input-group-text">W</span>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-between gap-2 flex-column flex-sm-row">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-muted btn-cancel"
|
||||
data-bs-dismiss="modal"
|
||||
>
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary order-1 order-sm-2 flex-grow-1 flex-sm-grow-0 px-4"
|
||||
:disabled="saving || nothingChanged"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericModal from "./../GenericModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import store from "../../store";
|
||||
import api from "../../api";
|
||||
|
||||
export default {
|
||||
name: "ControlModal",
|
||||
components: { FormRow, GenericModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
error: "",
|
||||
values: {},
|
||||
serverValues: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
intervalChanged() {
|
||||
return this.values.interval !== this.serverValues.interval;
|
||||
},
|
||||
residualPowerChanged() {
|
||||
return this.values.residualPower !== this.serverValues.residualPower;
|
||||
},
|
||||
maxGridSupplyChanged() {
|
||||
return this.values.maxGridSupply !== this.serverValues.maxGridSupply;
|
||||
},
|
||||
nothingChanged() {
|
||||
return (
|
||||
!this.intervalChanged && !this.residualPowerChanged && !this.maxGridSupplyChanged
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
const {
|
||||
interval,
|
||||
residualPower,
|
||||
maxGridSupplyWhileBatteryCharging: maxGridSupply,
|
||||
} = store?.state || {};
|
||||
this.saving = false;
|
||||
this.error = "";
|
||||
this.values = { interval, residualPower, maxGridSupply };
|
||||
this.serverValues = { ...this.values };
|
||||
},
|
||||
async open() {
|
||||
this.reset();
|
||||
},
|
||||
async saveValue(name) {
|
||||
let url = "";
|
||||
if (name === "interval") {
|
||||
url = `/config/interval/${encodeURIComponent(this.values.interval)}`;
|
||||
} else if (name === "residualPower") {
|
||||
url = `/residualpower/${encodeURIComponent(this.values.residualPower)}`;
|
||||
} else if (name === "maxGridSupply") {
|
||||
url = `/maxgridsupply/${encodeURIComponent(this.values.maxGridSupply)}`;
|
||||
}
|
||||
await api.post(url);
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.error = "";
|
||||
try {
|
||||
if (this.intervalChanged) {
|
||||
await this.saveValue("interval");
|
||||
}
|
||||
if (this.residualPowerChanged) {
|
||||
await this.saveValue("residualPower");
|
||||
}
|
||||
if (this.maxGridSupplyChanged) {
|
||||
await this.saveValue("maxGridSupply");
|
||||
}
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
padding-right: 0;
|
||||
}
|
||||
.btn-cancel {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +1,12 @@
|
|||
<template>
|
||||
<li class="root py-2 px-4">
|
||||
<li class="root round-box" :class="{ 'round-box--error': error }">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="icon me-2">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<strong class="flex-grow-1 text-nowrap text-truncate">{{ name }}</strong>
|
||||
<button
|
||||
v-if="unconfigured"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2"
|
||||
:title="$t('config.main.new')"
|
||||
@click="$emit('configure')"
|
||||
>
|
||||
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="editable"
|
||||
v-if="editable"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2"
|
||||
:title="$t('config.main.edit')"
|
||||
|
|
@ -34,17 +25,14 @@
|
|||
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="unconfigured" class="text-center py-3 evcc-gray">
|
||||
{{ $t("config.main.unconfigured") }}
|
||||
</div>
|
||||
<slot v-else name="tags" />
|
||||
<hr class="my-3 divide" />
|
||||
<slot name="tags" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/regular/adjust";
|
||||
import "@h2d2/shopicons/es/regular/invoice";
|
||||
import "@h2d2/shopicons/es/regular/edit";
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
|
||||
export default {
|
||||
|
|
@ -52,14 +40,14 @@ export default {
|
|||
props: {
|
||||
name: String,
|
||||
editable: Boolean,
|
||||
unconfigured: Boolean,
|
||||
error: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tooltip: null,
|
||||
};
|
||||
},
|
||||
emits: ["edit", "configure"],
|
||||
emits: ["edit"],
|
||||
mounted() {
|
||||
this.initTooltip();
|
||||
},
|
||||
|
|
@ -67,9 +55,6 @@ export default {
|
|||
editable() {
|
||||
this.initTooltip();
|
||||
},
|
||||
unconfigured() {
|
||||
this.initTooltip();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initTooltip() {
|
||||
|
|
@ -88,18 +73,15 @@ export default {
|
|||
.root {
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
min-height: 9rem;
|
||||
color: var(--evcc-default-text);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--evcc-gray-50);
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
transition: border-color var(--evcc-transition-fast) linear;
|
||||
background: var(--evcc-box);
|
||||
}
|
||||
.root:hover {
|
||||
border-color: var(--evcc-gray);
|
||||
padding: 1rem 1.5rem;
|
||||
min-height: 8rem;
|
||||
}
|
||||
.icon:empty {
|
||||
display: none;
|
||||
}
|
||||
.divide {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
<template>
|
||||
<div v-if="tags" class="d-flex mb-2 flex-wrap">
|
||||
<div v-if="tags" class="tags">
|
||||
<span
|
||||
v-for="(entry, index) in entries"
|
||||
:key="index"
|
||||
class="badge text-bg-secondary me-2 mb-2"
|
||||
:class="{
|
||||
'text-bg-secondary': !entry.error,
|
||||
'text-bg-danger': entry.error,
|
||||
}"
|
||||
:data-testid="`device-tag-${entry.name}`"
|
||||
class="d-flex gap-2 overflow-hidden text-truncate"
|
||||
>
|
||||
<strong>{{ $t(`config.deviceValue.${entry.name}`) }}:</strong>
|
||||
{{ fmtDeviceValue(entry) }}
|
||||
<div class="label overflow-hidden text-truncate flex-shrink-1 flex-grow-1">
|
||||
{{ $t(`config.deviceValue.${entry.name}`) }}
|
||||
</div>
|
||||
<div
|
||||
class="value overflow-hidden text-truncate"
|
||||
:class="{ 'value--error': hasError(entry), 'value--muted': entry.value === false }"
|
||||
>
|
||||
{{ fmtDeviceValue(entry) }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -25,14 +29,20 @@ export default {
|
|||
mixins: [formatter],
|
||||
computed: {
|
||||
entries() {
|
||||
return Object.entries(this.tags).map(([name, { value, error }]) => {
|
||||
return { name, value, error };
|
||||
return Object.entries(this.tags).map(([name, { value, error, options }]) => {
|
||||
return { name, value, error, options };
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasError(entry) {
|
||||
return !!entry.error;
|
||||
},
|
||||
fmtDeviceValue(entry) {
|
||||
const { name, value } = entry;
|
||||
const { name, value, options = {} } = entry;
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
switch (name) {
|
||||
case "power":
|
||||
return this.fmtKw(value);
|
||||
|
|
@ -43,6 +53,8 @@ export default {
|
|||
case "soc":
|
||||
case "socLimit":
|
||||
return this.fmtPercentage(value, 1);
|
||||
case "temp":
|
||||
return this.fmtTemperature(value);
|
||||
case "odometer":
|
||||
case "range":
|
||||
return `${this.fmtNumber(value, 0)} km`;
|
||||
|
|
@ -53,11 +65,43 @@ export default {
|
|||
case "phasePowers":
|
||||
return value.map((v) => this.fmtKw(v)).join(", ");
|
||||
case "chargeStatus":
|
||||
return value;
|
||||
return this.$t(`config.deviceValue.chargeStatus${value}`);
|
||||
case "gridPrice":
|
||||
case "feedinPrice":
|
||||
return this.fmtPricePerKWh(value, options.currency, true);
|
||||
case "co2":
|
||||
return this.fmtCo2Short(value);
|
||||
case "configured":
|
||||
return value
|
||||
? this.$t("config.deviceValue.yes")
|
||||
: this.$t("config.deviceValue.no");
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.tags {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
.label {
|
||||
min-width: 4rem;
|
||||
}
|
||||
.value {
|
||||
font-weight: bold;
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
.value:empty::after {
|
||||
color: var(--evcc-gray);
|
||||
content: "–";
|
||||
}
|
||||
.value--error {
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
.value--muted {
|
||||
color: var(--evcc-gray) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
25
assets/js/components/Config/EebusModal.vue
Normal file
25
assets/js/components/Config/EebusModal.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="eebusModal"
|
||||
:title="$t('config.eebus.title')"
|
||||
:description="$t('config.eebus.description')"
|
||||
docs="/docs/reference/configuration/eebus"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/eebus"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/eebus.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "EebusModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -12,13 +12,19 @@
|
|||
</div>
|
||||
<div class="form-text evcc-gray">
|
||||
<div v-if="example">{{ $t("config.form.example") }}: {{ example }}</div>
|
||||
<div v-if="help" v-html="helpHtml" class="text-gray"></div>
|
||||
<div v-if="help">
|
||||
<span class="text-gray" v-html="helpHtml"></span>
|
||||
<a class="ms-1 text-gray" v-if="link" :href="link" target="_blank">
|
||||
{{ $t("config.general.docsLink") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import linkify from "../../utils/linkify";
|
||||
import { docsPrefix } from "../../i18n";
|
||||
|
||||
export default {
|
||||
name: "FormRow",
|
||||
|
|
@ -28,11 +34,15 @@ export default {
|
|||
help: String,
|
||||
optional: Boolean,
|
||||
example: String,
|
||||
docsLink: String,
|
||||
},
|
||||
computed: {
|
||||
helpHtml() {
|
||||
return linkify(this.help);
|
||||
},
|
||||
link() {
|
||||
return this.docsLink ? `${docsPrefix()}${this.docsLink}` : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,93 +1,111 @@
|
|||
<template>
|
||||
<div class="group round-box pt-4 px-4 pb-1">
|
||||
<dl class="row" data-testid="generalconfig-title">
|
||||
<dt class="col-sm-4">Title</dt>
|
||||
<dd class="col-sm-8">
|
||||
{{ title || "---" }}
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('titleModal')"
|
||||
>
|
||||
edit
|
||||
</a>
|
||||
<div class="group round-box p-4">
|
||||
<div class="config-entry" data-testid="generalconfig-title">
|
||||
<strong class="config-label">{{ $t("config.general.title") }}</strong>
|
||||
<div class="config-text">{{ title || "---" }}</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('titleModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
<TitleModal ref="titleModal" @changed="load" />
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row" data-testid="generalconfig-password">
|
||||
<dt class="col-sm-4">Password</dt>
|
||||
<dd class="col-sm-8">
|
||||
*******
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('passwordModal')"
|
||||
>edit</a
|
||||
>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row" data-testid="generalconfig-telemetry">
|
||||
<dt class="col-sm-4">Telemetry</dt>
|
||||
<dd class="col-sm-8">
|
||||
{{ telemetryEnabled ? "on" : "off" }}
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('globalSettingsModal')"
|
||||
>
|
||||
change
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row" data-testid="generalconfig-experimental">
|
||||
<dt class="col-sm-4">Experimental</dt>
|
||||
<dd class="col-sm-8">
|
||||
{{ hiddenFeatures ? "on" : "off" }}
|
||||
<a
|
||||
href="#"
|
||||
class="ms-2 d-inline-block text-muted"
|
||||
@click.prevent="openModal('globalSettingsModal')"
|
||||
>
|
||||
change
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">API-Key</dt>
|
||||
<dd class="col-sm-8">
|
||||
*******
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">show</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Sponsoring</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="text-primary"> valid </span>
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">change</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Server</dt>
|
||||
<dd class="col-sm-8">
|
||||
http://evcc.local:7070
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">edit</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="row wip">
|
||||
<dt class="col-sm-4">Update Interval</dt>
|
||||
<dd class="col-sm-8">
|
||||
30s
|
||||
<a href="#" class="ms-2 d-inline-block text-muted" @click.prevent="todo">edit</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</button>
|
||||
</div>
|
||||
<div class="config-entry" data-testid="generalconfig-password">
|
||||
<strong class="config-label">{{ $t("config.general.password") }}</strong>
|
||||
<div class="config-text">*******</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('passwordModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="config-entry" data-testid="generalconfig-telemetry">
|
||||
<strong class="config-label">{{ $t("config.general.telemetry") }}</strong>
|
||||
<div class="config-text">
|
||||
{{ $t(`config.general.${telemetryEnabled ? "on" : "off"}`) }}
|
||||
</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('globalSettingsModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="config-entry" data-testid="generalconfig-experimental">
|
||||
<strong class="config-label">{{ $t("config.general.experimental") }}</strong>
|
||||
<div class="config-text">
|
||||
{{ $t(`config.general.${hiddenFeatures ? "on" : "off"}`) }}
|
||||
</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('globalSettingsModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="$hiddenFeatures()" class="config-entry" data-testid="generalconfig-sponsoring">
|
||||
<strong class="config-label">{{ $t("config.sponsor.title") }} 🧪</strong>
|
||||
<div class="config-text" :class="sponsorStatus.cssClass">
|
||||
<span
|
||||
v-if="sponsorStatus.expiresSoon"
|
||||
class="d-inline-block me-1 p-1 rounded-circle bg-warning rounded-circle"
|
||||
></span>
|
||||
{{ sponsorStatus.name || "---" }}
|
||||
</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('sponsorModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="$hiddenFeatures()" class="config-entry" data-testid="generalconfig-network">
|
||||
<strong class="config-label">{{ $t("config.network.title") }} 🧪</strong>
|
||||
<div class="config-text">{{ networkStatus }}</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('networkModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="$hiddenFeatures()" class="config-entry" data-testid="generalconfig-control">
|
||||
<strong class="config-label">{{ $t("config.control.title") }} 🧪</strong>
|
||||
<div class="config-text">{{ controlStatus }}</div>
|
||||
<button
|
||||
class="config-button btn btn-link"
|
||||
type="button"
|
||||
:title="$t('config.main.edit')"
|
||||
@click.prevent="openModal('controlModal')"
|
||||
>
|
||||
<EditIcon size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import TitleModal from "./TitleModal.vue";
|
||||
import EditIcon from "../MaterialIcon/Edit.vue";
|
||||
import api from "../../api";
|
||||
import settings from "../../settings";
|
||||
import store from "../../store";
|
||||
import formatter from "../../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "GeneralConfig",
|
||||
|
|
@ -96,7 +114,8 @@ export default {
|
|||
title: "",
|
||||
};
|
||||
},
|
||||
components: { TitleModal },
|
||||
components: { TitleModal, EditIcon },
|
||||
mixins: [formatter],
|
||||
emits: ["site-changed"],
|
||||
async mounted() {
|
||||
await this.load();
|
||||
|
|
@ -108,6 +127,26 @@ export default {
|
|||
hiddenFeatures() {
|
||||
return settings.hiddenFeatures === true;
|
||||
},
|
||||
networkStatus() {
|
||||
const { host, port } = store.state?.network || {};
|
||||
return host ? `${host}:${port}` : `${port || ""}`;
|
||||
},
|
||||
controlStatus() {
|
||||
const sec = store.state?.interval;
|
||||
return sec ? this.fmtDuration(sec) : "";
|
||||
},
|
||||
sponsorStatus() {
|
||||
const sponsor = store.state?.sponsor || {};
|
||||
const { name, expiresSoon } = sponsor;
|
||||
let cssClass = "";
|
||||
if (expiresSoon) {
|
||||
cssClass = "text-warning";
|
||||
} else if (name) {
|
||||
cssClass = "text-primary";
|
||||
}
|
||||
|
||||
return { name, expiresSoon, cssClass };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changed() {
|
||||
|
|
@ -116,8 +155,14 @@ export default {
|
|||
},
|
||||
async load() {
|
||||
try {
|
||||
let res = await api.get("/config/site");
|
||||
this.title = res.data.result.title;
|
||||
let res = await api.get("/config/site", {
|
||||
validateStatus: (code) => [200, 404].includes(code),
|
||||
});
|
||||
if (res.status === 200) {
|
||||
this.title = res.data.result.title;
|
||||
} else {
|
||||
console.log("TODO: implement site endpoint in config error mode");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -140,17 +185,39 @@ export default {
|
|||
<style scoped>
|
||||
.group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||
grid-gap: 2rem 5rem;
|
||||
margin-bottom: 5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.wip {
|
||||
opacity: 0.2;
|
||||
display: none !important;
|
||||
}
|
||||
dt {
|
||||
margin-bottom: 0.5rem;
|
||||
.config-entry {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
dd {
|
||||
margin-bottom: 1rem;
|
||||
.config-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.config-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
.config-button {
|
||||
margin-right: -1rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--evcc-text-default);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
26
assets/js/components/Config/HemsModal.vue
Normal file
26
assets/js/components/Config/HemsModal.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="hemsModal"
|
||||
:title="$t('config.hems.title')"
|
||||
:description="$t('config.hems.description')"
|
||||
docs="/docs/reference/configuration/hems"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/hems"
|
||||
size="md"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/hems.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "HemsModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
122
assets/js/components/Config/InfluxModal.vue
Normal file
122
assets/js/components/Config/InfluxModal.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<JsonModal
|
||||
id="influxModal"
|
||||
:title="$t('config.influx.title')"
|
||||
:description="$t('config.influx.description')"
|
||||
docs="/docs/reference/configuration/influx"
|
||||
endpoint="/config/influx"
|
||||
state-key="influx"
|
||||
data-testid="influx-modal"
|
||||
@changed="$emit('changed')"
|
||||
>
|
||||
<template v-slot:default="{ values }">
|
||||
<FormRow
|
||||
id="influxUrl"
|
||||
:label="$t('config.influx.labelUrl')"
|
||||
example="http://localhost:8086"
|
||||
>
|
||||
<input
|
||||
id="influxUrl"
|
||||
v-model="values.url"
|
||||
type="url"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
v-if="!showV1(values)"
|
||||
id="influxOrg"
|
||||
:label="$t('config.influx.labelOrg')"
|
||||
example="home"
|
||||
>
|
||||
<input id="influxOrg" v-model="values.org" class="form-control" required />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="influxDatabase"
|
||||
:label="$t(`config.influx.label${showV1(values) ? 'Database' : 'Bucket'}`)"
|
||||
example="evcc"
|
||||
>
|
||||
<input
|
||||
id="influxDatabase"
|
||||
v-model="values.database"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
v-if="!showV1(values)"
|
||||
id="influxToken"
|
||||
:label="$t('config.influx.labelToken')"
|
||||
:help="$t('config.influx.descriptionToken')"
|
||||
>
|
||||
<input id="influxToken" v-model="values.token" class="form-control" required />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
v-if="showV1(values)"
|
||||
id="influxUser"
|
||||
:label="$t('config.influx.labelUser')"
|
||||
optional
|
||||
>
|
||||
<input id="influxUser" v-model="values.user" class="form-control" />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
v-if="showV1(values)"
|
||||
id="influxPassword"
|
||||
:label="$t('config.influx.labelPassword')"
|
||||
optional
|
||||
>
|
||||
<input
|
||||
id="influxPassword"
|
||||
v-model="values.password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</FormRow>
|
||||
<p>
|
||||
<button
|
||||
v-if="showV1(values)"
|
||||
class="btn btn-link btn-sm text-primary px-0"
|
||||
type="button"
|
||||
@click="
|
||||
values.user = '';
|
||||
values.password = '';
|
||||
v1 = false;
|
||||
"
|
||||
>
|
||||
{{ $t("config.influx.v2Support") }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-link btn-sm text-primary px-0"
|
||||
type="button"
|
||||
@click="
|
||||
values.token = '';
|
||||
v1 = true;
|
||||
"
|
||||
>
|
||||
{{ $t("config.influx.v1Support") }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
|
||||
export default {
|
||||
name: "InfluxModal",
|
||||
components: { FormRow, JsonModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { v1: false };
|
||||
},
|
||||
methods: {
|
||||
showV1(values) {
|
||||
return this.v1 || values.user || values.password;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
174
assets/js/components/Config/JsonModal.vue
Normal file
174
assets/js/components/Config/JsonModal.vue
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<GenericModal ref="modal" :title="title" @open="open">
|
||||
<p v-if="description || docsLink">
|
||||
<span v-if="description">{{ description + " " }}</span>
|
||||
<a v-if="docsLink" :href="docsLink" target="_blank">
|
||||
{{ $t("config.general.docsLink") }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-danger" v-if="error">
|
||||
<span v-if="errorMessage" class="d-block">{{ errorMessage }}</span>
|
||||
{{ error }}
|
||||
</p>
|
||||
<form ref="form" class="container mx-0 px-0" @submit.prevent="save">
|
||||
<slot :values="values"></slot>
|
||||
|
||||
<div
|
||||
v-if="!noButtons"
|
||||
class="mt-4 d-flex justify-content-between gap-2 flex-column flex-sm-row"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-content-between order-2 order-sm-1 gap-2 flex-grow-1 flex-sm-grow-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-muted btn-cancel"
|
||||
data-bs-dismiss="modal"
|
||||
>
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!disableRemove"
|
||||
type="button"
|
||||
class="btn btn-link text-danger"
|
||||
:disabled="removing"
|
||||
@click="remove"
|
||||
>
|
||||
<span
|
||||
v-if="removing"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.remove") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary order-1 order-sm-2 flex-grow-1 flex-sm-grow-0 px-4"
|
||||
:disabled="saving || nothingChanged"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericModal from "../GenericModal.vue";
|
||||
import api from "../../api";
|
||||
import { docsPrefix } from "../../i18n";
|
||||
import store from "../../store";
|
||||
|
||||
export default {
|
||||
name: "JsonModal",
|
||||
components: { GenericModal },
|
||||
emits: ["changed", "open"],
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
removing: false,
|
||||
error: "",
|
||||
values: {},
|
||||
serverValues: {},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
description: String,
|
||||
errorMessage: String,
|
||||
docs: String,
|
||||
endpoint: String,
|
||||
disableRemove: Boolean,
|
||||
noButtons: Boolean,
|
||||
transformReadValues: Function,
|
||||
stateKey: String,
|
||||
saveMethod: { type: String, default: "post" },
|
||||
},
|
||||
computed: {
|
||||
docsLink() {
|
||||
return this.docs ? `${docsPrefix()}${this.docs}` : null;
|
||||
},
|
||||
nothingChanged() {
|
||||
return JSON.stringify(this.values) === JSON.stringify(this.serverValues);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.saving = false;
|
||||
this.deleting = false;
|
||||
this.error = "";
|
||||
this.values = "";
|
||||
this.serverValues = "";
|
||||
},
|
||||
async open() {
|
||||
this.$emit("open");
|
||||
this.reset();
|
||||
await this.load();
|
||||
},
|
||||
async load() {
|
||||
this.serverValues = this.stateKey ? store.state[this.stateKey] : store.state;
|
||||
if (this.transformReadValues) {
|
||||
this.serverValues = this.transformReadValues(this.serverValues);
|
||||
}
|
||||
this.values = { ...this.serverValues };
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.error = "";
|
||||
try {
|
||||
const res = await api[this.saveMethod](this.endpoint, this.values, {
|
||||
validateStatus: (code) => [200, 202, 400].includes(code),
|
||||
});
|
||||
if (res.status === 200 || res.status === 202) {
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
}
|
||||
if (res.status === 400) {
|
||||
this.error = res.data.error;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
async remove() {
|
||||
this.removing = true;
|
||||
this.error = "";
|
||||
try {
|
||||
const res = await api.delete(this.endpoint, {
|
||||
validateStatus: (code) => [200, 400].includes(code),
|
||||
});
|
||||
if (res.status === 200) {
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
}
|
||||
if (res.status === 400) {
|
||||
this.error = res.data.error;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.removing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
padding-right: 0;
|
||||
}
|
||||
.btn-cancel {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
25
assets/js/components/Config/MessagingModal.vue
Normal file
25
assets/js/components/Config/MessagingModal.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="messagingModal"
|
||||
:title="$t('config.messaging.title')"
|
||||
:description="$t('config.messaging.description')"
|
||||
docs="/docs/reference/configuration/messaging"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/messaging"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/messaging.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "MessagingModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
26
assets/js/components/Config/ModbusProxyModal.vue
Normal file
26
assets/js/components/Config/ModbusProxyModal.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="modbusProxyModal"
|
||||
:title="$t('config.modbusproxy.title')"
|
||||
:description="$t('config.modbusproxy.description')"
|
||||
docs="/docs/reference/configuration/modbusproxy"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/modbusproxy"
|
||||
size="md"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/modbusproxy.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "ModbusProxyModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
79
assets/js/components/Config/MqttModal.vue
Normal file
79
assets/js/components/Config/MqttModal.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<JsonModal
|
||||
id="mqttModal"
|
||||
:title="$t('config.mqtt.title')"
|
||||
:description="$t('config.mqtt.description')"
|
||||
docs="/docs/reference/configuration/mqtt"
|
||||
endpoint="/config/mqtt"
|
||||
state-key="mqtt"
|
||||
data-testid="mqtt-modal"
|
||||
@changed="$emit('changed')"
|
||||
>
|
||||
<template v-slot:default="{ values }">
|
||||
<FormRow
|
||||
id="mqttBroker"
|
||||
:label="$t('config.mqtt.labelBroker')"
|
||||
example="localhost:1883"
|
||||
>
|
||||
<input id="mqttBroker" v-model="values.broker" class="form-control" required />
|
||||
</FormRow>
|
||||
|
||||
<h6>{{ $t("config.mqtt.publishing") }}</h6>
|
||||
<FormRow
|
||||
id="mqttTopic"
|
||||
:label="$t('config.mqtt.labelTopic')"
|
||||
:help="$t('config.mqtt.descriptionTopic')"
|
||||
example="evcc"
|
||||
optional
|
||||
>
|
||||
<input id="mqttTopic" v-model="values.topic" class="form-control" />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="mqttClientId"
|
||||
:label="$t('config.mqtt.labelClientId')"
|
||||
:help="$t('config.mqtt.descriptionClientId')"
|
||||
optional
|
||||
>
|
||||
<input id="mqttClientId" v-model="values.clientID" class="form-control" />
|
||||
</FormRow>
|
||||
|
||||
<h6>{{ $t("config.mqtt.authentication") }}</h6>
|
||||
<FormRow id="mqttUser" :label="$t('config.mqtt.labelUser')" optional>
|
||||
<input id="mqttUser" v-model="values.user" class="form-control" />
|
||||
</FormRow>
|
||||
<FormRow id="mqttPassword" :label="$t('config.mqtt.labelPassword')" optional>
|
||||
<input
|
||||
id="mqttPassword"
|
||||
v-model="values.password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow id="mqttInsecure" :label="$t('config.mqtt.labelInsecure')">
|
||||
<div class="d-flex">
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="mqttInsecure"
|
||||
type="checkbox"
|
||||
v-model="values.insecure"
|
||||
/>
|
||||
<label class="form-check-label ms-2" for="mqttInsecure">
|
||||
{{ $t("config.mqtt.labelCheckInsecure") }}
|
||||
</label>
|
||||
</div>
|
||||
</FormRow>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
|
||||
export default {
|
||||
name: "MqttModal",
|
||||
components: { FormRow, JsonModal },
|
||||
emits: ["changed"],
|
||||
};
|
||||
</script>
|
||||
75
assets/js/components/Config/NetworkModal.vue
Normal file
75
assets/js/components/Config/NetworkModal.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<JsonModal
|
||||
id="networkModal"
|
||||
:title="$t('config.network.title')"
|
||||
endpoint="/config/network"
|
||||
state-key="network"
|
||||
disable-remove
|
||||
data-testid="network-modal"
|
||||
@changed="$emit('changed')"
|
||||
>
|
||||
<template v-slot:default="{ values }">
|
||||
<FormRow
|
||||
id="networkSchema"
|
||||
:label="$t('config.network.labelSchema')"
|
||||
:help="$t('config.network.descriptionSchema')"
|
||||
>
|
||||
<div class="btn-group" role="group">
|
||||
<input
|
||||
id="networkSchemaHttp"
|
||||
v-model="values.schema"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="networkSchema"
|
||||
value="http"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="networkSchemaHttp">HTTP</label>
|
||||
<input
|
||||
id="networkSchemaHttps"
|
||||
v-model="values.schema"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="networkSchema"
|
||||
value="https"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="networkSchemaHttps">HTTPS</label>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="networkHost"
|
||||
:label="$t('config.network.labelHost')"
|
||||
:help="$t('config.network.descriptionHost')"
|
||||
example="evcc.local"
|
||||
>
|
||||
<input id="networkHost" v-model="values.host" class="form-control" />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="networkPort"
|
||||
:label="$t('config.network.labelPort')"
|
||||
:help="$t('config.network.descriptionPort')"
|
||||
>
|
||||
<input
|
||||
id="networkPort"
|
||||
v-model="values.port"
|
||||
class="form-control w-50 me-2 w-50"
|
||||
type="number"
|
||||
example="7070"
|
||||
/>
|
||||
</FormRow>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
|
||||
export default {
|
||||
name: "NetworkModal",
|
||||
components: { FormRow, JsonModal },
|
||||
emits: ["changed"],
|
||||
};
|
||||
</script>
|
||||
96
assets/js/components/Config/SponsorModal.vue
Normal file
96
assets/js/components/Config/SponsorModal.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<JsonModal
|
||||
id="sponsorModal"
|
||||
:title="`${$t('config.sponsor.title')} 💚`"
|
||||
:description="$t('config.sponsor.description')"
|
||||
:error-message="$t('config.sponsor.error')"
|
||||
endpoint="/config/sponsortoken"
|
||||
:transform-read-values="transformReadValues"
|
||||
data-testid="sponsor-modal"
|
||||
size="lg"
|
||||
:no-buttons="!showForm"
|
||||
:disable-remove="!hasToken"
|
||||
@changed="$emit('changed')"
|
||||
@open="showForm = false"
|
||||
>
|
||||
<template v-slot:default="{ values }">
|
||||
<SponsorTokenExpires v-bind="sponsor" />
|
||||
<div class="mt-4 mb-3">
|
||||
<Sponsor v-bind="sponsor" />
|
||||
</div>
|
||||
<button
|
||||
v-if="!showForm"
|
||||
type="button"
|
||||
class="btn btn-link text-muted btn-form"
|
||||
@click="showForm = !showForm"
|
||||
>
|
||||
{{ sponsorTokenLabel }}
|
||||
</button>
|
||||
<div v-else>
|
||||
<hr />
|
||||
<FormRow
|
||||
id="sponsorToken"
|
||||
:label="sponsorTokenLabel"
|
||||
:help="
|
||||
$t('config.sponsor.descriptionToken', { url: 'https://sponsor.evcc.io' })
|
||||
"
|
||||
docs-link="/docs/sponsorship"
|
||||
class="mt-4"
|
||||
>
|
||||
<textarea
|
||||
id="sponsorToken"
|
||||
v-model="values.token"
|
||||
required
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
class="form-control"
|
||||
/>
|
||||
</FormRow>
|
||||
</div>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import Sponsor, { VICTRON_DEVICE } from "../Sponsor.vue";
|
||||
import SponsorTokenExpires from "../SponsorTokenExpires.vue";
|
||||
import store from "../../store";
|
||||
|
||||
export default {
|
||||
name: "SponsorModal",
|
||||
components: { FormRow, JsonModal, Sponsor, SponsorTokenExpires },
|
||||
emits: ["changed"],
|
||||
data: () => ({
|
||||
showForm: false,
|
||||
}),
|
||||
methods: {
|
||||
transformReadValues() {
|
||||
return { token: "" };
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sponsor() {
|
||||
return store?.state?.sponsor;
|
||||
},
|
||||
hasToken() {
|
||||
const name = this.sponsor?.name || "";
|
||||
return name !== "" && name !== VICTRON_DEVICE;
|
||||
},
|
||||
sponsorTokenLabel() {
|
||||
return this.hasToken
|
||||
? this.$t("config.sponsor.changeToken")
|
||||
: this.$t("config.sponsor.addToken");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
textarea {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
.btn-form {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
26
assets/js/components/Config/TariffsModal.vue
Normal file
26
assets/js/components/Config/TariffsModal.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<YamlModal
|
||||
id="tariffsModal"
|
||||
:title="$t('config.tariffs.title')"
|
||||
:description="$t('config.tariffs.description')"
|
||||
docs="/docs/reference/configuration/tariffs"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/tariffs"
|
||||
data-testid="tariffs-modal"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import YamlModal from "./YamlModal.vue";
|
||||
import defaultYaml from "./defaultYaml/tariffs.yaml?raw";
|
||||
|
||||
export default {
|
||||
name: "TariffsModal",
|
||||
components: { YamlModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return { defaultYaml: defaultYaml.trim() };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
<template>
|
||||
<div
|
||||
class="alert my-4"
|
||||
:class="{
|
||||
'alert-secondary': unknown || running,
|
||||
'alert-success': success,
|
||||
'alert-danger': failed,
|
||||
}"
|
||||
role="alert"
|
||||
>
|
||||
<div class="test-result my-4 p-4">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{{ $t("config.validation.label") }}:
|
||||
<strong>
|
||||
<span>{{ $t("config.validation.label") }}: </span>
|
||||
<span v-if="unknown">{{ $t("config.validation.unknown") }}</span>
|
||||
<span v-if="running">{{ $t("config.validation.running") }}</span>
|
||||
<strong v-if="success">{{ $t("config.validation.success") }}</strong>
|
||||
<strong v-if="failed">{{ $t("config.validation.failed") }}</strong>
|
||||
</div>
|
||||
<span class="text-success" v-if="success">
|
||||
{{ $t("config.validation.success") }}
|
||||
</span>
|
||||
<span class="text-danger" v-if="failed">
|
||||
{{ $t("config.validation.failed") }}
|
||||
</span>
|
||||
</strong>
|
||||
<span
|
||||
v-if="running"
|
||||
class="spinner-border spinner-border-sm"
|
||||
|
|
@ -26,13 +22,13 @@
|
|||
{{ $t("config.validation.validate") }}
|
||||
</a>
|
||||
</div>
|
||||
<hr v-if="error" />
|
||||
<div v-if="error">
|
||||
<hr class="divider" v-if="error" />
|
||||
<div class="text-danger" v-if="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<hr v-if="result" />
|
||||
<hr class="divider" v-if="result" />
|
||||
<div v-if="result">
|
||||
<DeviceTags :tags="result" />
|
||||
<DeviceTags :tags="result" class="success-values" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -54,3 +50,14 @@ export default {
|
|||
components: { DeviceTags },
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.test-result {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.divider {
|
||||
border-top-color: 1px solid var(--bs-border-color);
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,90 +1,39 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
<JsonModal
|
||||
id="titleModal"
|
||||
ref="modal"
|
||||
:title="$t('config.title.title')"
|
||||
endpoint="/config/site"
|
||||
state-key="siteTitle"
|
||||
save-method="put"
|
||||
:transform-read-values="transformReadValues"
|
||||
data-testid="title-modal"
|
||||
disable-remove
|
||||
@changed="$emit('changed')"
|
||||
>
|
||||
<p class="text-danger" v-if="error">{{ error }}</p>
|
||||
<form ref="form" class="container mx-0 px-0">
|
||||
<template v-slot:default="{ values }">
|
||||
<FormRow
|
||||
id="siteTitle"
|
||||
:label="$t('config.title.label')"
|
||||
:help="$t('config.title.description')"
|
||||
>
|
||||
<input id="siteTitle" v-model="title" class="form-control" />
|
||||
<input id="siteTitle" v-model="values.title" class="form-control" />
|
||||
</FormRow>
|
||||
<div class="my-4 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="saving"
|
||||
@click.prevent="save"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GenericModal>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericModal from "../GenericModal.vue";
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import api from "../../api";
|
||||
|
||||
export default {
|
||||
name: "TitleModal",
|
||||
components: { FormRow, GenericModal },
|
||||
components: { FormRow, JsonModal },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
error: "",
|
||||
title: "",
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const { data } = await api.get("/config/site");
|
||||
this.title = data.result.title;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.error = "";
|
||||
try {
|
||||
await api.put("/config/site", { title: this.title });
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = e.response.data.error;
|
||||
}
|
||||
this.saving = false;
|
||||
transformReadValues(siteTitle) {
|
||||
return { title: siteTitle };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<template>
|
||||
<SponsorTokenExpires :sponsorTokenExpires="sponsorTokenExpires" />
|
||||
<div class="container mx-0 px-0">
|
||||
<FormRow id="settingsDesign" :label="$t('settings.theme.label')">
|
||||
<SelectGroup
|
||||
|
|
@ -40,7 +39,7 @@
|
|||
/>
|
||||
</FormRow>
|
||||
<FormRow id="telemetryEnabled" :label="$t('settings.telemetry.label')">
|
||||
<TelemetrySettings :sponsor="sponsor" class="mt-1 mb-0" />
|
||||
<TelemetrySettings :sponsorActive="!!sponsor.name" class="mt-1 mb-0" />
|
||||
</FormRow>
|
||||
<FormRow id="hiddenFeaturesEnabled" :label="`${$t('settings.hiddenFeatures.label')} 🧪`">
|
||||
<div class="form-check form-switch my-1">
|
||||
|
|
@ -79,7 +78,6 @@
|
|||
|
||||
<script>
|
||||
import TelemetrySettings from "../TelemetrySettings.vue";
|
||||
import SponsorTokenExpires from "../SponsorTokenExpires.vue";
|
||||
import FormRow from "../FormRow.vue";
|
||||
import SelectGroup from "../SelectGroup.vue";
|
||||
import {
|
||||
|
|
@ -95,10 +93,9 @@ import { isApp } from "../../utils/native";
|
|||
|
||||
export default {
|
||||
name: "UserInterfaceSettings",
|
||||
components: { TelemetrySettings, FormRow, SelectGroup, SponsorTokenExpires },
|
||||
components: { TelemetrySettings, FormRow, SelectGroup },
|
||||
props: {
|
||||
sponsor: String,
|
||||
sponsorTokenExpires: Number,
|
||||
sponsor: Object,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
|
|
|||
122
assets/js/components/Config/YamlEditor.vue
Normal file
122
assets/js/components/Config/YamlEditor.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<VueMonacoEditor
|
||||
v-if="active"
|
||||
class="editor"
|
||||
language="yaml"
|
||||
:theme="theme"
|
||||
:options="options"
|
||||
:value="modelValue"
|
||||
ref="editor"
|
||||
@update:value="$emit('update:modelValue', $event)"
|
||||
@mount="ready"
|
||||
>
|
||||
<template #default> {{ $t("config.editor.loading") }} </template>
|
||||
<template #failure>
|
||||
<textarea
|
||||
class="form-control"
|
||||
:rows="lines"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</template>
|
||||
</VueMonacoEditor>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueMonacoEditor, loader } from "@guolao/vue-monaco-editor";
|
||||
const $html = document.querySelector("html");
|
||||
export default {
|
||||
name: "YamlEditor",
|
||||
props: {
|
||||
modelValue: String,
|
||||
errorLine: Number,
|
||||
disabled: Boolean,
|
||||
},
|
||||
components: { VueMonacoEditor },
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
theme: "vs",
|
||||
defaultOptions: {
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true,
|
||||
minimap: { enabled: false },
|
||||
showFoldingControls: "always",
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "off",
|
||||
wrappingStrategy: "advanced",
|
||||
overviewRulerLanes: 0,
|
||||
},
|
||||
active: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateTheme();
|
||||
$html.addEventListener("themechange", this.updateTheme);
|
||||
},
|
||||
watch: {
|
||||
errorLine() {
|
||||
// force rerender to update decorations
|
||||
this.active = false;
|
||||
this.$nextTick(() => (this.active = true));
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
return { ...this.defaultOptions, readOnly: this.disabled };
|
||||
},
|
||||
lines() {
|
||||
return (this.modelValue || "").split("\n").length;
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
loader.config({
|
||||
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" },
|
||||
});
|
||||
loader.init();
|
||||
},
|
||||
unmounted() {
|
||||
$html.removeEventListener("themechange", this.updateTheme);
|
||||
},
|
||||
methods: {
|
||||
updateTheme() {
|
||||
this.theme = $html.classList.contains("dark") ? "vs-dark" : "vs";
|
||||
},
|
||||
ready(editor, monaco) {
|
||||
let decorations = null;
|
||||
const highlight = () => {
|
||||
decorations?.clear();
|
||||
if (this.errorLine) {
|
||||
decorations = editor.createDecorationsCollection([
|
||||
{
|
||||
range: new monaco.Range(this.errorLine, 1, this.errorLine, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: "error",
|
||||
lineNumberClassName: "error",
|
||||
marginClassName: "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
editor.onDidChangeModelContent(() => highlight());
|
||||
highlight();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.editor :global(.monaco-editor) {
|
||||
--vscode-editor-background: var(--evcc-box) !important;
|
||||
--vscode-editorGutter-background: var(--evcc-box-border) !important;
|
||||
}
|
||||
.editor :global(.error) {
|
||||
background-color: var(--bs-danger-50) !important;
|
||||
}
|
||||
.editor {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
}
|
||||
</style>
|
||||
145
assets/js/components/Config/YamlModal.vue
Normal file
145
assets/js/components/Config/YamlModal.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<GenericModal ref="modal" :size="size" :title="title" @open="open">
|
||||
<p v-if="description || docsLink">
|
||||
<span v-if="description">{{ description + " " }}</span>
|
||||
<a v-if="docsLink" :href="docsLink" target="_blank">
|
||||
{{ $t("config.general.docsLink") }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-danger" v-if="error" data-testid="error">{{ error }}</p>
|
||||
<form ref="form" class="container mx-0 px-0">
|
||||
<div class="editor-container" :style="{ height }">
|
||||
<YamlEditor v-model="yaml" class="editor" :errorLine="errorLine" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-between">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-muted btn-cancel"
|
||||
data-bs-dismiss="modal"
|
||||
>
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="saving || nothingChanged"
|
||||
@click.prevent="save"
|
||||
>
|
||||
<span
|
||||
v-if="saving"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericModal from "../GenericModal.vue";
|
||||
import api from "../../api";
|
||||
import { docsPrefix } from "../../i18n";
|
||||
import YamlEditor from "./YamlEditor.vue";
|
||||
|
||||
export default {
|
||||
name: "YamlModal",
|
||||
components: { GenericModal, YamlEditor },
|
||||
emits: ["changed"],
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
error: "",
|
||||
errorLine: undefined,
|
||||
yaml: "",
|
||||
serverYaml: "",
|
||||
};
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
description: String,
|
||||
docs: String,
|
||||
endpoint: String,
|
||||
defaultYaml: String,
|
||||
size: { type: String, default: "xl" },
|
||||
},
|
||||
computed: {
|
||||
docsLink() {
|
||||
return `${docsPrefix()}${this.docs}`;
|
||||
},
|
||||
height() {
|
||||
return Math.max(150, this.yaml.split("\n").length * 18) + 22 + "px";
|
||||
},
|
||||
nothingChanged() {
|
||||
return this.yaml === this.serverYaml && this.yaml !== "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.yaml = "";
|
||||
this.serverYaml = "";
|
||||
this.error = "";
|
||||
this.saving = false;
|
||||
this.errorLine = undefined;
|
||||
},
|
||||
async open() {
|
||||
this.reset();
|
||||
await this.load();
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
const { data } = await api.get(this.endpoint);
|
||||
this.serverYaml = data.result;
|
||||
this.yaml = data.result || this.defaultYaml;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.error = "";
|
||||
this.errorLine = undefined;
|
||||
try {
|
||||
const data = this.yaml === this.defaultYaml ? "" : this.yaml;
|
||||
const res = await api.post(this.endpoint, data, {
|
||||
validateStatus: (code) => [200, 400].includes(code),
|
||||
});
|
||||
if (res.status === 200) {
|
||||
this.$emit("changed");
|
||||
this.$refs.modal.close();
|
||||
}
|
||||
if (res.status === 400) {
|
||||
this.error = res.data.error;
|
||||
this.errorLine = res.data.line;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
padding-right: 0;
|
||||
}
|
||||
.editor-container {
|
||||
margin: 0 -1rem 0 -1.25rem;
|
||||
}
|
||||
/* reset margins on lg */
|
||||
@media (min-width: 992px) {
|
||||
.editor-container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
18
assets/js/components/Config/defaultYaml/circuits.yaml
Normal file
18
assets/js/components/Config/defaultYaml/circuits.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# - name: main # unique name, used as reference, e.g. as parent in other circuits
|
||||
# title: Main Circuit # used in the UI
|
||||
# maxCurrent: 63 # 63A main circuit breaker (optional)
|
||||
# maxPower: 30000 # 30kW (optional)
|
||||
# meter: grid # associated meter to monitor the power consumption (optional)
|
||||
# parent: # no parent, this is the root circuit
|
||||
# - name: garage # unique name, used as reference, e.g. to associate loadpoints
|
||||
# title: Garage # used in the UI
|
||||
# maxCurrent: 24 # allow individual phase use up to 24A
|
||||
# maxPower: 11000 # limit total power to 11kW
|
||||
# meter: garage # dedicated meter for the garage
|
||||
# parent: main # parent to the main circuit
|
||||
# - name: carport # unique name, used as reference, e.g. to associate loadpoints
|
||||
# title: Carport # used in the UI
|
||||
# maxCurrent: 32 # 32A circuit breaker
|
||||
# maxPower: # no limit, only check current
|
||||
# meter: # no meter, using data from loadpoints
|
||||
# parent: main # parent to the main circuit
|
||||
12
assets/js/components/Config/defaultYaml/eebus.yaml
Normal file
12
assets/js/components/Config/defaultYaml/eebus.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# shipid: EVCC-1234567890abcdef
|
||||
# interfaces:
|
||||
# - eth0
|
||||
# certificate:
|
||||
# public: |
|
||||
# -----BEGIN CERTIFICATE-----
|
||||
# 1234567890abcdef==
|
||||
# -----END CERTIFICATE-----
|
||||
# private: |
|
||||
# -----BEGIN EC PRIVATE KEY-----
|
||||
# 1234567890abcdef
|
||||
# -----END EC PRIVATE KEY-----
|
||||
4
assets/js/components/Config/defaultYaml/hems.yaml
Normal file
4
assets/js/components/Config/defaultYaml/hems.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# type: sma
|
||||
# allowcontrol: false
|
||||
# vendorid: AAAAAAAA
|
||||
# deviceid: BBBBBBBB
|
||||
35
assets/js/components/Config/defaultYaml/messaging.yaml
Normal file
35
assets/js/components/Config/defaultYaml/messaging.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# events:
|
||||
# start:
|
||||
# title: Charge started
|
||||
# msg: Started charging in "${mode}" mode
|
||||
# stop:
|
||||
# title: Charge finished
|
||||
# msg: Finished charging ${chargedEnergy:%.1fk}kWh in ${chargeDuration}.
|
||||
# connect:
|
||||
# title: Car connected
|
||||
# msg: "Car connected at ${pvPower:%.1fk}kW PV"
|
||||
# disconnect:
|
||||
# title: Car disconnected
|
||||
# msg: Car disconnected after ${connectedDuration}
|
||||
# soc:
|
||||
# title: Soc updated
|
||||
# msg: Battery charged to ${vehicleSoc:%.0f}%
|
||||
# guest:
|
||||
# title: Unknown vehicle
|
||||
# msg: Unknown vehicle, guest connected?
|
||||
|
||||
# services:
|
||||
# - type: pushover
|
||||
# app: # app id
|
||||
# recipients:
|
||||
# - # list of recipient ids
|
||||
# - type: telegram
|
||||
# token: # bot id
|
||||
# chats:
|
||||
# - # list of chat ids
|
||||
# - type: email
|
||||
# uri: smtp://<user>:<password>@<host>:<port>/?fromAddress=<from>&toAddresses=<to>
|
||||
# - type: ntfy
|
||||
# uri: https://<host>/<topics>
|
||||
# priority: <priority>
|
||||
# tags: <tags>
|
||||
9
assets/js/components/Config/defaultYaml/modbusproxy.yaml
Normal file
9
assets/js/components/Config/defaultYaml/modbusproxy.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# - port: 5021
|
||||
# uri: 192.0.2.2:502
|
||||
# - port: 5022
|
||||
# device: /dev/ttyUSB0
|
||||
# baudrate: 9600
|
||||
# comset: "8N1"
|
||||
# - port: 5023
|
||||
# uri: 192.0.2.3:502
|
||||
# rtu: true
|
||||
13
assets/js/components/Config/defaultYaml/tariffs.yaml
Normal file
13
assets/js/components/Config/defaultYaml/tariffs.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# currency: EUR
|
||||
|
||||
# grid: # price using energy from the grid
|
||||
# type: fixed
|
||||
# price: 0.294 # EUR/kWh
|
||||
|
||||
# feedin: # price for feeding solar energy to the grid
|
||||
# type: fixed
|
||||
# price: 0.08 # EUR/kWh
|
||||
|
||||
# co2: # carbon intensity forecast data
|
||||
# type: grünstromindex
|
||||
# zip: <zip>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="container py-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<Version v-bind="version" />
|
||||
<Savings v-bind="savings" :sponsor="sponsor" />
|
||||
<Savings v-bind="savings" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -20,7 +20,6 @@ export default {
|
|||
components: { Version, Savings },
|
||||
props: {
|
||||
version: Object,
|
||||
sponsor: String,
|
||||
savings: Object,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@
|
|||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<UserInterfaceSettings
|
||||
:sponsor="sponsor"
|
||||
:sponsorTokenExpires="sponsorTokenExpires"
|
||||
/>
|
||||
<UserInterfaceSettings :sponsor="sponsor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,8 +35,7 @@ export default {
|
|||
name: "GlobalSettingsModal",
|
||||
components: { UserInterfaceSettings },
|
||||
props: {
|
||||
sponsor: String,
|
||||
sponsorTokenExpires: Number,
|
||||
sponsor: { type: Object, default: () => ({}) },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@
|
|||
<script>
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { docsPrefix } from "../i18n";
|
||||
import api from "../api";
|
||||
import { performRestart } from "../restart";
|
||||
|
||||
export default {
|
||||
name: "HelpModal",
|
||||
|
|
@ -159,11 +159,7 @@ export default {
|
|||
modal.show();
|
||||
},
|
||||
async restartConfirmed() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
} catch (e) {
|
||||
alert("Unabled to restart server.");
|
||||
}
|
||||
await performRestart();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
17
assets/js/components/MaterialIcon/Circuits.vue
Normal file
17
assets/js/components/MaterialIcon/Circuits.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 21q-1.25 0-2.125-.875T1 18t.875-2.125T4 15q.225 0 .438.038t.412.087L8.9 9.55q-.425-.525-.663-1.175T8 7q0-1.65 1.175-2.825T12 3t2.825 1.175T16 7q0 .725-.25 1.375t-.675 1.175l4.075 5.575q.2-.05.413-.088T20 15q1.25 0 2.125.875T23 18t-.875 2.125T20 21t-2.125-.875T17 18q0-.475.138-.913t.387-.787l-4.05-5.575q-.125.05-.237.075t-.238.075v4.3q.875.3 1.438 1.075T15 18q0 1.25-.875 2.125T12 21t-2.125-.875T9 18q0-.975.563-1.737T11 15.174v-4.3q-.125-.05-.238-.075t-.237-.075L6.475 16.3q.25.35.388.788T7 18q0 1.25-.875 2.125T4 21m0-2q.425 0 .713-.288T5 18t-.288-.712T4 17t-.712.288T3 18t.288.713T4 19m8 0q.425 0 .713-.288T13 18t-.288-.712T12 17t-.712.288T11 18t.288.713T12 19m8 0q.425 0 .713-.288T21 18t-.288-.712T20 17t-.712.288T19 18t.288.713T20 19M12 9q.825 0 1.413-.587T14 7t-.587-1.412T12 5t-1.412.588T10 7t.588 1.413T12 9"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Circuits",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/Edit.vue
Normal file
17
assets/js/components/MaterialIcon/Edit.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 19h1.425L16.2 9.225L14.775 7.8L5 17.575zm-1 2q-.425 0-.712-.288T3 20v-2.425q0-.4.15-.763t.425-.637L16.2 3.575q.3-.275.663-.425t.762-.15t.775.15t.65.45L20.425 5q.3.275.437.65T21 6.4q0 .4-.138.763t-.437.662l-12.6 12.6q-.275.275-.638.425t-.762.15zM19 6.4L17.6 5zm-3.525 2.125l-.7-.725L16.2 9.225z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Edit",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
21
assets/js/components/MaterialIcon/Eebus.vue
Normal file
21
assets/js/components/MaterialIcon/Eebus.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M15.998,8.095c1.369,-0 2.669,0.219 3.902,0.657c1.232,0.438 2.349,1.04 3.352,1.807c0.365,0.273 0.553,0.634 0.562,1.081c0.01,0.448 -0.15,0.835 -0.48,1.163c-0.31,0.31 -0.693,0.47 -1.149,0.48c-0.456,0.009 -0.867,-0.114 -1.232,-0.37c-0.693,-0.475 -1.46,-0.849 -2.299,-1.123c-0.84,-0.273 -1.725,-0.41 -2.656,-0.41c-0.93,-0 -1.815,0.137 -2.655,0.41c-0.839,0.274 -1.606,0.648 -2.299,1.123c-0.365,0.255 -0.776,0.374 -1.232,0.356c-0.456,-0.019 -0.84,-0.183 -1.15,-0.493c-0.31,-0.329 -0.465,-0.716 -0.465,-1.163c-0,-0.447 0.182,-0.807 0.547,-1.082c1.004,-0.766 2.122,-1.364 3.354,-1.793c1.232,-0.43 2.532,-0.644 3.9,-0.643m0,-6.57c2.281,0 4.43,0.374 6.447,1.123c2.017,0.748 3.828,1.806 5.433,3.175c0.365,0.31 0.557,0.693 0.575,1.15c0.019,0.456 -0.137,0.848 -0.465,1.177c-0.31,0.31 -0.693,0.47 -1.15,0.479c-0.456,0.01 -0.867,-0.132 -1.232,-0.425c-1.313,-1.076 -2.787,-1.911 -4.42,-2.504c-1.633,-0.592 -3.362,-0.889 -5.188,-0.89c-1.825,-0.001 -3.554,0.296 -5.186,0.89c-1.633,0.595 -3.106,1.429 -4.422,2.504c-0.365,0.292 -0.775,0.434 -1.232,0.425c-0.456,-0.008 -0.839,-0.168 -1.149,-0.479c-0.329,-0.329 -0.484,-0.721 -0.466,-1.177c0.019,-0.457 0.21,-0.84 0.575,-1.15c1.606,-1.369 3.418,-2.427 5.434,-3.175c2.017,-0.749 4.166,-1.123 6.446,-1.123"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M15.992,23.961c-1.368,0 -2.669,-0.219 -3.901,-0.657c-1.232,-0.437 -2.35,-1.04 -3.353,-1.806c-0.365,-0.274 -0.552,-0.635 -0.561,-1.082c-0.01,-0.447 0.15,-0.835 0.479,-1.163c0.31,-0.31 0.694,-0.47 1.15,-0.479c0.456,-0.01 0.867,0.113 1.232,0.37c0.693,0.474 1.46,0.848 2.299,1.122c0.84,0.274 1.725,0.411 2.655,0.411c0.931,-0 1.816,-0.137 2.656,-0.411c0.839,-0.274 1.605,-0.648 2.299,-1.122c0.365,-0.256 0.776,-0.374 1.232,-0.356c0.456,0.018 0.839,0.182 1.149,0.493c0.311,0.328 0.466,0.716 0.466,1.162c-0,0.447 -0.183,0.808 -0.548,1.082c-1.003,0.767 -2.121,1.365 -3.353,1.794c-1.233,0.429 -2.533,0.643 -3.901,0.642m0,6.57c-2.281,0 -4.43,-0.374 -6.447,-1.122c-2.017,-0.748 -3.828,-1.807 -5.433,-3.176c-0.365,-0.31 -0.556,-0.693 -0.575,-1.149c-0.018,-0.456 0.137,-0.849 0.466,-1.177c0.31,-0.311 0.693,-0.47 1.149,-0.48c0.457,-0.009 0.867,0.132 1.232,0.425c1.314,1.077 2.788,1.911 4.421,2.504c1.632,0.593 3.362,0.89 5.187,0.89c1.826,0.001 3.555,-0.296 5.187,-0.89c1.632,-0.594 3.106,-1.429 4.421,-2.504c0.365,-0.292 0.776,-0.434 1.232,-0.425c0.456,0.009 0.84,0.169 1.15,0.48c0.328,0.328 0.484,0.721 0.465,1.177c-0.018,0.456 -0.21,0.839 -0.575,1.149c-1.605,1.369 -3.417,2.428 -5.434,3.176c-2.017,0.748 -4.165,1.122 -6.446,1.122"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Eebus",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/Hems.vue
Normal file
17
assets/js/components/MaterialIcon/Hems.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 11q0 1.8 1.15 3.175T7.075 15.9l-.775-.775q-.275-.275-.275-.687t.275-.713q.3-.3.713-.3t.712.3L10.3 16.3q.3.3.3.7t-.3.7l-2.6 2.6q-.275.275-.7.275t-.725-.3Q6 20 6 19.6t.275-.7l.9-.95q-2.65-.3-4.412-2.287T1 11q0-2.925 2.038-4.962T8 4h2q.425 0 .713.288T11 5t-.288.713T10 6H8Q5.925 6 4.463 7.463T3 11m11 9q-.425 0-.712-.288T13 19v-5q0-.425.288-.712T14 13h7q.425 0 .713.288T22 14v5q0 .425-.288.713T21 20zm0-9q-.425 0-.712-.288T13 10V5q0-.425.288-.712T14 4h7q.425 0 .713.288T22 5v5q0 .425-.288.713T21 11zm1-2h5V6h-5z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Hems",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/Influx.vue
Normal file
17
assets/js/components/MaterialIcon/Influx.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m23.778 14.482l-2.287-9.959c-.13-.545-.624-1.09-1.169-1.248L9.87.051C9.74 0 9.584 0 9.426 0c-.443 0-.909.18-1.222.443L.716 7.412C.3 7.776.092 8.504.222 9.024l2.445 10.662c.13.545.624 1.092 1.169 1.248l9.775 3.015c.13.051.285.051.443.051c.443 0 .91-.18 1.223-.443l8.007-7.435c.418-.39.624-1.092.494-1.64M10.962 2.417l7.175 2.21c.285.08.285.21 0 .286l-3.77.858c-.285.08-.674-.05-.883-.26l-2.626-2.834c-.235-.232-.184-.336.104-.26m4.47 12.872c.079.286-.105.444-.39.365l-7.748-2.392c-.285-.079-.338-.313-.13-.52l5.93-5.514c.209-.209.443-.13.52.156zM2.667 8.267l6.293-5.85c.21-.209.545-.18.754.025L12.86 5.85c.209.21.18.545-.026.754l-6.293 5.85c-.21.21-.545.181-.754-.025L2.64 9.024a.536.536 0 0 1 .026-.757zm1.536 9.284L2.54 10.244c-.08-.285.05-.34.234-.13L5.4 12.949c.209.209.285.624.209.909L4.462 17.55c-.079.285-.208.285-.26 0zm9.202 4.264l-8.217-2.522a.547.547 0 0 1-.364-.675l1.378-4.421a.547.547 0 0 1 .675-.365l8.216 2.522c.285.079.443.39.364.675L14.08 21.45a.553.553 0 0 1-.674.365zm7.279-5.98L15.2 20.93c-.209.209-.31.13-.234-.155l1.144-3.694c.079-.285.39-.573.674-.624l3.77-.858c.288-.076.339.054.13.234zm.598-1.09l-4.523 1.039a.534.534 0 0 1-.65-.39l-1.922-8.372a.534.534 0 0 1 .39-.65L19.1 5.335a.534.534 0 0 1 .649.39l1.923 8.371c.079.31-.102.596-.39.65Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Influx",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/ModbusProxy.vue
Normal file
17
assets/js/components/MaterialIcon/ModbusProxy.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15 19v-1h-2q-.825 0-1.412-.587T11 16V8H9v1q0 .825-.587 1.413T7 11H4q-.825 0-1.412-.587T2 9V5q0-.825.588-1.412T4 3h3q.825 0 1.413.588T9 5v1h6V5q0-.825.588-1.412T17 3h3q.825 0 1.413.588T22 5v4q0 .825-.587 1.413T20 11h-3q-.825 0-1.412-.587T15 9V8h-2v8h2v-1q0-.825.588-1.412T17 13h3q.825 0 1.413.588T22 15v4q0 .825-.587 1.413T20 21h-3q-.825 0-1.412-.587T15 19M4 5v4zm13 10v4zm0-10v4zm0 4h3V5h-3zm0 10h3v-4h-3zM4 9h3V5H4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "ModbusProxy",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
33
assets/js/components/MaterialIcon/Mqtt.vue
Normal file
33
assets/js/components/MaterialIcon/Mqtt.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="-40 -40 400 400">
|
||||
<g>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.1,180.6v117.1c0,8.4,6.8,15.3,15.3,15.3H142C141,239.8,80.9,180.7,7.1,180.6z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.1,84.1v49.8c99,0.9,179.4,80.7,180.4,179.1h51.7C238.2,186.6,134.5,84.2,7.1,84.1z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M312.9,297.6V193.5C278.1,107.2,207.3,38.9,119,7.1H22.4c-8.4,0-15.3,6.8-15.3,15.3v15
|
||||
c152.6,0.9,276.6,124,277.6,275.6h13C306.1,312.9,312.9,306.1,312.9,297.6z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M272.6,49.8c14.5,14.4,28.6,31.7,40.4,47.8V22.4c0-8.4-6.8-15.3-15.3-15.3h-77.3
|
||||
C238.4,19.7,256.6,33.9,272.6,49.8z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Mqtt",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/Notification.vue
Normal file
17
assets/js/components/MaterialIcon/Notification.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 19q-.425 0-.712-.288T4 18t.288-.712T5 17h1v-7q0-2.075 1.25-3.687T10.5 4.2v-.7q0-.625.438-1.062T12 2t1.063.438T13.5 3.5v.7q2 .5 3.25 2.113T18 10v7h1q.425 0 .713.288T20 18t-.288.713T19 19zm7 3q-.825 0-1.412-.587T10 20h4q0 .825-.587 1.413T12 22m-4-5h8v-7q0-1.65-1.175-2.825T12 6T9.175 7.175T8 10zm-5-7q-.425 0-.712-.325t-.238-.75q.2-1.875 1.05-3.488t2.175-2.812q.325-.275.738-.25t.662.375t.2.75t-.375.7q-.975.925-1.6 2.15T4.075 9q-.05.425-.35.713T3 10m18 0q-.425 0-.725-.288T19.925 9q-.2-1.425-.825-2.65T17.5 4.2q-.325-.3-.375-.7t.2-.75t.663-.375t.737.25q1.325 1.2 2.175 2.812t1.05 3.488q.05.425-.237.75T21 10"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Notification",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
17
assets/js/components/MaterialIcon/Restart.vue
Normal file
17
assets/js/components/MaterialIcon/Restart.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9.825 20.7q-2.575-.725-4.2-2.837T4 13q0-1.425.475-2.713t1.35-2.362q.275-.3.675-.313t.725.313q.275.275.288.675t-.263.75q-.6.775-.925 1.7T6 13q0 2.025 1.188 3.613t3.062 2.162q.325.1.538.375t.212.6q0 .5-.35.788t-.825.162m4.35 0q-.475.125-.825-.175t-.35-.8q0-.3.213-.575t.537-.375q1.875-.6 3.063-2.175T18 13q0-2.5-1.75-4.25T12 7h-.075l.4.4q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-2.1-2.1q-.15-.15-.212-.325T8.55 6t.063-.375t.212-.325l2.1-2.1q.275-.275.7-.275t.7.275t.275.7t-.275.7l-.4.4H12q3.35 0 5.675 2.325T20 13q0 2.725-1.625 4.85t-4.2 2.85"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icon from "../../mixins/icon";
|
||||
|
||||
export default {
|
||||
name: "Restart",
|
||||
mixins: [icon],
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,23 +1,159 @@
|
|||
<template>
|
||||
<div
|
||||
class="fixed-bottom alert alert-secondary d-flex justify-content-center align-items-center mb-0 rounded-0"
|
||||
role="alert"
|
||||
>
|
||||
<shopicon-regular-cloud class="me-2"></shopicon-regular-cloud>
|
||||
{{ $t("offline.message") }}
|
||||
<div>
|
||||
<div class="modal-backdrop" v-if="offline" />
|
||||
<div
|
||||
class="fixed-bottom alert d-flex justify-content-center align-items-center mb-0 rounded-0 p-2"
|
||||
:class="{ visible: visible, 'alert-danger': showError, 'alert-secondary': !showError }"
|
||||
role="alert"
|
||||
data-testid="bottom-banner"
|
||||
>
|
||||
<div v-if="restarting" class="d-flex align-items-center">
|
||||
<button
|
||||
class="btn btn-secondary me-2 btn-sm d-flex align-items-center"
|
||||
type="button"
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm m-1 me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("offline.restart") }}
|
||||
</button>
|
||||
{{ $t("offline.restarting") }}
|
||||
</div>
|
||||
<div v-else-if="restartNeeded" class="d-flex align-items-center">
|
||||
<button
|
||||
class="btn btn-secondary me-2 btn-sm d-flex align-items-center"
|
||||
type="button"
|
||||
@click="restart"
|
||||
>
|
||||
<Sync class="restart me-2" />
|
||||
{{ $t("offline.restart") }}
|
||||
</button>
|
||||
{{ $t("offline.restartNeeded") }}
|
||||
</div>
|
||||
<div v-else-if="offline" class="d-flex align-items-center">
|
||||
<CloudOffline class="m-2" />
|
||||
{{ $t("offline.message") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="showError"
|
||||
class="d-flex align-items-start container px-4 justify-content-center"
|
||||
>
|
||||
<shopicon-regular-car1
|
||||
size="m"
|
||||
class="fatal-icon flex-grow-0 flex-shrink-0"
|
||||
></shopicon-regular-car1>
|
||||
<div class="mx-4 mt-1">
|
||||
<div>
|
||||
<strong>
|
||||
Error during startup. Check your configuration and restart.
|
||||
</strong>
|
||||
</div>
|
||||
<div v-if="fatal">{{ fatal.error }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close mt-1"
|
||||
aria-label="Close"
|
||||
@click="dismissed = true"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/regular/cloud";
|
||||
import "@h2d2/shopicons/es/regular/car1";
|
||||
import CloudOffline from "./MaterialIcon/CloudOffline.vue";
|
||||
import Sync from "./MaterialIcon/Sync.vue";
|
||||
import restart, { performRestart, restartComplete } from "../restart";
|
||||
|
||||
export default {
|
||||
name: "OfflineIndicator",
|
||||
components: {
|
||||
CloudOffline,
|
||||
Sync,
|
||||
},
|
||||
props: {
|
||||
offline: Boolean,
|
||||
fatal: Object,
|
||||
},
|
||||
data() {
|
||||
return { dismissed: false };
|
||||
},
|
||||
watch: {
|
||||
offline: function () {
|
||||
if (!this.offline) {
|
||||
restartComplete();
|
||||
this.dismissed = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
restartNeeded() {
|
||||
return restart.restartNeeded;
|
||||
},
|
||||
restarting() {
|
||||
return restart.restarting;
|
||||
},
|
||||
visible() {
|
||||
return this.offline || this.restartNeeded || this.restarting || this.showError;
|
||||
},
|
||||
showError() {
|
||||
return (
|
||||
!this.offline &&
|
||||
!this.restartNeeded &&
|
||||
!this.restarting &&
|
||||
this.fatal?.error &&
|
||||
!this.dismissed
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
restart() {
|
||||
performRestart();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.fixed-bottom {
|
||||
/* $zindex-toast https://getbootstrap.com/docs/5.3/layout/z-index/ */
|
||||
z-index: 1090 !important;
|
||||
<style scoped>
|
||||
.restart {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.alert {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition:
|
||||
transform var(--evcc-transition-fast) ease-in,
|
||||
opacity var(--evcc-transition-fast) ease-in;
|
||||
min-height: 58px;
|
||||
/* above backdrop, below modal https://getbootstrap.com/docs/5.3/layout/z-index/ */
|
||||
z-index: 1054 !important;
|
||||
}
|
||||
.alert.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.fatal-icon {
|
||||
transform-origin: 60% 40%;
|
||||
animation: swinging 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes swinging {
|
||||
0% {
|
||||
transform: translateY(6px) rotate(170deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(6px) rotate(185deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(6px) rotate(170deg);
|
||||
}
|
||||
}
|
||||
.btn-close {
|
||||
filter: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -179,10 +179,9 @@
|
|||
</div>
|
||||
<div v-else class="my-4">
|
||||
<LiveCommunity />
|
||||
<TelemetrySettings :sponsor="sponsor" />
|
||||
<TelemetrySettings :sponsorActive="!!sponsor.name" />
|
||||
</div>
|
||||
|
||||
<Sponsor :sponsor="sponsor" />
|
||||
<Sponsor v-bind="sponsor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -211,7 +210,7 @@ export default {
|
|||
props: {
|
||||
statistics: { type: Object, default: () => ({}) },
|
||||
co2Configured: Boolean,
|
||||
sponsor: String,
|
||||
sponsor: Object,
|
||||
currency: String,
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
<Energyflow v-bind="energyflow" />
|
||||
</div>
|
||||
<div class="d-flex flex-column justify-content-between content-area">
|
||||
<div v-if="fatal" class="flex-grow-1 align-items-center d-flex justify-content-center">
|
||||
<h1 class="mb-5 text-gray fs-4">{{ $t("startupError.title") }}</h1>
|
||||
</div>
|
||||
<Loadpoints
|
||||
v-else
|
||||
class="mt-1 mt-sm-2 flex-grow-1"
|
||||
:loadpoints="loadpoints"
|
||||
:vehicles="vehicleList"
|
||||
|
|
@ -93,10 +97,10 @@ export default {
|
|||
hasUpdater: Boolean,
|
||||
uploadMessage: String,
|
||||
uploadProgress: Number,
|
||||
sponsor: String,
|
||||
sponsorTokenExpires: Number,
|
||||
sponsor: { type: Object, default: () => ({}) },
|
||||
smartCostType: String,
|
||||
smartCostActive: Boolean,
|
||||
fatal: Object,
|
||||
},
|
||||
computed: {
|
||||
batteryConfigured: function () {
|
||||
|
|
@ -143,8 +147,8 @@ export default {
|
|||
uploadMessage: this.uploadMessage,
|
||||
uploadProgress: this.uploadProgress,
|
||||
},
|
||||
sponsor: this.sponsor,
|
||||
savings: {
|
||||
sponsor: this.sponsor,
|
||||
statistics: this.statistics,
|
||||
co2Configured: this.tariffCo2 !== undefined,
|
||||
priceConfigured: this.tariffGrid !== undefined,
|
||||
|
|
@ -164,4 +168,6 @@ export default {
|
|||
flex-grow: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
.fatal {
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import Sponsor from "./Sponsor.vue";
|
|||
<Sponsor sponsor="naltatis" />
|
||||
</Variant>
|
||||
<Variant title="victron device">
|
||||
<Sponsor sponsor="victron-device" />
|
||||
<Sponsor sponsor="victron" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
{{ $t(`footer.sponsor.${isVictronDevice ? "titleVictron" : "titleSponsor"}`) }}
|
||||
</p>
|
||||
<p class="mb-3">
|
||||
{{ $t(`footer.sponsor.${isVictronDevice ? "victron" : "thanks"}`, { sponsor }) }}
|
||||
{{ $t(`footer.sponsor.${isVictronDevice ? "victron" : "thanks"}`, { sponsor: name }) }}
|
||||
</p>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center flex-column flex-lg-row align-items-lg-baseline justify-content-lg-start"
|
||||
>
|
||||
<button
|
||||
ref="confetti"
|
||||
type="button"
|
||||
class="btn btn btn-outline-primary mb-2 confetti-button bg-evcc rounded flex-shrink-0"
|
||||
@click="surprise"
|
||||
>
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!sponsor || isTrial">
|
||||
<div v-if="!name || isTrial">
|
||||
<p class="fw-bold mb-1">
|
||||
<shopicon-regular-clock
|
||||
v-if="isTrial"
|
||||
|
|
@ -67,20 +68,25 @@ import "@h2d2/shopicons/es/regular/stars";
|
|||
import "@h2d2/shopicons/es/regular/clock";
|
||||
import { docsPrefix } from "../i18n";
|
||||
|
||||
export const TRIAL = "trial";
|
||||
export const VICTRON_DEVICE = "victron";
|
||||
|
||||
export default {
|
||||
name: "Sponsor",
|
||||
props: {
|
||||
sponsor: String,
|
||||
name: String,
|
||||
expiresAt: String,
|
||||
expiresSoon: Boolean,
|
||||
},
|
||||
computed: {
|
||||
isTrial() {
|
||||
return this.sponsor === "trial";
|
||||
return this.name === TRIAL;
|
||||
},
|
||||
isVictronDevice() {
|
||||
return this.sponsor === "victron-device";
|
||||
return this.name === VICTRON_DEVICE;
|
||||
},
|
||||
isIndividual() {
|
||||
return this.sponsor && !this.isTrial && !this.isVictronDevice;
|
||||
return this.name && !this.isTrial && !this.isVictronDevice;
|
||||
},
|
||||
sponsorLink() {
|
||||
return `${docsPrefix()}/docs/sponsorship`;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import SponsorTokenExpires from "./SponsorTokenExpires.vue";
|
|||
|
||||
<template>
|
||||
<Story title="Sponsor Token Expires">
|
||||
<Variant title="in 9 days">
|
||||
<SponsorTokenExpires :sponsorTokenExpires="768423" />
|
||||
<Variant title="some day">
|
||||
<SponsorTokenExpires :sponsorSoon="true" expiresAt="2055-01-01T00:00:00Z" />
|
||||
</Variant>
|
||||
<Variant title="empty">
|
||||
<SponsorTokenExpires />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-if="sponsorTokenExpires" class="alert alert-warning" role="alert">
|
||||
<i18n-t tag="div" class="mb-2" keypath="settings.sponsorToken.expires">
|
||||
<div v-if="expiresSoon" class="alert alert-warning my-4" role="alert">
|
||||
<i18n-t tag="div" keypath="settings.sponsorToken.expires">
|
||||
<template #inXDays>
|
||||
{{ inXDays }}
|
||||
</template>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
|
||||
<em>
|
||||
<em class="d-block mt-2" v-if="!isTrial">
|
||||
{{ $t("settings.sponsorToken.hint") }}
|
||||
</em>
|
||||
</div>
|
||||
|
|
@ -19,16 +19,22 @@
|
|||
|
||||
<script>
|
||||
import formatter from "../mixins/formatter";
|
||||
import { TRIAL } from "./Sponsor.vue";
|
||||
|
||||
export default {
|
||||
name: "SponsorTokenExpires",
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
sponsorTokenExpires: Number,
|
||||
expiresSoon: Boolean,
|
||||
expiresAt: String,
|
||||
name: String,
|
||||
},
|
||||
computed: {
|
||||
inXDays() {
|
||||
return this.fmtTimeAgo(this.sponsorTokenExpires * 1000);
|
||||
return this.fmtTimeAgo(new Date(this.expiresAt) - new Date());
|
||||
},
|
||||
isTrial() {
|
||||
return this.name === TRIAL;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
:disabled="!sponsor"
|
||||
:disabled="!sponsorActive"
|
||||
@change="change"
|
||||
/>
|
||||
<div class="form-check-label">
|
||||
<label for="telemetryEnabled">
|
||||
{{ $t("footer.telemetry.optIn") }}
|
||||
<i18n-t v-if="sponsor" tag="span" keypath="footer.telemetry.optInMoreDetails">
|
||||
<i18n-t v-if="sponsorActive" tag="span" keypath="footer.telemetry.optInMoreDetails">
|
||||
<a :href="docsLink" target="_blank">
|
||||
{{ $t("footer.telemetry.optInMoreDetailsLink") }}
|
||||
</a>
|
||||
|
|
@ -40,7 +40,7 @@ function parseMarkdown(markdownText) {
|
|||
|
||||
export default {
|
||||
name: "TelemetrySettings",
|
||||
props: { sponsor: String },
|
||||
props: { sponsorActive: Boolean },
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import TopNavigation from "./TopNavigation.vue";
|
|||
/>
|
||||
</Variant>
|
||||
<Variant title="token expires">
|
||||
<TopNavigation :sponsorTokenExpires="768423" />
|
||||
<TopNavigation :sponsor="{ expiresSoon: true }" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
>
|
||||
<span
|
||||
v-if="showBadge"
|
||||
class="position-absolute top-0 start-100 translate-middle p-2 bg-danger border border-light rounded-circle"
|
||||
class="position-absolute top-0 start-100 translate-middle p-2 rounded-circle"
|
||||
:class="badgeClass"
|
||||
>
|
||||
<span class="visually-hidden">action required</span>
|
||||
</span>
|
||||
|
|
@ -34,10 +35,6 @@
|
|||
data-testid="topnavigation-settings"
|
||||
@click="openSettingsModal"
|
||||
>
|
||||
<span
|
||||
v-if="sponsorTokenExpires"
|
||||
class="d-inline-block p-1 rounded-circle bg-danger border border-light rounded-circle"
|
||||
></span>
|
||||
{{ $t("settings.title") }}
|
||||
</button>
|
||||
</li>
|
||||
|
|
@ -53,6 +50,11 @@
|
|||
</li>
|
||||
<li>
|
||||
<router-link class="dropdown-item" to="/config" active-class="active">
|
||||
<span
|
||||
v-if="showBadge"
|
||||
class="d-inline-block p-1 rounded-circle bg-warning rounded-circle"
|
||||
:class="badgeClass"
|
||||
></span>
|
||||
{{ $t("config.main.title") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
|
@ -75,7 +77,8 @@
|
|||
>
|
||||
<span
|
||||
v-if="!login.loggedIn"
|
||||
class="d-inline-block p-1 rounded-circle bg-danger border border-light rounded-circle"
|
||||
class="d-inline-block p-1 rounded-circle border border-light rounded-circle"
|
||||
:class="badgeClass"
|
||||
></span>
|
||||
{{ login.title }}
|
||||
{{ $t(login.loggedIn ? "main.provider.logout" : "main.provider.login") }}
|
||||
|
|
@ -119,7 +122,6 @@ import "@h2d2/shopicons/es/regular/menu";
|
|||
import "@h2d2/shopicons/es/regular/newtab";
|
||||
import collector from "../mixins/collector";
|
||||
import { logout, isLoggedIn, openLoginModal } from "../auth";
|
||||
|
||||
import baseAPI from "../baseapi";
|
||||
import { isApp, sendToApp } from "../utils/native";
|
||||
|
||||
|
|
@ -133,9 +135,14 @@ export default {
|
|||
return {};
|
||||
},
|
||||
},
|
||||
sponsor: String,
|
||||
sponsorTokenExpires: Number,
|
||||
sponsor: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
battery: Array,
|
||||
fatal: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -161,7 +168,13 @@ export default {
|
|||
return this.logoutCount > 0;
|
||||
},
|
||||
showBadge() {
|
||||
return this.loginRequired || this.sponsorTokenExpires;
|
||||
return this.loginRequired || this.sponsor.expiresSoon || this.fatal?.error;
|
||||
},
|
||||
badgeClass() {
|
||||
if (this.fatal?.error) {
|
||||
return "bg-danger";
|
||||
}
|
||||
return "bg-warning";
|
||||
},
|
||||
batteryModalAvailable() {
|
||||
return this.batteryConfigured;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ export default {
|
|||
size: {
|
||||
type: String,
|
||||
validator: function (value) {
|
||||
return ["s", "m", "l", "xl"].includes(value);
|
||||
return ["xs", "s", "m", "l", "xl"].includes(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
svgStyle() {
|
||||
const sizes = {
|
||||
xs: "16px",
|
||||
s: "24px",
|
||||
m: "32px",
|
||||
l: "48px",
|
||||
|
|
|
|||
23
assets/js/restart.js
Normal file
23
assets/js/restart.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { reactive } from "vue";
|
||||
import api from "./api";
|
||||
|
||||
const restart = reactive({
|
||||
restartNeeded: false,
|
||||
restarting: false,
|
||||
});
|
||||
|
||||
export async function performRestart() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
restart.restarting = true;
|
||||
} catch (e) {
|
||||
alert("Unabled to restart server.");
|
||||
}
|
||||
}
|
||||
|
||||
export function restartComplete() {
|
||||
restart.restarting = false;
|
||||
restart.restartNeeded = false;
|
||||
}
|
||||
|
||||
export default restart;
|
||||
|
|
@ -54,10 +54,6 @@ export default function setupRouter(i18n) {
|
|||
component: () => import("./views/Log.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
},
|
||||
{
|
||||
path: "/error",
|
||||
component: () => import("./views/StartupError.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<HelpModal />
|
||||
<PasswordModal />
|
||||
<LoginModal />
|
||||
<OfflineIndicator v-if="offline" />
|
||||
<OfflineIndicator v-bind="offlineIndicatorProps" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -64,15 +64,6 @@ export default {
|
|||
offline: function () {
|
||||
updateAuthStatus();
|
||||
},
|
||||
startupErrors: function (now) {
|
||||
if (now) {
|
||||
console.log("startup errors detected. redirecting to error page");
|
||||
this.$router.push("/error");
|
||||
} else {
|
||||
console.log("startup errors resolved. redirecting to home page");
|
||||
this.$router.push("/");
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
version: function () {
|
||||
|
|
@ -87,8 +78,8 @@ export default {
|
|||
batterySettingsProps() {
|
||||
return this.collectProps(BatterySettingsModal, store.state);
|
||||
},
|
||||
startupErrors: function () {
|
||||
return store.state.fatal?.length > 0;
|
||||
offlineIndicatorProps() {
|
||||
return this.collectProps(OfflineIndicator, store.state);
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
|
|
|
|||
|
|
@ -2,76 +2,48 @@
|
|||
<div class="root safe-area-inset">
|
||||
<div class="container px-4">
|
||||
<TopHeader :title="$t('config.main.title')" />
|
||||
<div class="wrapper">
|
||||
<div
|
||||
v-if="dirty || restarting"
|
||||
class="alert alert-secondary d-flex justify-content-between align-items-center my-4"
|
||||
role="alert"
|
||||
>
|
||||
<div v-if="restarting">
|
||||
<strong>{{ $t("config.system.restartingMessage") }}</strong>
|
||||
{{ $t("config.system.restartingDescription") }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<strong>{{ $t("config.system.restartRequiredMessage") }}</strong>
|
||||
{{ $t("config.system.restartRequiredDescription") }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-dark btn-sm"
|
||||
:disabled="restarting || offline"
|
||||
@click="restart"
|
||||
>
|
||||
<span
|
||||
v-if="restarting || offline"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span v-else>{{ $t("config.system.restart") }}</span>
|
||||
</button>
|
||||
<div class="wrapper pb-5">
|
||||
<div class="alert alert-danger my-4 pb-0" role="alert" v-if="$hiddenFeatures()">
|
||||
<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>grid meter</li>
|
||||
<li>aux meters</li>
|
||||
<li>loadpoints and chargers</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" />
|
||||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
|
||||
<div class="round-box p-4 d-flex gap-4">
|
||||
<router-link to="/log" class="btn btn-outline-secondary">
|
||||
{{ $t("config.system.logs") }}
|
||||
</router-link>
|
||||
<button
|
||||
class="btn btn-outline-danger"
|
||||
:disabled="restarting || offline"
|
||||
@click="restart"
|
||||
>
|
||||
<span
|
||||
v-if="restarting || offline"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ $t("config.system.restart") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="$hiddenFeatures()">
|
||||
<hr class="my-5" />
|
||||
|
||||
<div class="alert alert-danger my-4" role="alert">
|
||||
<strong>Highly experimental!</strong> Only play around with these settings
|
||||
if you know what you're doing. Otherwise you might have to reset or manually
|
||||
repair your database.
|
||||
</div>
|
||||
|
||||
<h2 class="my-4 mt-5">Grid, PV & Battery Systems</h2>
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.grid") }} 🧪</h2>
|
||||
<ul class="p-0 config-list">
|
||||
<DeviceCard
|
||||
:name="gridMeter?.config?.template || 'Grid meter'"
|
||||
:unconfigured="!gridMeter"
|
||||
:name="$t('config.grid.title')"
|
||||
:editable="!!gridMeter?.id"
|
||||
:error="deviceError('meter', gridMeter?.name)"
|
||||
data-testid="grid"
|
||||
@configure="addMeter('grid')"
|
||||
@edit="editMeter(gridMeter.id, 'grid')"
|
||||
>
|
||||
<template #icon>
|
||||
|
|
@ -81,11 +53,29 @@
|
|||
<DeviceTags :tags="deviceTags('meter', gridMeter?.name)" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
: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>
|
||||
</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"
|
||||
: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')"
|
||||
>
|
||||
|
|
@ -101,6 +91,7 @@
|
|||
: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')"
|
||||
>
|
||||
|
|
@ -114,47 +105,13 @@
|
|||
<AddDeviceButton :title="$t('config.main.addPvBattery')" @add="addMeter" />
|
||||
</ul>
|
||||
|
||||
<h2 class="my-4 wip">Tariffs</h2>
|
||||
|
||||
<ul class="p-0 config-list wip">
|
||||
<DeviceCard
|
||||
name="Grid"
|
||||
unconfigured
|
||||
data-testid="tariff-grid"
|
||||
@configure="todo"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-money></shopicon-regular-money>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
name="Feed-in"
|
||||
unconfigured
|
||||
data-testid="tariff-feedin"
|
||||
@configure="todo"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-receivepayment></shopicon-regular-receivepayment>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
name="CO₂ estimate"
|
||||
unconfigured
|
||||
data-testid="tariff-co2"
|
||||
@configure="todo"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-eco1></shopicon-regular-eco1>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
</ul>
|
||||
|
||||
<h2 class="my-4 wip">Charge Points</h2>
|
||||
<h2 class="my-4 wip">{{ $t("config.section.loadpoints") }} 🧪</h2>
|
||||
|
||||
<ul class="p-0 config-list wip">
|
||||
<DeviceCard
|
||||
name="Fake Carport"
|
||||
editable
|
||||
:error="deviceError('charger', 'fake-charger')"
|
||||
data-testid="chargepoint-1"
|
||||
@edit="todo"
|
||||
>
|
||||
|
|
@ -172,14 +129,15 @@
|
|||
/>
|
||||
</ul>
|
||||
|
||||
<h2 class="my-4">Vehicles</h2>
|
||||
<h2 class="my-4">{{ $t("config.section.vehicles") }} 🧪</h2>
|
||||
<div>
|
||||
<ul class="p-0 config-list">
|
||||
<DeviceCard
|
||||
v-for="vehicle in vehicles"
|
||||
:key="vehicle.id"
|
||||
: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)"
|
||||
>
|
||||
|
|
@ -196,41 +154,108 @@
|
|||
@click="addVehicle"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<h2 class="my-4 wip">Integrations</h2>
|
||||
|
||||
<ul class="p-0 config-list wip">
|
||||
<DeviceCard name="MQTT" unconfigured data-testid="mqtt" @configure="todo">
|
||||
<template #icon>
|
||||
<shopicon-regular-fastdelivery1></shopicon-regular-fastdelivery1>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
name="Notifications"
|
||||
unconfigured
|
||||
data-testid="eebus"
|
||||
@configure="todo"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-sendit></shopicon-regular-sendit>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
name="InfluxDB"
|
||||
unconfigured
|
||||
data-testid="influx"
|
||||
@configure="todo"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-diagram></shopicon-regular-diagram>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard name="EEBus" unconfigured data-testid="eebus" @configure="todo">
|
||||
<template #icon>
|
||||
<shopicon-regular-polygon></shopicon-regular-polygon>
|
||||
</template>
|
||||
</DeviceCard>
|
||||
</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="yamlTags('messaging')" />
|
||||
</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="yamlTags('eebus')" />
|
||||
</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 :tags="yamlTags('circuits')" />
|
||||
</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="yamlTags('hems')" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<VehicleModal :id="selectedVehicleId" @vehicle-changed="vehicleChanged" />
|
||||
|
|
@ -242,6 +267,17 @@
|
|||
@updated="meterChanged"
|
||||
@removed="removeMeterFromSite"
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -252,36 +288,69 @@ import TopHeader from "../components/TopHeader.vue";
|
|||
import "@h2d2/shopicons/es/regular/sun";
|
||||
import "@h2d2/shopicons/es/regular/batterythreequarters";
|
||||
import "@h2d2/shopicons/es/regular/powersupply";
|
||||
import "@h2d2/shopicons/es/regular/money";
|
||||
import "@h2d2/shopicons/es/regular/receivepayment";
|
||||
import "@h2d2/shopicons/es/regular/eco1";
|
||||
import "@h2d2/shopicons/es/regular/fastdelivery1";
|
||||
import "@h2d2/shopicons/es/regular/sendit";
|
||||
import "@h2d2/shopicons/es/regular/diagram";
|
||||
import "@h2d2/shopicons/es/regular/polygon";
|
||||
import "@h2d2/shopicons/es/regular/cablecharge";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import AddDeviceButton from "../components/Config/AddDeviceButton.vue";
|
||||
import api from "../api";
|
||||
import VehicleIcon from "../components/VehicleIcon";
|
||||
import VehicleModal from "../components/Config/VehicleModal.vue";
|
||||
import CircuitsIcon from "../components/MaterialIcon/Circuits.vue";
|
||||
import CircuitsModal from "../components/Config/CircuitsModal.vue";
|
||||
import ControlModal from "../components/Config/ControlModal.vue";
|
||||
import collector from "../mixins/collector";
|
||||
import DeviceCard from "../components/Config/DeviceCard.vue";
|
||||
import DeviceTags from "../components/Config/DeviceTags.vue";
|
||||
import AddDeviceButton from "../components/Config/AddDeviceButton.vue";
|
||||
import MeterModal from "../components/Config/MeterModal.vue";
|
||||
import GeneralConfig from "../components/Config/GeneralConfig.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 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 store from "../store";
|
||||
import SponsorModal from "../components/Config/SponsorModal.vue";
|
||||
import TariffsModal from "../components/Config/TariffsModal.vue";
|
||||
import VehicleIcon from "../components/VehicleIcon";
|
||||
import VehicleModal from "../components/Config/VehicleModal.vue";
|
||||
|
||||
export default {
|
||||
name: "Config",
|
||||
components: {
|
||||
TopHeader,
|
||||
GeneralConfig,
|
||||
VehicleIcon,
|
||||
VehicleModal,
|
||||
AddDeviceButton,
|
||||
CircuitsIcon,
|
||||
CircuitsModal,
|
||||
ControlModal,
|
||||
DeviceCard,
|
||||
DeviceTags,
|
||||
AddDeviceButton,
|
||||
EebusIcon,
|
||||
EebusModal,
|
||||
GeneralConfig,
|
||||
HemsIcon,
|
||||
HemsModal,
|
||||
InfluxIcon,
|
||||
InfluxModal,
|
||||
MessagingModal,
|
||||
MeterModal,
|
||||
ModbusProxyIcon,
|
||||
ModbusProxyModal,
|
||||
MqttIcon,
|
||||
MqttModal,
|
||||
NetworkModal,
|
||||
NotificationIcon,
|
||||
SponsorModal,
|
||||
TariffsModal,
|
||||
TopHeader,
|
||||
VehicleIcon,
|
||||
VehicleModal,
|
||||
},
|
||||
props: {
|
||||
offline: Boolean,
|
||||
|
|
@ -289,8 +358,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
dirty: false,
|
||||
restarting: false,
|
||||
vehicles: [],
|
||||
meters: [],
|
||||
selectedVehicleId: undefined,
|
||||
|
|
@ -299,10 +366,20 @@ export default {
|
|||
site: { grid: "", pv: [], battery: [] },
|
||||
deviceValueTimeout: undefined,
|
||||
deviceValues: {},
|
||||
yamlConfigState: {
|
||||
messaging: false,
|
||||
eebus: false,
|
||||
circuits: false,
|
||||
modbusproxy: false,
|
||||
hems: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
mixins: [formatter],
|
||||
mixins: [formatter, collector],
|
||||
computed: {
|
||||
fatalClass() {
|
||||
return store.state?.fatal?.class;
|
||||
},
|
||||
siteTitle() {
|
||||
return this.site?.title;
|
||||
},
|
||||
|
|
@ -321,11 +398,43 @@ export default {
|
|||
selectedMeterName() {
|
||||
return this.getMeterById(this.selectedMeterId)?.name;
|
||||
},
|
||||
tariffTags() {
|
||||
const { currency, tariffGrid, tariffFeedIn, tariffCo2 } = store.state;
|
||||
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;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
offline() {
|
||||
if (!this.offline) {
|
||||
this.restarting = false;
|
||||
this.loadAll();
|
||||
}
|
||||
},
|
||||
|
|
@ -343,13 +452,12 @@ export default {
|
|||
await this.loadSite();
|
||||
await this.loadDirty();
|
||||
await this.updateValues();
|
||||
await this.updateYamlConfigState();
|
||||
},
|
||||
async loadDirty() {
|
||||
try {
|
||||
const response = await api.get("/config/dirty");
|
||||
this.dirty = response.data?.result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const response = await api.get("/config/dirty");
|
||||
if (response.data?.result) {
|
||||
restart.restartNeeded = true;
|
||||
}
|
||||
},
|
||||
async loadVehicles() {
|
||||
|
|
@ -361,8 +469,12 @@ export default {
|
|||
this.meters = response.data?.result || [];
|
||||
},
|
||||
async loadSite() {
|
||||
const response = await api.get("/config/site");
|
||||
this.site = response.data?.result;
|
||||
const response = await api.get("/config/site", {
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
if (response.status === 200) {
|
||||
this.site = response.data?.result;
|
||||
}
|
||||
},
|
||||
getMetersByNames(names) {
|
||||
if (!names || !this.meters) {
|
||||
|
|
@ -417,6 +529,10 @@ export default {
|
|||
siteChanged() {
|
||||
this.loadDirty();
|
||||
},
|
||||
yamlChanged() {
|
||||
this.loadDirty();
|
||||
this.updateYamlConfigState();
|
||||
},
|
||||
addMeterToSite(type, name) {
|
||||
if (type === "grid") {
|
||||
this.site.grid = name;
|
||||
|
|
@ -447,12 +563,7 @@ export default {
|
|||
alert("not implemented yet");
|
||||
},
|
||||
async restart() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
this.restarting = true;
|
||||
} catch (e) {
|
||||
alert("Unabled to restart server.");
|
||||
}
|
||||
await performRestart();
|
||||
},
|
||||
async updateDeviceValue(type, name) {
|
||||
try {
|
||||
|
|
@ -489,6 +600,20 @@ export default {
|
|||
console.error(`modal ${id} not found`);
|
||||
}
|
||||
},
|
||||
updateYamlConfigState() {
|
||||
const keys = Object.keys(this.yamlConfigState);
|
||||
keys.forEach(async (key) => {
|
||||
const res = await api.get(`/config/${key}`);
|
||||
this.yamlConfigState[key] = !!res.data.result;
|
||||
});
|
||||
},
|
||||
yamlTags(key) {
|
||||
return { configured: { value: this.yamlConfigState[key] } };
|
||||
},
|
||||
deviceError(type, name) {
|
||||
const fatal = store.state?.fatal || {};
|
||||
return fatal.class === type && fatal.device === name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -496,7 +621,7 @@ export default {
|
|||
.config-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-gap: 1rem;
|
||||
grid-gap: 2rem;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
.wip {
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
<template>
|
||||
<div class="container px-4 safe-area-inset">
|
||||
<div class="d-flex justify-content-between align-items-center my-3 my-md-4">
|
||||
<h1 class="d-block mt-0 d-flex">
|
||||
{{ $t("startupError.title") }}
|
||||
<shopicon-regular-car1 size="m" class="ms-2 icon"></shopicon-regular-car1>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<code class="fs-6 mb-3">
|
||||
<div v-for="(error, index) in errors" :key="index">{{ error }}</div>
|
||||
</code>
|
||||
<i18n-t tag="p" keypath="startupError.description">
|
||||
<a href="https://github.com/evcc-io/evcc/discussions">
|
||||
{{ $t("startupError.discussions") }}
|
||||
</a>
|
||||
</i18n-t>
|
||||
<p>
|
||||
<em>{{ $t("startupError.hint") }}</em>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<h5 class="mb-3">{{ $t("startupError.configuration") }}</h5>
|
||||
<div class="d-md-flex justify-content-between">
|
||||
<p class="me-md-4">
|
||||
<span class="d-block">
|
||||
{{ $t("startupError.configFile") }}
|
||||
<code>{{ file }}</code>
|
||||
</span>
|
||||
<i18n-t v-if="line" tag="span" keypath="startupError.lineError">
|
||||
<a :href="`#line${line}`" @click.prevent="scrollTo">{{
|
||||
$t("startupError.lineErrorLink", [line])
|
||||
}}</a>
|
||||
</i18n-t>
|
||||
{{ $t("startupError.fixAndRestart") }}
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
class="btn btn-primary text-nowrap"
|
||||
type="button"
|
||||
:disabled="offline"
|
||||
@click="shutdown"
|
||||
>
|
||||
<span
|
||||
v-if="offline"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
{{ $t("startupError.restartButton") }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<code v-if="config">
|
||||
<div class="my-2"></div>
|
||||
<div class="py-2 text-muted config">
|
||||
<div
|
||||
v-for="(configLine, lineNumber) in config.split('\n')"
|
||||
:id="`line${lineNumber + 1}`"
|
||||
:key="lineNumber"
|
||||
class="m-0 px-2"
|
||||
:class="{
|
||||
highlighted: line === lineNumber + 1,
|
||||
}"
|
||||
>
|
||||
{{ configLine }}
|
||||
</div>
|
||||
</div>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/regular/car1";
|
||||
import api from "../api";
|
||||
import collector from "../mixins/collector";
|
||||
import store from "../store";
|
||||
|
||||
export default {
|
||||
name: "StartupError",
|
||||
mixins: [collector],
|
||||
props: {
|
||||
offline: Boolean,
|
||||
},
|
||||
computed: {
|
||||
errors() {
|
||||
return store.state.fatal || [];
|
||||
},
|
||||
config() {
|
||||
return store.state.config;
|
||||
},
|
||||
file() {
|
||||
return store.state.file;
|
||||
},
|
||||
line() {
|
||||
return store.state.line;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
shutdown() {
|
||||
api.post("/system/shutdown");
|
||||
},
|
||||
scrollTo(e) {
|
||||
const id = e.currentTarget.getAttribute("href").substring(1);
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.highlighted {
|
||||
position: relative;
|
||||
color: var(--bs-code-color) !important;
|
||||
}
|
||||
.highlighted:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.2em;
|
||||
border-top: 0.5em solid transparent;
|
||||
border-bottom: 0.5em solid transparent;
|
||||
border-left: 0.5em solid var(--bs-code-color);
|
||||
}
|
||||
.config {
|
||||
max-width: 100%;
|
||||
overflow-x: scroll;
|
||||
white-space: pre;
|
||||
}
|
||||
.config {
|
||||
border: 1px solid var(--bs-gray-400);
|
||||
}
|
||||
.icon {
|
||||
transform-origin: 60% 40%;
|
||||
animation: swinging 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes swinging {
|
||||
0% {
|
||||
transform: translateY(8%) rotate(170deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(8%) rotate(185deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(8%) rotate(170deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/enbility/cemd/cem"
|
||||
"github.com/enbility/cemd/emobility"
|
||||
"github.com/enbility/eebus-go/service"
|
||||
|
|
@ -28,6 +29,15 @@ const (
|
|||
EEBUSDeviceCode string = "EVCC_HEMS_01" // used as common name in cert generation
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
URI string
|
||||
ShipID string
|
||||
Interfaces []string
|
||||
Certificate struct {
|
||||
Public, Private []byte
|
||||
}
|
||||
}
|
||||
|
||||
type EEBusClientCBs struct {
|
||||
onConnect func(string) // , ship.Conn) error
|
||||
onDisconnect func(string)
|
||||
|
|
@ -46,25 +56,17 @@ type EEBus struct {
|
|||
|
||||
var Instance *EEBus
|
||||
|
||||
func NewServer(other map[string]interface{}) (*EEBus, error) {
|
||||
cc := struct {
|
||||
Uri string
|
||||
ShipID string
|
||||
Interfaces []string
|
||||
Certificate struct {
|
||||
Public, Private []byte
|
||||
}
|
||||
}{
|
||||
Uri: ":4712",
|
||||
func NewServer(other Config) (*EEBus, error) {
|
||||
cc := Config{
|
||||
URI: ":4712",
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := util.NewLogger("eebus")
|
||||
|
||||
var err error
|
||||
protectedID, err := machine.ProtectedID("evcc-eebus")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -80,7 +82,7 @@ func NewServer(other map[string]interface{}) (*EEBus, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
_, portValue, err := net.SplitHostPort(cc.Uri)
|
||||
_, portValue, err := net.SplitHostPort(cc.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func runCharger(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func runChargerRamp(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
147
cmd/class_enumer.go
Normal file
147
cmd/class_enumer.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Code generated by "enumer -type Class -trimprefix Class -transform=lower -text"; DO NOT EDIT.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _ClassName = "meterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
|
||||
var _ClassIndex = [...]uint8{0, 5, 12, 19, 25, 32, 36, 40, 48, 59, 64, 74, 76, 80, 86, 95, 106}
|
||||
|
||||
const _ClassLowerName = "meterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
|
||||
func (i Class) String() string {
|
||||
i -= 1
|
||||
if i < 0 || i >= Class(len(_ClassIndex)-1) {
|
||||
return fmt.Sprintf("Class(%d)", i+1)
|
||||
}
|
||||
return _ClassName[_ClassIndex[i]:_ClassIndex[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 _ClassNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[ClassMeter-(1)]
|
||||
_ = x[ClassCharger-(2)]
|
||||
_ = x[ClassVehicle-(3)]
|
||||
_ = x[ClassTariff-(4)]
|
||||
_ = x[ClassCircuit-(5)]
|
||||
_ = x[ClassSite-(6)]
|
||||
_ = x[ClassMqtt-(7)]
|
||||
_ = x[ClassDatabase-(8)]
|
||||
_ = x[ClassModbusProxy-(9)]
|
||||
_ = x[ClassEEBus-(10)]
|
||||
_ = x[ClassJavascript-(11)]
|
||||
_ = x[ClassGo-(12)]
|
||||
_ = x[ClassHEMS-(13)]
|
||||
_ = x[ClassInflux-(14)]
|
||||
_ = x[ClassMessenger-(15)]
|
||||
_ = x[ClassSponsorship-(16)]
|
||||
}
|
||||
|
||||
var _ClassValues = []Class{ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassInflux, ClassMessenger, ClassSponsorship}
|
||||
|
||||
var _ClassNameToValueMap = map[string]Class{
|
||||
_ClassName[0:5]: ClassMeter,
|
||||
_ClassLowerName[0:5]: ClassMeter,
|
||||
_ClassName[5:12]: ClassCharger,
|
||||
_ClassLowerName[5:12]: ClassCharger,
|
||||
_ClassName[12:19]: ClassVehicle,
|
||||
_ClassLowerName[12:19]: ClassVehicle,
|
||||
_ClassName[19:25]: ClassTariff,
|
||||
_ClassLowerName[19:25]: ClassTariff,
|
||||
_ClassName[25:32]: ClassCircuit,
|
||||
_ClassLowerName[25:32]: ClassCircuit,
|
||||
_ClassName[32:36]: ClassSite,
|
||||
_ClassLowerName[32:36]: ClassSite,
|
||||
_ClassName[36:40]: ClassMqtt,
|
||||
_ClassLowerName[36:40]: ClassMqtt,
|
||||
_ClassName[40:48]: ClassDatabase,
|
||||
_ClassLowerName[40:48]: ClassDatabase,
|
||||
_ClassName[48:59]: ClassModbusProxy,
|
||||
_ClassLowerName[48:59]: ClassModbusProxy,
|
||||
_ClassName[59:64]: ClassEEBus,
|
||||
_ClassLowerName[59:64]: ClassEEBus,
|
||||
_ClassName[64:74]: ClassJavascript,
|
||||
_ClassLowerName[64:74]: ClassJavascript,
|
||||
_ClassName[74:76]: ClassGo,
|
||||
_ClassLowerName[74:76]: ClassGo,
|
||||
_ClassName[76:80]: ClassHEMS,
|
||||
_ClassLowerName[76:80]: ClassHEMS,
|
||||
_ClassName[80:86]: ClassInflux,
|
||||
_ClassLowerName[80:86]: ClassInflux,
|
||||
_ClassName[86:95]: ClassMessenger,
|
||||
_ClassLowerName[86:95]: ClassMessenger,
|
||||
_ClassName[95:106]: ClassSponsorship,
|
||||
_ClassLowerName[95:106]: ClassSponsorship,
|
||||
}
|
||||
|
||||
var _ClassNames = []string{
|
||||
_ClassName[0:5],
|
||||
_ClassName[5:12],
|
||||
_ClassName[12:19],
|
||||
_ClassName[19:25],
|
||||
_ClassName[25:32],
|
||||
_ClassName[32:36],
|
||||
_ClassName[36:40],
|
||||
_ClassName[40:48],
|
||||
_ClassName[48:59],
|
||||
_ClassName[59:64],
|
||||
_ClassName[64:74],
|
||||
_ClassName[74:76],
|
||||
_ClassName[76:80],
|
||||
_ClassName[80:86],
|
||||
_ClassName[86:95],
|
||||
_ClassName[95:106],
|
||||
}
|
||||
|
||||
// ClassString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func ClassString(s string) (Class, error) {
|
||||
if val, ok := _ClassNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _ClassNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Class values", s)
|
||||
}
|
||||
|
||||
// ClassValues returns all values of the enum
|
||||
func ClassValues() []Class {
|
||||
return _ClassValues
|
||||
}
|
||||
|
||||
// ClassStrings returns a slice of all String values of the enum
|
||||
func ClassStrings() []string {
|
||||
strs := make([]string, len(_ClassNames))
|
||||
copy(strs, _ClassNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAClass returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Class) IsAClass() bool {
|
||||
for _, v := range _ClassValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for Class
|
||||
func (i Class) MarshalText() ([]byte, error) {
|
||||
return []byte(i.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for Class
|
||||
func (i *Class) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
*i, err = ClassString(string(text))
|
||||
return err
|
||||
}
|
||||
|
|
@ -5,12 +5,22 @@ import (
|
|||
|
||||
"github.com/evcc-io/evcc/charger/eebus"
|
||||
"github.com/evcc-io/evcc/cmd/shutdown"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// configureEEBus setup EEBus
|
||||
func (c *CmdConfigure) configureEEBus(conf map[string]interface{}) error {
|
||||
var err error
|
||||
if eebus.Instance, err = eebus.NewServer(conf); err == nil {
|
||||
func (c *CmdConfigure) configureEEBus(other map[string]interface{}) error {
|
||||
conf := eebus.Config{
|
||||
URI: ":4712",
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv, err := eebus.NewServer(conf)
|
||||
if err == nil {
|
||||
eebus.Instance = srv
|
||||
go eebus.Instance.Run()
|
||||
shutdown.Register(eebus.Instance.Shutdown)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import (
|
|||
_ "embed" // for yaml
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
)
|
||||
|
||||
//go:embed demo.yaml
|
||||
var demoYaml string
|
||||
|
||||
func demoConfig(conf *globalConfig) error {
|
||||
func demoConfig(conf *globalconfig.All) error {
|
||||
viper.SetConfigType("yaml")
|
||||
if err := viper.ReadConfig(strings.NewReader(demoYaml)); err != nil {
|
||||
return fmt.Errorf("failed decoding demo config: %w", err)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func runDevice(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ func runDump(cmd *cobra.Command, args []string) {
|
|||
|
||||
// setup environment
|
||||
if err == nil {
|
||||
err = configureEnvironment(cmd, conf)
|
||||
err = configureEnvironment(cmd, &conf)
|
||||
}
|
||||
|
||||
var site *core.Site
|
||||
if err == nil {
|
||||
site, err = configureSiteAndLoadpoints(conf)
|
||||
site, err = configureSiteAndLoadpoints(&conf)
|
||||
}
|
||||
|
||||
if *dumpConfig {
|
||||
|
|
|
|||
78
cmd/error.go
Normal file
78
cmd/error.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Class int
|
||||
|
||||
//go:generate enumer -type Class -trimprefix Class -transform=lower -text
|
||||
const (
|
||||
_ Class = iota
|
||||
ClassMeter
|
||||
ClassCharger
|
||||
ClassVehicle
|
||||
ClassTariff
|
||||
ClassCircuit
|
||||
ClassSite
|
||||
ClassMqtt
|
||||
ClassDatabase
|
||||
ClassModbusProxy
|
||||
ClassEEBus
|
||||
ClassJavascript
|
||||
ClassGo
|
||||
ClassHEMS
|
||||
ClassInflux
|
||||
ClassMessenger
|
||||
ClassSponsorship
|
||||
)
|
||||
|
||||
// DeviceError indicates the specific device that failed
|
||||
type DeviceError struct {
|
||||
Name string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *DeviceError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
// ClassError indicates the class of devices that failed
|
||||
type ClassError struct {
|
||||
Class Class
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *ClassError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e ClassError) MarshalJSON() ([]byte, error) {
|
||||
res := struct {
|
||||
Class string `json:"class"`
|
||||
Device string `json:"device,omitempty"`
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Class: e.Class.String(),
|
||||
Error: e.err.Error(),
|
||||
}
|
||||
|
||||
var de *DeviceError
|
||||
if errors.As(e.err, &de) {
|
||||
res.Device = de.Name
|
||||
}
|
||||
|
||||
return json.Marshal(res)
|
||||
}
|
||||
|
||||
func wrapErrorWithClass(class Class, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ClassError{
|
||||
Class: class,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
22
cmd/error_test.go
Normal file
22
cmd/error_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
res := &ClassError{
|
||||
ClassMeter,
|
||||
&DeviceError{
|
||||
"0815",
|
||||
errors.New("foo"),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := res.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"class":"meter","device":"0815","error":"foo"}`, string(b))
|
||||
}
|
||||
|
|
@ -25,6 +25,9 @@ const (
|
|||
flagCloud = "cloud"
|
||||
flagCloudDescription = "Use cloud service (requires sponsor token)"
|
||||
|
||||
flagReset = "reset"
|
||||
flagResetDescription = "Reset migrated settings"
|
||||
|
||||
flagEnable = "enable"
|
||||
flagDisable = "disable"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/cmd/shutdown"
|
||||
|
|
@ -63,31 +61,6 @@ func redact(src string) string {
|
|||
ReplaceAllString(src, "$1: *****")
|
||||
}
|
||||
|
||||
func publishErrorInfo(valueChan chan<- util.Param, cfgFile string, err error) {
|
||||
if cfgFile != "" {
|
||||
file, pathErr := filepath.Abs(cfgFile)
|
||||
if pathErr != nil {
|
||||
file = cfgFile
|
||||
}
|
||||
valueChan <- util.Param{Key: "file", Val: file}
|
||||
|
||||
if src, fileErr := os.ReadFile(cfgFile); fileErr != nil {
|
||||
log.ERROR.Println("could not open config file:", fileErr)
|
||||
} else {
|
||||
valueChan <- util.Param{Key: "config", Val: redact(string(src))}
|
||||
|
||||
// find line number
|
||||
if match := regexp.MustCompile(`yaml: line (\d+):`).FindStringSubmatch(err.Error()); len(match) == 2 {
|
||||
if line, err := strconv.Atoi(match[1]); err == nil {
|
||||
valueChan <- util.Param{Key: "line", Val: line}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valueChan <- util.Param{Key: "fatal", Val: unwrap(err)}
|
||||
}
|
||||
|
||||
// fatal logs a fatal error and runs shutdown functions before terminating
|
||||
func fatal(err error) {
|
||||
log.FATAL.Println(err)
|
||||
|
|
@ -102,7 +75,7 @@ func shutdownDoneC() <-chan struct{} {
|
|||
return doneC
|
||||
}
|
||||
|
||||
func wrapErrors(err error) error {
|
||||
func wrapError(err error) error {
|
||||
if err != nil {
|
||||
var opErr *net.OpError
|
||||
var pathErr *os.PathError
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func runMeter(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
116
cmd/migrate.go
Normal file
116
cmd/migrate.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// migrateCmd represents the migrate command
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Migrate yaml to database (overwrites db settings)",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: runMigrate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(migrateCmd)
|
||||
migrateCmd.Flags().BoolP(flagReset, "r", false, flagResetDescription)
|
||||
}
|
||||
|
||||
func runMigrate(cmd *cobra.Command, args []string) {
|
||||
// load config
|
||||
if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup persistence
|
||||
if err := configureDatabase(conf.Database); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
reset := cmd.Flags().Lookup(flagReset).Changed
|
||||
|
||||
// TODO remove yaml file
|
||||
if reset {
|
||||
log.WARN.Println("resetting:")
|
||||
} else {
|
||||
log.WARN.Println("migrating:")
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- global settings")
|
||||
if reset {
|
||||
settings.Delete(keys.Interval)
|
||||
settings.Delete(keys.SponsorToken)
|
||||
} else {
|
||||
settings.SetInt(keys.Interval, int64(conf.Interval))
|
||||
settings.SetString(keys.SponsorToken, conf.SponsorToken)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- network")
|
||||
if reset {
|
||||
settings.Delete(keys.Network)
|
||||
} else {
|
||||
_ = settings.SetJson(keys.Network, conf.Network)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- mqtt")
|
||||
if reset {
|
||||
settings.Delete(keys.Mqtt)
|
||||
} else {
|
||||
_ = settings.SetJson(keys.Mqtt, conf.Mqtt)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- influx")
|
||||
if reset {
|
||||
settings.Delete(keys.Influx)
|
||||
} else {
|
||||
_ = settings.SetJson(keys.Influx, conf.Influx)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- hems")
|
||||
if reset {
|
||||
settings.Delete(keys.Hems)
|
||||
} else if conf.HEMS.Type != "" {
|
||||
_ = settings.SetYaml(keys.Hems, conf.HEMS)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- eebus")
|
||||
if reset {
|
||||
settings.Delete(keys.EEBus)
|
||||
} else if conf.EEBus.URI != "" {
|
||||
_ = settings.SetYaml(keys.EEBus, conf.EEBus)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- modbusproxy")
|
||||
if reset {
|
||||
settings.Delete(keys.ModbusProxy)
|
||||
} else if len(conf.ModbusProxy) > 0 {
|
||||
_ = settings.SetYaml(keys.ModbusProxy, conf.ModbusProxy)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- messaging")
|
||||
if reset {
|
||||
settings.Delete(keys.Messaging)
|
||||
} else if len(conf.Messaging.Services) > 0 {
|
||||
_ = settings.SetYaml(keys.Messaging, conf.Messaging)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- tariffs")
|
||||
if reset {
|
||||
settings.Delete(keys.Tariffs)
|
||||
} else if conf.Tariffs.Grid.Type != "" || conf.Tariffs.FeedIn.Type != "" || conf.Tariffs.Co2.Type != "" || conf.Tariffs.Planner.Type != "" {
|
||||
_ = settings.SetYaml(keys.Tariffs, conf.Tariffs)
|
||||
}
|
||||
|
||||
log.DEBUG.Println("- circuits")
|
||||
if reset {
|
||||
settings.Delete(keys.Circuits)
|
||||
} else if len(conf.Circuits) > 0 {
|
||||
_ = settings.SetYaml(keys.Circuits, conf.Circuits)
|
||||
}
|
||||
|
||||
// wait for shutdown
|
||||
<-shutdownDoneC()
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ func runPasswordReset(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ func runPasswordSet(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
146
cmd/root.go
146
cmd/root.go
|
|
@ -13,24 +13,24 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
"github.com/evcc-io/evcc/server"
|
||||
"github.com/evcc-io/evcc/server/modbus"
|
||||
"github.com/evcc-io/evcc/server/updater"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/auth"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/pipe"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
"github.com/evcc-io/evcc/util/telemetry"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
vpr "github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
rebootDelay = 5 * time.Minute // delayed reboot on error
|
||||
rebootDelay = 15 * time.Minute // delayed reboot on error
|
||||
serviceDB = "/var/lib/evcc/evcc.db"
|
||||
)
|
||||
|
||||
|
|
@ -122,15 +122,22 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
err = cfgErr
|
||||
}
|
||||
|
||||
// network config
|
||||
if viper.GetString("uri") != "" {
|
||||
log.WARN.Println("`uri` is deprecated and will be ignored. Use `network` instead.")
|
||||
// setup environment
|
||||
if err == nil {
|
||||
err = configureEnvironment(cmd, &conf)
|
||||
}
|
||||
|
||||
log.INFO.Printf("starting ui and api at :%d", conf.Network.Port)
|
||||
// configure network
|
||||
if err == nil {
|
||||
err = networkSettings(&conf.Network)
|
||||
}
|
||||
|
||||
log.INFO.Printf("listening at :%d", conf.Network.Port)
|
||||
|
||||
// start broadcasting values
|
||||
tee := new(util.Tee)
|
||||
valueChan := make(chan util.Param, 64)
|
||||
go tee.Run(valueChan)
|
||||
|
||||
// value cache
|
||||
cache := util.NewCache()
|
||||
|
|
@ -153,18 +160,9 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
// publish to UI
|
||||
go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache)
|
||||
|
||||
// setup values channel
|
||||
valueChan := make(chan util.Param, 64)
|
||||
go tee.Run(valueChan)
|
||||
|
||||
// capture log messages for UI
|
||||
util.CaptureLogs(valueChan)
|
||||
|
||||
// setup environment
|
||||
if err == nil {
|
||||
err = configureEnvironment(cmd, conf)
|
||||
}
|
||||
|
||||
// setup telemetry
|
||||
if err == nil {
|
||||
telemetry.Create(conf.Plant)
|
||||
|
|
@ -175,30 +173,43 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
|
||||
// setup modbus proxy
|
||||
if err == nil {
|
||||
for _, cfg := range conf.ModbusProxy {
|
||||
var mode modbus.ReadOnlyMode
|
||||
mode, err = modbus.ReadOnlyModeString(cfg.ReadOnly)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err = modbus.StartProxy(cfg.Port, cfg.Settings, mode); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = wrapErrorWithClass(ClassModbusProxy, configureModbusProxy(conf.ModbusProxy))
|
||||
}
|
||||
|
||||
// setup site and loadpoints
|
||||
var site *core.Site
|
||||
if err == nil {
|
||||
site, err = configureSiteAndLoadpoints(conf)
|
||||
site, err = configureSiteAndLoadpoints(&conf)
|
||||
}
|
||||
|
||||
// setup database
|
||||
if err == nil && conf.Influx.URL != "" {
|
||||
configureInflux(conf.Influx, site, pipe.NewDropper(append(ignoreLogs, ignoreEmpty)...).Pipe(tee.Attach()))
|
||||
// setup influx
|
||||
if err == nil {
|
||||
influx, ierr := configureInflux(&conf.Influx)
|
||||
if ierr != nil {
|
||||
err = wrapErrorWithClass(ClassInflux, ierr)
|
||||
}
|
||||
|
||||
if err == nil && influx != nil {
|
||||
// eliminate duplicate values
|
||||
dedupe := pipe.NewDeduplicator(30*time.Minute,
|
||||
keys.VehicleSoc, keys.VehicleRange, keys.VehicleOdometer,
|
||||
keys.ChargedEnergy, keys.ChargeRemainingEnergy)
|
||||
go influx.Run(site, dedupe.Pipe(
|
||||
pipe.NewDropper(append(ignoreLogs, ignoreEmpty)...).Pipe(tee.Attach()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// remove previous fatal startup errors
|
||||
valueChan <- util.Param{Key: keys.Fatal, Val: nil}
|
||||
// publish initial settings
|
||||
valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
|
||||
valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
|
||||
valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
|
||||
valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
|
||||
// TODO
|
||||
valueChan <- util.Param{Key: keys.Sponsor, Val: sponsor.Status()}
|
||||
|
||||
// setup mqtt publisher
|
||||
if err == nil && conf.Mqtt.Broker != "" {
|
||||
var mqtt *server.MQTT
|
||||
|
|
@ -214,14 +225,15 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// start HEMS server
|
||||
if err == nil && conf.HEMS.Type != "" {
|
||||
err = configureHEMS(conf.HEMS, site, httpd)
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassHEMS, configureHEMS(conf.HEMS, site, httpd))
|
||||
}
|
||||
|
||||
// setup messaging
|
||||
var pushChan chan push.Event
|
||||
if err == nil {
|
||||
pushChan, err = configureMessengers(conf.Messaging, site.Vehicles(), valueChan, cache)
|
||||
err = wrapErrorWithClass(ClassMessenger, err)
|
||||
}
|
||||
|
||||
// run shutdown functions on stop
|
||||
|
|
@ -246,65 +258,43 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
case <-time.After(conf.Interval):
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
// exit code 1 on error
|
||||
os.Exit(cast.ToInt(err != nil))
|
||||
}()
|
||||
|
||||
auth := auth.New()
|
||||
// allow web access for vehicles
|
||||
configureAuth(conf.Network, config.Instances(config.Vehicles().Devices()), httpd.Router(), valueChan)
|
||||
|
||||
// show main ui
|
||||
httpd.RegisterSystemHandler(valueChan, cache, func() {
|
||||
log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.")
|
||||
once.Do(func() { close(stopC) }) // signal loop to end
|
||||
})
|
||||
|
||||
// show and check version, reduce api load during development
|
||||
if server.Version != server.DevVersion {
|
||||
valueChan <- util.Param{Key: keys.Version, Val: server.FormattedVersion()}
|
||||
go updater.Run(log, httpd, valueChan)
|
||||
}
|
||||
|
||||
// setup site
|
||||
if err == nil {
|
||||
httpd.RegisterSiteHandlers(site, auth, cache)
|
||||
httpd.RegisterAuthHandlers(auth)
|
||||
httpd.RegisterSystemHandler(auth, func() {
|
||||
log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.")
|
||||
once.Do(func() { close(stopC) }) // signal loop to end
|
||||
})
|
||||
|
||||
// set channels
|
||||
site.DumpConfig()
|
||||
site.Prepare(valueChan, pushChan)
|
||||
|
||||
// show and check version, reduce api load during development
|
||||
if server.Version != server.DevVersion {
|
||||
valueChan <- util.Param{Key: "version", Val: server.FormattedVersion()}
|
||||
go updater.Run(log, httpd, valueChan)
|
||||
}
|
||||
|
||||
// remove previous fatal startup errors
|
||||
valueChan <- util.Param{Key: "fatal", Val: nil}
|
||||
|
||||
// expose sponsor to UI
|
||||
if sponsor.Subject != "" {
|
||||
valueChan <- util.Param{Key: "sponsor", Val: sponsor.Subject}
|
||||
var validDuration time.Duration
|
||||
if d := time.Until(sponsor.ExpiresAt); d > 0 && d < 30*24*time.Hour {
|
||||
validDuration = d
|
||||
}
|
||||
valueChan <- util.Param{Key: "sponsorTokenExpires", Val: validDuration}
|
||||
}
|
||||
|
||||
// allow web access for vehicles
|
||||
configureAuth(conf.Network, config.Instances(config.Vehicles().Devices()), httpd.Router(), valueChan)
|
||||
httpd.RegisterSiteHandlers(site, valueChan)
|
||||
|
||||
go func() {
|
||||
site.Run(stopC, conf.Interval)
|
||||
}()
|
||||
} else {
|
||||
httpd.RegisterAuthHandlers(auth)
|
||||
httpd.RegisterSystemHandler(auth, func() {
|
||||
log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.")
|
||||
once.Do(func() { close(stopC) }) // signal loop to end
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// improve error message
|
||||
err = wrapErrors(err)
|
||||
|
||||
publishErrorInfo(valueChan, cfgFile, err)
|
||||
err = wrapError(err)
|
||||
valueChan <- util.Param{Key: keys.Fatal, Val: err}
|
||||
|
||||
// TODO stop reboot loop if user updates config (or show countdown in UI)
|
||||
log.FATAL.Println(err)
|
||||
log.FATAL.Printf("will attempt restart in: %v", rebootDelay)
|
||||
|
||||
|
|
@ -317,5 +307,5 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
// uds health check listener
|
||||
go server.HealthListener(site)
|
||||
|
||||
log.FATAL.Println(wrapErrors(httpd.ListenAndServe()))
|
||||
log.FATAL.Println(wrapError(httpd.ListenAndServe()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ func runSettingsGet(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
// setup persistence
|
||||
if err := configureDatabase(conf.Database); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ func runSettingsSet(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
// setup persistence
|
||||
if err := configureDatabase(conf.Database); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
406
cmd/setup.go
406
cmd/setup.go
|
|
@ -5,7 +5,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
|
|
@ -17,10 +16,12 @@ import (
|
|||
|
||||
paho "github.com/eclipse/paho.mqtt.golang"
|
||||
"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/eebus"
|
||||
"github.com/evcc-io/evcc/cmd/shutdown"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/hems"
|
||||
"github.com/evcc-io/evcc/meter"
|
||||
|
|
@ -31,14 +32,13 @@ import (
|
|||
"github.com/evcc-io/evcc/server"
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/evcc-io/evcc/server/modbus"
|
||||
"github.com/evcc-io/evcc/server/oauth2redirect"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/evcc-io/evcc/util/machine"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
"github.com/evcc-io/evcc/util/pipe"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
"github.com/evcc-io/evcc/util/templates"
|
||||
|
|
@ -52,18 +52,18 @@ import (
|
|||
"golang.org/x/text/currency"
|
||||
)
|
||||
|
||||
var conf = globalConfig{
|
||||
var conf = globalconfig.All{
|
||||
Interval: 10 * time.Second,
|
||||
Log: "info",
|
||||
Network: networkConfig{
|
||||
Network: globalconfig.Network{
|
||||
Schema: "http",
|
||||
Host: "evcc.local",
|
||||
Port: 7070,
|
||||
},
|
||||
Mqtt: mqttConfig{
|
||||
Mqtt: globalconfig.Mqtt{
|
||||
Topic: "evcc",
|
||||
},
|
||||
Database: dbConfig{
|
||||
Database: globalconfig.DB{
|
||||
Type: "sqlite",
|
||||
Dsn: "~/.evcc/evcc.db",
|
||||
},
|
||||
|
|
@ -71,90 +71,6 @@ var conf = globalConfig{
|
|||
|
||||
var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
|
||||
|
||||
type globalConfig struct {
|
||||
Network networkConfig
|
||||
Log string
|
||||
SponsorToken string
|
||||
Plant string // telemetry plant id
|
||||
Telemetry bool
|
||||
Metrics bool
|
||||
Profile bool
|
||||
Levels map[string]string
|
||||
Interval time.Duration
|
||||
Database dbConfig
|
||||
Mqtt mqttConfig
|
||||
ModbusProxy []proxyConfig
|
||||
Javascript []javascriptConfig
|
||||
Go []goConfig
|
||||
Influx server.InfluxConfig
|
||||
EEBus map[string]interface{}
|
||||
HEMS config.Typed
|
||||
Messaging messagingConfig
|
||||
Meters []config.Named
|
||||
Chargers []config.Named
|
||||
Vehicles []config.Named
|
||||
Tariffs tariffConfig
|
||||
Site map[string]interface{}
|
||||
Loadpoints []map[string]interface{}
|
||||
Circuits []config.Named
|
||||
}
|
||||
|
||||
type mqttConfig struct {
|
||||
mqtt.Config `mapstructure:",squash"`
|
||||
Topic string
|
||||
}
|
||||
|
||||
type javascriptConfig struct {
|
||||
VM string
|
||||
Script string
|
||||
}
|
||||
|
||||
type goConfig struct {
|
||||
VM string
|
||||
Script string
|
||||
}
|
||||
|
||||
type proxyConfig struct {
|
||||
Port int
|
||||
ReadOnly string
|
||||
modbus.Settings `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
Type string
|
||||
Dsn string
|
||||
}
|
||||
|
||||
type messagingConfig struct {
|
||||
Events map[string]push.EventTemplateConfig
|
||||
Services []config.Typed
|
||||
}
|
||||
|
||||
type tariffConfig struct {
|
||||
Currency string
|
||||
Grid config.Typed
|
||||
FeedIn config.Typed
|
||||
Co2 config.Typed
|
||||
Planner config.Typed
|
||||
}
|
||||
|
||||
type networkConfig struct {
|
||||
Schema string
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (c networkConfig) HostPort() string {
|
||||
if c.Schema == "http" && c.Port == 80 || c.Schema == "https" && c.Port == 443 {
|
||||
return c.Host
|
||||
}
|
||||
return net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
||||
}
|
||||
|
||||
func (c networkConfig) URI() string {
|
||||
return fmt.Sprintf("%s://%s", c.Schema, c.HostPort())
|
||||
}
|
||||
|
||||
func nameValid(name string) error {
|
||||
if !nameRE.MatchString(name) {
|
||||
return fmt.Errorf("name must not contain special characters or spaces: %s", name)
|
||||
|
|
@ -178,7 +94,7 @@ func tokenDanger(conf []config.Named) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func loadConfigFile(conf *globalConfig, checkDB bool) error {
|
||||
func loadConfigFile(conf *globalconfig.All, checkDB bool) error {
|
||||
err := viper.ReadInConfig()
|
||||
|
||||
if cfgFile = viper.ConfigFileUsed(); cfgFile == "" {
|
||||
|
|
@ -339,11 +255,11 @@ func configureMeters(static []config.Named, names ...string) error {
|
|||
|
||||
instance, err := meter.NewFromConfig(cc.Type, cc.Other)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)
|
||||
return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
|
||||
}
|
||||
|
||||
if err := config.Meters().Add(config.NewStaticDevice(cc, instance)); err != nil {
|
||||
return err
|
||||
return &DeviceError{cc.Name, err}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -360,13 +276,15 @@ func configureMeters(static []config.Named, names ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TOTO add fake devices
|
||||
|
||||
instance, err := meter.NewFromConfig(cc.Type, cc.Other)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)
|
||||
return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
|
||||
}
|
||||
|
||||
if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
|
||||
return err
|
||||
return &DeviceError{cc.Name, err}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -392,10 +310,14 @@ func configureChargers(static []config.Named, names ...string) error {
|
|||
g.Go(func() error {
|
||||
instance, err := charger.NewFromConfig(cc.Type, cc.Other)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
|
||||
return &DeviceError{cc.Name, fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)}
|
||||
}
|
||||
|
||||
return config.Chargers().Add(config.NewStaticDevice(cc, instance))
|
||||
if err := config.Chargers().Add(config.NewStaticDevice(cc, instance)); err != nil {
|
||||
return &DeviceError{cc.Name, err}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -413,12 +335,18 @@ func configureChargers(static []config.Named, names ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TOTO add fake devices
|
||||
|
||||
instance, err := charger.NewFromConfig(cc.Type, cc.Other)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
|
||||
}
|
||||
|
||||
return config.Chargers().Add(config.NewConfigurableDevice(conf, instance))
|
||||
if err := config.Chargers().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
|
||||
return &DeviceError{cc.Name, err}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +466,19 @@ func configureVehicles(static []config.Named, names ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func configureEnvironment(cmd *cobra.Command, conf globalConfig) (err error) {
|
||||
func configureSponsorship(token string) (err error) {
|
||||
if settings.Exists(keys.SponsorToken) {
|
||||
if token, err = settings.String(keys.SponsorToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO migrate settings
|
||||
|
||||
return sponsor.ConfigureSponsorship(token)
|
||||
}
|
||||
|
||||
func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) (err error) {
|
||||
// full http request log
|
||||
if cmd.Flags().Lookup(flagHeaders).Changed {
|
||||
request.LogHeaders = true
|
||||
|
|
@ -546,46 +486,49 @@ func configureEnvironment(cmd *cobra.Command, conf globalConfig) (err error) {
|
|||
|
||||
// setup machine id
|
||||
if conf.Plant != "" {
|
||||
// TODO decide wrapping
|
||||
err = machine.CustomID(conf.Plant)
|
||||
}
|
||||
|
||||
// setup sponsorship (allow env override)
|
||||
if err == nil {
|
||||
err = sponsor.ConfigureSponsorship(conf.SponsorToken)
|
||||
err = wrapErrorWithClass(ClassSponsorship, configureSponsorship(conf.SponsorToken))
|
||||
}
|
||||
|
||||
// setup translations
|
||||
if err == nil {
|
||||
// TODO decide wrapping
|
||||
err = locale.Init()
|
||||
}
|
||||
|
||||
// setup persistence
|
||||
if err == nil && conf.Database.Dsn != "" {
|
||||
err = configureDatabase(conf.Database)
|
||||
err = wrapErrorWithClass(ClassDatabase, configureDatabase(conf.Database))
|
||||
}
|
||||
|
||||
// setup mqtt client listener
|
||||
if err == nil && conf.Mqtt.Broker != "" {
|
||||
err = configureMQTT(conf.Mqtt)
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassMqtt, configureMqtt(&conf.Mqtt))
|
||||
}
|
||||
|
||||
// setup EEBus server
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassEEBus, configureEEBus(conf.EEBus))
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
if err == nil {
|
||||
err = configureJavascript(conf.Javascript)
|
||||
err = wrapErrorWithClass(ClassJavascript, configureJavascript(conf.Javascript))
|
||||
}
|
||||
|
||||
// setup go VMs
|
||||
if err == nil {
|
||||
err = configureGo(conf.Go)
|
||||
}
|
||||
|
||||
// setup EEBus server
|
||||
if err == nil && conf.EEBus != nil {
|
||||
err = configureEEBus(conf.EEBus)
|
||||
err = wrapErrorWithClass(ClassGo, configureGo(conf.Go))
|
||||
}
|
||||
|
||||
// setup config database
|
||||
if err == nil {
|
||||
// TODO decide wrapping
|
||||
err = config.Init(db.Instance)
|
||||
}
|
||||
|
||||
|
|
@ -593,7 +536,7 @@ func configureEnvironment(cmd *cobra.Command, conf globalConfig) (err error) {
|
|||
}
|
||||
|
||||
// configureDatabase configures session database
|
||||
func configureDatabase(conf dbConfig) error {
|
||||
func configureDatabase(conf globalconfig.DB) error {
|
||||
if err := db.NewInstance(conf.Type, conf.Dsn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -622,7 +565,26 @@ func configureDatabase(conf dbConfig) error {
|
|||
}
|
||||
|
||||
// configureInflux configures influx database
|
||||
func configureInflux(conf server.InfluxConfig, site site.API, in <-chan util.Param) {
|
||||
func configureInflux(conf *globalconfig.Influx) (*server.Influx, error) {
|
||||
// read settings
|
||||
if settings.Exists(keys.Influx) {
|
||||
if err := settings.Json(keys.Influx, &conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if conf.URL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// // migrate settings
|
||||
// if !settings.Exists(keys.Influx) {
|
||||
// if err := settings.SetJson(keys.Influx, conf); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
|
||||
influx := server.NewInfluxClient(
|
||||
conf.URL,
|
||||
conf.Token,
|
||||
|
|
@ -632,15 +594,29 @@ func configureInflux(conf server.InfluxConfig, site site.API, in <-chan util.Par
|
|||
conf.Database,
|
||||
)
|
||||
|
||||
// eliminate duplicate values
|
||||
dedupe := pipe.NewDeduplicator(30*time.Minute, "vehicleCapacity", "vehicleSoc", "vehicleRange", "vehicleOdometer", "chargedEnergy", "chargeRemainingEnergy")
|
||||
in = dedupe.Pipe(in)
|
||||
|
||||
go influx.Run(site, in)
|
||||
return influx, nil
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
func configureMQTT(conf mqttConfig) error {
|
||||
func configureMqtt(conf *globalconfig.Mqtt) error {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.Mqtt) {
|
||||
if err := settings.Json(keys.Mqtt, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// } else {
|
||||
// // migrate settings & write defaults
|
||||
// if err := settings.SetJson(keys.Mqtt, conf); err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
|
||||
if conf.Broker == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log := util.NewLogger("mqtt")
|
||||
|
||||
instance, err := mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, conf.ClientID, 1, conf.Insecure, func(options *paho.ClientOptions) {
|
||||
|
|
@ -662,7 +638,7 @@ func configureMQTT(conf mqttConfig) error {
|
|||
}
|
||||
|
||||
// setup javascript
|
||||
func configureJavascript(conf []javascriptConfig) error {
|
||||
func configureJavascript(conf []globalconfig.Javascript) error {
|
||||
for _, cc := range conf {
|
||||
if _, err := javascript.RegisteredVM(cc.VM, cc.Script); err != nil {
|
||||
return fmt.Errorf("failed configuring javascript: %w", err)
|
||||
|
|
@ -672,7 +648,7 @@ func configureJavascript(conf []javascriptConfig) error {
|
|||
}
|
||||
|
||||
// setup go
|
||||
func configureGo(conf []goConfig) error {
|
||||
func configureGo(conf []globalconfig.Go) error {
|
||||
for _, cc := range conf {
|
||||
if _, err := golang.RegisteredVM(cc.VM, cc.Script); err != nil {
|
||||
return fmt.Errorf("failed configuring go: %w", err)
|
||||
|
|
@ -683,6 +659,25 @@ func configureGo(conf []goConfig) error {
|
|||
|
||||
// setup HEMS
|
||||
func configureHEMS(conf config.Typed, site *core.Site, httpd *server.HTTPd) error {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.Hems) {
|
||||
if err := settings.Yaml(keys.Hems, new(map[string]any), &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Type == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// // migrate settings
|
||||
// if !settings.Exists(keys.Hems) {
|
||||
// if err := settings.SetYaml(keys.Hems, conf); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
hems, err := hems.NewFromConfig(conf.Type, conf.Other, site, httpd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed configuring hems: %w", err)
|
||||
|
|
@ -693,8 +688,21 @@ func configureHEMS(conf config.Typed, site *core.Site, httpd *server.HTTPd) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// networkSettings reads/migrates network settings
|
||||
func networkSettings(conf *globalconfig.Network) error {
|
||||
if settings.Exists(keys.Network) {
|
||||
return settings.Json(keys.Network, &conf)
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// // migrate settings
|
||||
// return settings.SetJson(keys.Network, conf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setup MDNS
|
||||
func configureMDNS(conf networkConfig) error {
|
||||
func configureMDNS(conf globalconfig.Network) error {
|
||||
host := strings.TrimSuffix(conf.Host, ".local")
|
||||
|
||||
zc, err := zeroconf.RegisterProxy("evcc", "_http._tcp", "local.", conf.Port, host, nil, []string{"path=/"}, nil)
|
||||
|
|
@ -708,7 +716,26 @@ func configureMDNS(conf networkConfig) error {
|
|||
}
|
||||
|
||||
// setup EEBus
|
||||
func configureEEBus(conf map[string]interface{}) error {
|
||||
func configureEEBus(conf eebus.Config) error {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.EEBus) {
|
||||
if err := settings.Yaml(keys.EEBus, new(map[string]any), &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if conf.URI == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// // migrate settings
|
||||
// if !settings.Exists(keys.EEBus) {
|
||||
// if err := settings.SetYaml(keys.EEBus, conf); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
var err error
|
||||
if eebus.Instance, err = eebus.NewServer(conf); err != nil {
|
||||
return fmt.Errorf("failed configuring eebus: %w", err)
|
||||
|
|
@ -721,7 +748,20 @@ func configureEEBus(conf map[string]interface{}) error {
|
|||
}
|
||||
|
||||
// setup messaging
|
||||
func configureMessengers(conf messagingConfig, vehicles push.Vehicles, valueChan chan util.Param, cache *util.Cache) (chan push.Event, error) {
|
||||
func configureMessengers(conf globalconfig.Messaging, vehicles push.Vehicles, valueChan chan<- util.Param, cache *util.Cache) (chan push.Event, error) {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.Messaging) {
|
||||
if err := settings.Yaml(keys.Messaging, new(map[string]any), &conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// } else if len(conf.Services)+len(conf.Events) > 0 {
|
||||
// if err := settings.SetYaml(keys.Messaging, conf); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
}
|
||||
|
||||
messageChan := make(chan push.Event, 1)
|
||||
|
||||
messageHub, err := push.NewHub(conf.Events, vehicles, cache)
|
||||
|
|
@ -742,23 +782,34 @@ func configureMessengers(conf messagingConfig, vehicles push.Vehicles, valueChan
|
|||
return messageChan, nil
|
||||
}
|
||||
|
||||
func configureTariff(name string, conf config.Typed, t *api.Tariff, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
func configureTariff(name string, conf config.Typed, t *api.Tariff) error {
|
||||
if conf.Type == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
res, err := tariff.NewFromConfig(conf.Type, conf.Other)
|
||||
if err != nil {
|
||||
log.ERROR.Printf("failed configuring %s tariff: %v", name, err)
|
||||
return
|
||||
return &DeviceError{name, err}
|
||||
}
|
||||
|
||||
*t = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureTariffs(conf tariffConfig) (*tariff.Tariffs, error) {
|
||||
func configureTariffs(conf globalconfig.Tariffs) (*tariff.Tariffs, error) {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.Tariffs) {
|
||||
if err := settings.Yaml(keys.Tariffs, new(map[string]any), &conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// } else if conf.Grid.Type != "" || conf.FeedIn.Type != "" || conf.Co2.Type != "" || conf.Planner.Type != "" {
|
||||
// if err := settings.SetYaml(keys.Tariffs, conf); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
}
|
||||
|
||||
tariffs := tariff.Tariffs{
|
||||
Currency: currency.EUR,
|
||||
}
|
||||
|
|
@ -767,45 +818,91 @@ func configureTariffs(conf tariffConfig) (*tariff.Tariffs, error) {
|
|||
tariffs.Currency = currency.MustParseISO(conf.Currency)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(4)
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
g.Go(func() error { return configureTariff("grid", conf.Grid, &tariffs.Grid) })
|
||||
g.Go(func() error { return configureTariff("feedin", conf.FeedIn, &tariffs.FeedIn) })
|
||||
g.Go(func() error { return configureTariff("co2", conf.Co2, &tariffs.Co2) })
|
||||
g.Go(func() error { return configureTariff("planner", conf.Planner, &tariffs.Planner) })
|
||||
|
||||
go configureTariff("grid", conf.Grid, &tariffs.Grid, &wg)
|
||||
go configureTariff("feedin", conf.FeedIn, &tariffs.FeedIn, &wg)
|
||||
go configureTariff("co2", conf.Co2, &tariffs.Co2, &wg)
|
||||
go configureTariff("planner", conf.Planner, &tariffs.Planner, &wg)
|
||||
|
||||
wg.Wait()
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, &ClassError{ClassTariff, err}
|
||||
}
|
||||
|
||||
return &tariffs, nil
|
||||
}
|
||||
|
||||
func configureDevices(conf globalConfig) error {
|
||||
func configureDevices(conf globalconfig.All) error {
|
||||
// TODO: add name/identifier to error for better highlighting in UI
|
||||
if err := configureMeters(conf.Meters); err != nil {
|
||||
return err
|
||||
return &ClassError{ClassMeter, err}
|
||||
}
|
||||
if err := configureChargers(conf.Chargers); err != nil {
|
||||
return err
|
||||
return &ClassError{ClassCharger, err}
|
||||
}
|
||||
if err := configureVehicles(conf.Vehicles); err != nil {
|
||||
return &ClassError{ClassVehicle, err}
|
||||
}
|
||||
if err := configureCircuits(conf.Circuits); err != nil {
|
||||
return err
|
||||
return &ClassError{ClassCircuit, err}
|
||||
}
|
||||
return configureVehicles(conf.Vehicles)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
|
||||
if err := configureDevices(conf); err != nil {
|
||||
func configureModbusProxy(conf []globalconfig.ModbusProxy) error {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.ModbusProxy) {
|
||||
if err := settings.Yaml(keys.ModbusProxy, new([]map[string]any), &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO remove yaml file
|
||||
// } else if len(conf) > 0 {
|
||||
// if err := settings.SetYaml(keys.ModbusProxy, conf); err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
|
||||
for _, cfg := range conf {
|
||||
var mode modbus.ReadOnlyMode
|
||||
mode, err := modbus.ReadOnlyModeString(cfg.ReadOnly)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = modbus.StartProxy(cfg.Port, cfg.Settings, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) {
|
||||
// migrate settings
|
||||
if settings.Exists(keys.Interval) {
|
||||
d, err := settings.Int(keys.Interval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.Interval = time.Duration(d)
|
||||
|
||||
// TODO remove yaml file
|
||||
// } else if conf.Interval != 0 {
|
||||
// settings.SetInt(keys.Interval, int64(conf.Interval))
|
||||
}
|
||||
|
||||
if err := configureDevices(*conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadpoints, err := configureLoadpoints(conf)
|
||||
loadpoints, err := configureLoadpoints(*conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed configuring loadpoints: %w", err)
|
||||
}
|
||||
|
||||
tariffs, err := configureTariffs(conf.Tariffs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &ClassError{ClassTariff, err}
|
||||
}
|
||||
|
||||
site, err := configureSite(conf.Site, loadpoints, tariffs)
|
||||
|
|
@ -815,7 +912,7 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
|
|||
|
||||
if len(config.Circuits().Devices()) > 0 {
|
||||
if err := validateCircuits(site, loadpoints); err != nil {
|
||||
return nil, err
|
||||
return nil, &ClassError{ClassCircuit, err}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -848,8 +945,13 @@ CONTINUE:
|
|||
}
|
||||
|
||||
func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
|
||||
site, err := core.NewSiteFromConfig(log, conf, loadpoints, tariffs)
|
||||
site, err := core.NewSiteFromConfig(conf)
|
||||
if err != nil {
|
||||
// TODO proper handling
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := site.Boot(log, loadpoints, tariffs); err != nil {
|
||||
return nil, fmt.Errorf("failed configuring site: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -860,7 +962,7 @@ func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, ta
|
|||
return site, nil
|
||||
}
|
||||
|
||||
func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) {
|
||||
func configureLoadpoints(conf globalconfig.All) ([]*core.Loadpoint, error) {
|
||||
if len(conf.Loadpoints) == 0 {
|
||||
return nil, errors.New("missing loadpoints")
|
||||
}
|
||||
|
|
@ -883,7 +985,7 @@ func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) {
|
|||
}
|
||||
|
||||
// configureAuth handles routing for devices. For now only api.AuthProvider related routes
|
||||
func configureAuth(conf networkConfig, vehicles []api.Vehicle, router *mux.Router, paramC chan<- util.Param) {
|
||||
func configureAuth(conf globalconfig.Network, vehicles []api.Vehicle, router *mux.Router, paramC chan<- util.Param) {
|
||||
auth := router.PathPrefix("/oauth").Subrouter()
|
||||
auth.Use(handlers.CompressHandler)
|
||||
auth.Use(handlers.CORS(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/meter"
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
|
|
@ -35,7 +36,7 @@ func (suite *circuitsTestSuite) SetupTest() {
|
|||
}
|
||||
|
||||
func (suite *circuitsTestSuite) TestCircuitConf() {
|
||||
var conf globalConfig
|
||||
var conf globalconfig.All
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`circuits:
|
||||
|
|
@ -67,7 +68,7 @@ loadpoints:
|
|||
}
|
||||
|
||||
func (suite *circuitsTestSuite) TestLoadpointMissingCircuitError() {
|
||||
var conf globalConfig
|
||||
var conf globalconfig.All
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
|
||||
|
|
@ -99,7 +100,7 @@ loadpoints:
|
|||
}
|
||||
|
||||
func (suite *circuitsTestSuite) TestSiteMissingCircuitError() {
|
||||
var conf globalConfig
|
||||
var conf globalconfig.All
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/core"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
func TestYamlOff(t *testing.T) {
|
||||
var conf globalConfig
|
||||
var conf globalconfig.All
|
||||
viper.SetConfigType("yaml")
|
||||
if err := viper.ReadConfig(strings.NewReader(`loadpoints:
|
||||
- mode: off
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func runTariff(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/vehicle"
|
||||
|
|
@ -100,7 +101,7 @@ func tronityAuthorize(addr string, oc *oauth2.Config) (*oauth2.Token, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func tronityToken(conf globalConfig, vehicleConf config.Named) (*oauth2.Token, error) {
|
||||
func tronityToken(conf globalconfig.All, vehicleConf config.Named) (*oauth2.Token, error) {
|
||||
var cc struct {
|
||||
Credentials vehicle.ClientCredentials
|
||||
RedirectURI string
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func runVehicle(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// setup environment
|
||||
if err := configureEnvironment(cmd, conf); err != nil {
|
||||
if err := configureEnvironment(cmd, &conf); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
18
core/keys/global.go
Normal file
18
core/keys/global.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package keys
|
||||
|
||||
const (
|
||||
Interval = "interval"
|
||||
PasswordConfigured = "passwordConfigured"
|
||||
Sponsor = "sponsor"
|
||||
SponsorToken = "sponsorToken"
|
||||
Network = "network"
|
||||
Mqtt = "mqtt"
|
||||
Influx = "influx"
|
||||
EEBus = "eebus"
|
||||
Hems = "hems"
|
||||
Messaging = "messaging"
|
||||
ModbusProxy = "modbusproxy"
|
||||
Tariffs = "tariffs"
|
||||
Version = "version"
|
||||
Fatal = "fatal"
|
||||
)
|
||||
|
|
@ -29,8 +29,6 @@ const (
|
|||
TariffPriceLoadpoints = "tariffPriceLoadpoints"
|
||||
Vehicles = "vehicles"
|
||||
Circuits = "circuits"
|
||||
PasswordConfigured = "passwordConfigured"
|
||||
Interval = "interval"
|
||||
|
||||
// meters
|
||||
GridMeter = "gridMeter"
|
||||
|
|
@ -39,10 +37,11 @@ const (
|
|||
AuxMeters = "auxMeters"
|
||||
|
||||
// battery settings
|
||||
BatteryCapacity = "batteryCapacity"
|
||||
BatteryDischargeControl = "batteryDischargeControl"
|
||||
BufferSoc = "bufferSoc"
|
||||
BufferStartSoc = "bufferStartSoc"
|
||||
BatteryCapacity = "batteryCapacity"
|
||||
BatteryDischargeControl = "batteryDischargeControl"
|
||||
BufferSoc = "bufferSoc"
|
||||
BufferStartSoc = "bufferStartSoc"
|
||||
MaxGridSupplyWhileBatteryCharging = "maxGridSupplyWhileBatteryCharging"
|
||||
|
||||
// battery status
|
||||
Battery = "battery"
|
||||
|
|
|
|||
87
core/site.go
87
core/site.go
|
|
@ -81,10 +81,11 @@ type Site struct {
|
|||
auxMeters []api.Meter // Auxiliary meters
|
||||
|
||||
// battery settings
|
||||
prioritySoc float64 // prefer battery up to this Soc
|
||||
bufferSoc float64 // continue charging on battery above this Soc
|
||||
bufferStartSoc float64 // start charging on battery above this Soc
|
||||
batteryDischargeControl bool // prevent battery discharge for fast and planned charging
|
||||
prioritySoc float64 // prefer battery up to this Soc
|
||||
bufferSoc float64 // continue charging on battery above this Soc
|
||||
bufferStartSoc float64 // start charging on battery above this Soc
|
||||
maxGridSupplyWhileBatteryCharging float64 // ignore battery charging if AC consumption is above this value
|
||||
batteryDischargeControl bool // prevent battery discharge for fast and planned charging
|
||||
|
||||
loadpoints []*Loadpoint // Loadpoints
|
||||
tariffs *tariff.Tariffs // Tariffs
|
||||
|
|
@ -102,7 +103,7 @@ type Site struct {
|
|||
publishCache map[string]any // store last published values to avoid unnecessary republishing
|
||||
}
|
||||
|
||||
// MetersConfig contains the loadpoint's meter configuration
|
||||
// MetersConfig contains the site's meter configuration
|
||||
type MetersConfig struct {
|
||||
GridMeterRef string `mapstructure:"grid"` // Grid usage meter
|
||||
PVMetersRef []string `mapstructure:"pv"` // PV meter
|
||||
|
|
@ -111,18 +112,24 @@ type MetersConfig struct {
|
|||
}
|
||||
|
||||
// NewSiteFromConfig creates a new site
|
||||
func NewSiteFromConfig(
|
||||
log *util.Logger,
|
||||
other map[string]interface{},
|
||||
loadpoints []*Loadpoint,
|
||||
tariffs *tariff.Tariffs,
|
||||
) (*Site, error) {
|
||||
func NewSiteFromConfig(other map[string]interface{}) (*Site, error) {
|
||||
site := NewSite()
|
||||
|
||||
// TODO remove
|
||||
if err := util.DecodeOther(other, site); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add meters from config
|
||||
site.restoreMetersAndTitle()
|
||||
|
||||
// TODO title
|
||||
Voltage = site.Voltage
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tariff.Tariffs) error {
|
||||
site.loadpoints = loadpoints
|
||||
site.tariffs = tariffs
|
||||
|
||||
|
|
@ -150,11 +157,11 @@ func NewSiteFromConfig(
|
|||
if db.Instance != nil {
|
||||
var err error
|
||||
if lp.db, err = session.NewStore(lp.Title(), db.Instance); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
// Fix any dangling history
|
||||
if err := lp.db.ClosePendingSessionsInHistory(lp.chargeMeterTotal()); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE: this requires stopSession to respect async access
|
||||
|
|
@ -162,14 +169,11 @@ func NewSiteFromConfig(
|
|||
}
|
||||
}
|
||||
|
||||
// add meters from config
|
||||
site.restoreMeters()
|
||||
|
||||
// circuit
|
||||
if site.CircuitRef != "" {
|
||||
dev, err := config.Circuits().ByName(site.CircuitRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
site.circuit = dev.Instance()
|
||||
}
|
||||
|
|
@ -178,7 +182,7 @@ func NewSiteFromConfig(
|
|||
if site.Meters.GridMeterRef != "" {
|
||||
dev, err := config.Meters().ByName(site.Meters.GridMeterRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
site.gridMeter = dev.Instance()
|
||||
}
|
||||
|
|
@ -187,7 +191,7 @@ func NewSiteFromConfig(
|
|||
for _, ref := range site.Meters.PVMetersRef {
|
||||
dev, err := config.Meters().ByName(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
site.pvMeters = append(site.pvMeters, dev.Instance())
|
||||
}
|
||||
|
|
@ -196,12 +200,12 @@ func NewSiteFromConfig(
|
|||
for _, ref := range site.Meters.BatteryMetersRef {
|
||||
dev, err := config.Meters().ByName(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
site.batteryMeters = append(site.batteryMeters, dev.Instance())
|
||||
}
|
||||
|
||||
if len(site.batteryMeters) > 0 && site.ResidualPower <= 0 {
|
||||
if len(site.batteryMeters) > 0 && site.GetResidualPower() <= 0 {
|
||||
site.log.WARN.Println("battery configured but residualPower is missing or <= 0 (add residualPower: 100 to site), see https://docs.evcc.io/en/docs/reference/configuration/site#residualpower")
|
||||
}
|
||||
|
||||
|
|
@ -209,14 +213,14 @@ func NewSiteFromConfig(
|
|||
for _, ref := range site.Meters.AuxMetersRef {
|
||||
dev, err := config.Meters().ByName(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
site.auxMeters = append(site.auxMeters, dev.Instance())
|
||||
}
|
||||
|
||||
// configure meter from references
|
||||
if site.gridMeter == nil && len(site.pvMeters) == 0 {
|
||||
return nil, errors.New("missing either grid or pv meter")
|
||||
return errors.New("missing either grid or pv meter")
|
||||
}
|
||||
|
||||
// revert battery mode on shutdown
|
||||
|
|
@ -228,7 +232,7 @@ func NewSiteFromConfig(
|
|||
}
|
||||
})
|
||||
|
||||
return site, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSite creates a Site with sane defaults
|
||||
|
|
@ -242,11 +246,14 @@ func NewSite() *Site {
|
|||
return lp
|
||||
}
|
||||
|
||||
// restoreMeters restores site meter configuration
|
||||
func (site *Site) restoreMeters() {
|
||||
// restoreMetersAndTitle restores site meter configuration
|
||||
func (site *Site) restoreMetersAndTitle() {
|
||||
if testing.Testing() {
|
||||
return
|
||||
}
|
||||
if v, err := settings.String(keys.Title); err == nil {
|
||||
site.Title = v
|
||||
}
|
||||
if v, err := settings.String(keys.GridMeter); err == nil && v != "" {
|
||||
site.Meters.GridMeterRef = v
|
||||
}
|
||||
|
|
@ -266,9 +273,6 @@ func (site *Site) restoreSettings() error {
|
|||
if testing.Testing() {
|
||||
return nil
|
||||
}
|
||||
if v, err := settings.String(keys.Title); err == nil {
|
||||
site.Title = v
|
||||
}
|
||||
if v, err := settings.Float(keys.BufferSoc); err == nil {
|
||||
if err := site.SetBufferSoc(v); err != nil {
|
||||
return err
|
||||
|
|
@ -279,6 +283,12 @@ func (site *Site) restoreSettings() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
// TODO migrate from YAML
|
||||
if v, err := settings.Float(keys.MaxGridSupplyWhileBatteryCharging); err == nil {
|
||||
if err := site.SetMaxGridSupplyWhileBatteryCharging(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v, err := settings.Float(keys.PrioritySoc); err == nil {
|
||||
if err := site.SetPrioritySoc(v); err != nil {
|
||||
return err
|
||||
|
|
@ -289,6 +299,11 @@ func (site *Site) restoreSettings() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if v, err := settings.Float(keys.ResidualPower); err == nil {
|
||||
if err := site.SetResidualPower(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -394,10 +409,6 @@ func (site *Site) publish(key string, val interface{}) {
|
|||
return
|
||||
}
|
||||
|
||||
if s, ok := val.(fmt.Stringer); ok {
|
||||
val = s.String()
|
||||
}
|
||||
|
||||
site.uiChan <- util.Param{Key: key, Val: val}
|
||||
}
|
||||
|
||||
|
|
@ -625,7 +636,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
|
||||
// allow using grid and charge as estimate for pv power
|
||||
if site.pvMeters == nil {
|
||||
site.pvPower = totalChargePower - site.gridPower + site.ResidualPower
|
||||
site.pvPower = totalChargePower - site.gridPower + site.GetResidualPower()
|
||||
if site.pvPower < 0 {
|
||||
site.pvPower = 0
|
||||
}
|
||||
|
|
@ -654,7 +665,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
}
|
||||
}
|
||||
|
||||
sitePower := sitePower(site.log, site.MaxGridSupplyWhileBatteryCharging, site.gridPower, batteryPower, site.ResidualPower)
|
||||
sitePower := sitePower(site.log, site.GetMaxGridSupplyWhileBatteryCharging(), site.gridPower, batteryPower, site.GetResidualPower())
|
||||
|
||||
// deduct smart loads
|
||||
if len(site.auxMeters) > 0 {
|
||||
|
|
@ -837,12 +848,13 @@ func (site *Site) prepare() {
|
|||
site.publish(keys.GridConfigured, site.gridMeter != nil)
|
||||
site.publish(keys.Pv, make([]api.Meter, len(site.pvMeters)))
|
||||
site.publish(keys.Battery, make([]api.Meter, len(site.batteryMeters)))
|
||||
site.publish(keys.PrioritySoc, site.prioritySoc)
|
||||
site.publish(keys.BufferSoc, site.bufferSoc)
|
||||
site.publish(keys.BufferStartSoc, site.bufferStartSoc)
|
||||
site.publish(keys.PrioritySoc, site.prioritySoc)
|
||||
site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
|
||||
site.publish(keys.BatteryMode, site.batteryMode)
|
||||
site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
|
||||
site.publish(keys.ResidualPower, site.ResidualPower)
|
||||
site.publish(keys.ResidualPower, site.GetResidualPower())
|
||||
|
||||
site.publish(keys.Currency, site.tariffs.Currency)
|
||||
if tariff := site.GetTariff(PlannerTariff); tariff != nil {
|
||||
|
|
@ -921,7 +933,6 @@ func (site *Site) Run(stopC chan struct{}, interval time.Duration) {
|
|||
go site.loopLoadpoints(loadpointChan)
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
site.publish(keys.Interval, interval.Seconds())
|
||||
site.update(<-loadpointChan) // start immediately
|
||||
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ type API interface {
|
|||
// battery
|
||||
//
|
||||
|
||||
GetPrioritySoc() float64
|
||||
SetPrioritySoc(float64) error
|
||||
GetBufferSoc() float64
|
||||
SetBufferSoc(float64) error
|
||||
GetBufferStartSoc() float64
|
||||
SetBufferStartSoc(float64) error
|
||||
GetPrioritySoc() float64
|
||||
SetPrioritySoc(float64) error
|
||||
GetMaxGridSupplyWhileBatteryCharging() float64
|
||||
SetMaxGridSupplyWhileBatteryCharging(float64) error
|
||||
|
||||
//
|
||||
// power and energy
|
||||
|
|
|
|||
|
|
@ -243,6 +243,31 @@ func (site *Site) SetBufferStartSoc(soc float64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) GetMaxGridSupplyWhileBatteryCharging() float64 {
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
return site.maxGridSupplyWhileBatteryCharging
|
||||
}
|
||||
|
||||
func (site *Site) SetMaxGridSupplyWhileBatteryCharging(power float64) error {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
if len(site.batteryMeters) == 0 {
|
||||
return ErrBatteryNotConfigured
|
||||
}
|
||||
|
||||
site.log.DEBUG.Println("set max grid supply while battery charging:", power)
|
||||
|
||||
if site.maxGridSupplyWhileBatteryCharging != power {
|
||||
site.maxGridSupplyWhileBatteryCharging = power
|
||||
settings.SetFloat(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
|
||||
site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResidualPower returns the ResidualPower
|
||||
func (site *Site) GetResidualPower() float64 {
|
||||
site.RLock()
|
||||
|
|
@ -252,13 +277,14 @@ func (site *Site) GetResidualPower() float64 {
|
|||
|
||||
// SetResidualPower sets the ResidualPower
|
||||
func (site *Site) SetResidualPower(power float64) error {
|
||||
site.log.DEBUG.Println("set residual power:", power)
|
||||
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
site.log.DEBUG.Println("set residual power:", power)
|
||||
|
||||
if site.ResidualPower != power {
|
||||
site.ResidualPower = power
|
||||
settings.SetFloat(keys.ResidualPower, site.ResidualPower)
|
||||
site.publish(keys.ResidualPower, site.ResidualPower)
|
||||
}
|
||||
|
||||
|
|
@ -312,7 +338,10 @@ func (site *Site) GetBatteryDischargeControl() bool {
|
|||
func (site *Site) SetBatteryDischargeControl(val bool) error {
|
||||
site.log.DEBUG.Println("set battery discharge control:", val)
|
||||
|
||||
if site.GetBatteryDischargeControl() != val {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
if site.batteryDischargeControl != val {
|
||||
// reset to normal when disabling
|
||||
if mode := site.GetBatteryMode(); !val && batteryModeModified(mode) {
|
||||
if err := site.applyBatteryMode(api.BatteryNormal); err != nil {
|
||||
|
|
@ -320,9 +349,6 @@ func (site *Site) SetBatteryDischargeControl(val bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
site.batteryDischargeControl = val
|
||||
settings.SetBool(keys.BatteryDischargeControl, val)
|
||||
site.publish(keys.BatteryDischargeControl, val)
|
||||
|
|
|
|||
9
docs/encoding.md
Normal file
9
docs/encoding.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
## API encoding
|
||||
|
||||
| | socket | /api/status | mqtt | push | influx | rest |
|
||||
| ------------- | -------------------- | ----------------------------- | --------- | ------------- | ------------- | ---- |
|
||||
| source | chan<br>encode.New() | cache.State()<br>encode.New() | chan | cache.All() | chan | - |
|
||||
| time.Time | rfc3999/nil | rfc3999/nil | rfc3999 | time.Time | time.Time | ? |
|
||||
| time.Duration | s | s | s | time.Duration | time.Duration | s |
|
||||
| fmt.Stringer | string | string | string | string | string | ? |
|
||||
| struct{} | ? | recursive | recursive | n/a | ? | ? |
|
||||
2
go.mod
2
go.mod
|
|
@ -194,3 +194,5 @@ require (
|
|||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.29.9 // indirect
|
||||
)
|
||||
|
||||
replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467
|
||||
|
|
|
|||
5
go.sum
5
go.sum
|
|
@ -33,6 +33,8 @@ github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912 h1:pphS+WUa9zK0ZH/
|
|||
github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k=
|
||||
github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ=
|
||||
github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM=
|
||||
github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467 h1:JqIoHxsQSV39xaemvVRu3HOkSf5AbhQ1YEENJa2cL3Q=
|
||||
github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
|
|
@ -921,9 +923,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/hems/ocpp"
|
||||
"github.com/evcc-io/evcc/hems/semp"
|
||||
"github.com/evcc-io/evcc/server"
|
||||
)
|
||||
|
|
@ -20,8 +19,8 @@ func NewFromConfig(typ string, other map[string]interface{}, site site.API, http
|
|||
switch strings.ToLower(typ) {
|
||||
case "sma", "shm", "semp":
|
||||
return semp.New(other, site, httpd)
|
||||
case "ocpp":
|
||||
return ocpp.New(other, site)
|
||||
// case "ocpp":
|
||||
// return ocpp.New(other, site)
|
||||
default:
|
||||
return nil, errors.New("unknown hems: " + typ)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
package ocpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/hems/ocpp/profile"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/machine"
|
||||
ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6"
|
||||
ocppcore "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
||||
"github.com/lorenzodonini/ocpp-go/ws"
|
||||
)
|
||||
|
||||
// OCPP is an OCPP client
|
||||
type OCPP struct {
|
||||
log *util.Logger
|
||||
site site.API
|
||||
cp ocpp16.ChargePoint
|
||||
}
|
||||
|
||||
const retryTimeout = 5 * time.Second
|
||||
|
||||
// New generates OCPP chargepoint client
|
||||
func New(conf map[string]interface{}, site site.API) (*OCPP, error) {
|
||||
var cc struct {
|
||||
URI string
|
||||
StationID string
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(conf, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := util.NewLogger("ocpp")
|
||||
|
||||
if cc.StationID == "" {
|
||||
id, err := machine.ProtectedID("evcc-ocpp")
|
||||
if err == nil {
|
||||
cc.StationID = fmt.Sprintf("evcc-%s", strings.ToLower(id))
|
||||
} else {
|
||||
cc.StationID = fmt.Sprintf("evcc-%d", rand.Int31())
|
||||
}
|
||||
log.DEBUG.Println("station id:", cc.StationID)
|
||||
}
|
||||
|
||||
ws := ws.NewClient()
|
||||
cp := ocpp16.NewChargePoint(cc.StationID, nil, ws)
|
||||
|
||||
s := &OCPP{
|
||||
log: log,
|
||||
site: site,
|
||||
cp: cp,
|
||||
}
|
||||
|
||||
err := cp.Start(cc.URI)
|
||||
if err == nil {
|
||||
cp.SetCoreHandler(profile.NewCore(log, profile.GetDefaultConfig()))
|
||||
cp.SetSmartChargingHandler(profile.NewSmartCharging(log))
|
||||
|
||||
go s.errorHandler(ws.Errors())
|
||||
go s.errorHandler(cp.Errors())
|
||||
}
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
||||
// errorHandler logs error channel
|
||||
func (s *OCPP) errorHandler(errC <-chan error) {
|
||||
for err := range errC {
|
||||
s.log.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the OCPP chargepoint client
|
||||
func (s *OCPP) Run() {
|
||||
for {
|
||||
for id, lp := range s.site.Loadpoints() {
|
||||
connector := id + 1
|
||||
|
||||
status := ocppcore.ChargePointStatusAvailable
|
||||
if lp.GetStatus() == api.StatusC {
|
||||
status = ocppcore.ChargePointStatusCharging
|
||||
}
|
||||
|
||||
s.log.DEBUG.Printf("send: lp-%d status: %+v", connector, status)
|
||||
if _, err := s.cp.StatusNotification(connector, ocppcore.NoError, status); err != nil {
|
||||
s.log.ERROR.Printf("lp-%d: %v", connector, err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(retryTimeout)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
|
||||
)
|
||||
|
||||
// modified from https://github.com/lorenzodonini/ocpp-go/tree/master/example/1.6/cp
|
||||
|
||||
// configuration options
|
||||
const (
|
||||
AuthorizeRemoteTxRequests string = "AuthorizeRemoteTxRequests"
|
||||
ClockAlignedDataInterval string = "ClockAlignedDataInterval"
|
||||
ConnectionTimeOut string = "ConnectionTimeOut"
|
||||
ConnectorPhaseRotation string = "ConnectorPhaseRotation"
|
||||
GetConfigurationMaxKeys string = "GetConfigurationMaxKeys"
|
||||
HeartbeatInterval string = "HeartbeatInterval"
|
||||
LocalAuthorizeOffline string = "LocalAuthorizeOffline"
|
||||
LocalPreAuthorize string = "LocalPreAuthorize"
|
||||
MeterValuesAlignedData string = "MeterValuesAlignedData"
|
||||
MeterValuesSampledData string = "MeterValuesSampledData"
|
||||
MeterValueSampleInterval string = "MeterValueSampleInterval"
|
||||
NumberOfConnectors string = "NumberOfConnectors"
|
||||
ResetRetries string = "ResetRetries"
|
||||
StopTransactionOnEVSideDisconnect string = "StopTransactionOnEVSideDisconnect"
|
||||
StopTransactionOnInvalidID string = "StopTransactionOnInvalidId"
|
||||
StopTxnAlignedData string = "StopTxnAlignedData"
|
||||
StopTxnSampledData string = "StopTxnSampledData"
|
||||
SupportedFeatureProfiles string = "SupportedFeatureProfiles"
|
||||
TransactionMessageAttempts string = "TransactionMessageAttempts"
|
||||
TransactionMessageRetryInterval string = "TransactionMessageRetryInterval"
|
||||
UnlockConnectorOnEVSideDisconnect string = "UnlockConnectorOnEVSideDisconnect"
|
||||
WebSocketPingInterval string = "WebSocketPingInterval"
|
||||
LocalAuthListEnabled string = "LocalAuthListEnabled"
|
||||
LocalAuthListMaxLength string = "LocalAuthListMaxLength"
|
||||
SendLocalListMaxLength string = "SendLocalListMaxLength"
|
||||
ChargeProfileMaxStackLevel string = "ChargeProfileMaxStackLevel"
|
||||
ChargingScheduleAllowedChargingRateUnit string = "ChargingScheduleAllowedChargingRateUnit"
|
||||
ChargingScheduleMaxPeriods string = "ChargingScheduleMaxPeriods"
|
||||
MaxChargingProfilesInstalled string = "MaxChargingProfilesInstalled"
|
||||
)
|
||||
|
||||
// ConfigMap defines the active configuration settings
|
||||
type ConfigMap map[string]core.ConfigurationKey
|
||||
|
||||
// func (c ConfigMap) updateInt(key string, i int64) {
|
||||
// configKey, ok := c[key]
|
||||
// if ok {
|
||||
// configKey.Value = strconv.FormatInt(i, 10)
|
||||
// }
|
||||
// c[key] = configKey
|
||||
// }
|
||||
|
||||
// func (c ConfigMap) updateBool(key string, b bool) {
|
||||
// configKey, ok := c[key]
|
||||
// if ok {
|
||||
// configKey.Value = strconv.FormatBool(b)
|
||||
// }
|
||||
// c[key] = configKey
|
||||
// }
|
||||
|
||||
// func (c ConfigMap) getInt(key string) (int, bool) {
|
||||
// configKey, ok := c[key]
|
||||
// if !ok {
|
||||
// return 0, ok
|
||||
// }
|
||||
// result, err := strconv.ParseInt(configKey.Value, 10, 32)
|
||||
// if err != nil {
|
||||
// return 0, false
|
||||
// }
|
||||
// return int(result), true
|
||||
// }
|
||||
|
||||
// func (c ConfigMap) getBool(key string) (bool, bool) {
|
||||
// configKey, ok := c[key]
|
||||
// if !ok {
|
||||
// return false, ok
|
||||
// }
|
||||
// result, err := strconv.ParseBool(configKey.Value)
|
||||
// if err != nil {
|
||||
// return false, false
|
||||
// }
|
||||
// return result, true
|
||||
// }
|
||||
|
||||
func (c ConfigMap) set(key string, readonly bool, value string) {
|
||||
c[key] = core.ConfigurationKey{
|
||||
Key: key,
|
||||
Readonly: readonly,
|
||||
Value: &value,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDefaultConfig() ConfigMap {
|
||||
intBase := 10
|
||||
|
||||
var cfg ConfigMap = make(map[string]core.ConfigurationKey)
|
||||
|
||||
// readonly
|
||||
cfg.set(SupportedFeatureProfiles, true, core.ProfileName)
|
||||
cfg.set(AuthorizeRemoteTxRequests, true, strconv.FormatBool(false))
|
||||
cfg.set(GetConfigurationMaxKeys, true, strconv.FormatInt(50, intBase))
|
||||
cfg.set(NumberOfConnectors, true, strconv.FormatInt(1, intBase))
|
||||
cfg.set(LocalAuthListMaxLength, true, strconv.FormatInt(100, intBase))
|
||||
cfg.set(SendLocalListMaxLength, true, strconv.FormatInt(20, intBase))
|
||||
cfg.set(ChargeProfileMaxStackLevel, true, strconv.FormatInt(10, intBase))
|
||||
cfg.set(ChargingScheduleAllowedChargingRateUnit, true, "Power")
|
||||
cfg.set(ChargingScheduleMaxPeriods, true, strconv.FormatInt(5, intBase))
|
||||
cfg.set(MaxChargingProfilesInstalled, true, strconv.FormatInt(10, intBase))
|
||||
|
||||
// read/write
|
||||
cfg.set(ClockAlignedDataInterval, false, strconv.FormatInt(0, intBase))
|
||||
cfg.set(ConnectionTimeOut, false, strconv.FormatInt(60, intBase))
|
||||
cfg.set(ConnectorPhaseRotation, false, "Unknown")
|
||||
cfg.set(HeartbeatInterval, false, strconv.FormatInt(86400, intBase))
|
||||
cfg.set(LocalAuthorizeOffline, false, strconv.FormatBool(true))
|
||||
cfg.set(LocalAuthListEnabled, false, strconv.FormatBool(true))
|
||||
cfg.set(LocalPreAuthorize, false, strconv.FormatBool(false))
|
||||
cfg.set(MeterValuesAlignedData, false, string(types.MeasurandEnergyActiveExportRegister))
|
||||
cfg.set(MeterValuesSampledData, false, string(types.MeasurandEnergyActiveExportRegister))
|
||||
cfg.set(MeterValueSampleInterval, false, strconv.FormatInt(5, intBase))
|
||||
cfg.set(ResetRetries, false, strconv.FormatInt(10, intBase))
|
||||
cfg.set(StopTransactionOnEVSideDisconnect, false, strconv.FormatBool(true))
|
||||
cfg.set(StopTransactionOnInvalidID, false, strconv.FormatBool(true))
|
||||
cfg.set(StopTxnAlignedData, false, strconv.FormatBool(true))
|
||||
cfg.set(StopTxnSampledData, false, string(types.MeasurandEnergyActiveExportRegister))
|
||||
cfg.set(TransactionMessageAttempts, false, strconv.FormatInt(5, intBase))
|
||||
cfg.set(TransactionMessageRetryInterval, false, strconv.FormatInt(60, intBase))
|
||||
cfg.set(UnlockConnectorOnEVSideDisconnect, false, strconv.FormatBool(true))
|
||||
cfg.set(WebSocketPingInterval, false, strconv.FormatInt(54, intBase))
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
|
||||
)
|
||||
|
||||
type Core struct {
|
||||
log *util.Logger
|
||||
configuration ConfigMap
|
||||
}
|
||||
|
||||
func NewCore(log *util.Logger, config ConfigMap) *Core {
|
||||
return &Core{
|
||||
log: log,
|
||||
configuration: config,
|
||||
}
|
||||
}
|
||||
|
||||
// OnChangeAvailability handles the CS message
|
||||
func (s *Core) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewChangeAvailabilityConfirmation(core.AvailabilityStatusRejected), nil
|
||||
}
|
||||
|
||||
// OnUnlockConnector handles the CS message
|
||||
func (s *Core) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewUnlockConnectorConfirmation(core.UnlockStatusUnlocked), nil
|
||||
}
|
||||
|
||||
// OnRemoteStartTransaction handles the CS message
|
||||
func (s *Core) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewRemoteStartTransactionConfirmation(types.RemoteStartStopStatusRejected), nil
|
||||
}
|
||||
|
||||
// OnRemoteStopTransaction handles the CS message
|
||||
func (s *Core) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewRemoteStopTransactionConfirmation(types.RemoteStartStopStatusRejected), nil
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// OnGetConfiguration handles the CS message
|
||||
func (s *Core) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
|
||||
var resultKeys []core.ConfigurationKey
|
||||
var unknownKeys []string
|
||||
|
||||
for _, key := range request.Key {
|
||||
configKey, ok := s.configuration[key]
|
||||
if !ok {
|
||||
unknownKeys = append(unknownKeys, *configKey.Value)
|
||||
} else {
|
||||
resultKeys = append(resultKeys, configKey)
|
||||
}
|
||||
}
|
||||
|
||||
// return config for all keys
|
||||
if len(request.Key) == 0 {
|
||||
resultKeys = maps.Values(s.configuration)
|
||||
}
|
||||
|
||||
s.log.TRACE.Printf("%s: configuration for requested keys: %v", request.GetFeatureName(), request.Key)
|
||||
|
||||
conf := core.NewGetConfigurationConfirmation(resultKeys)
|
||||
conf.UnknownKey = unknownKeys
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// OnChangeConfiguration handles the CS message
|
||||
func (s *Core) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
|
||||
)
|
||||
|
||||
// OnClearCache handles the CS message
|
||||
func (s *Core) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s", request.GetFeatureName())
|
||||
return core.NewClearCacheConfirmation(core.ClearCacheStatusAccepted), nil
|
||||
}
|
||||
|
||||
// OnDataTransfer handles the CS message
|
||||
func (s *Core) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewDataTransferConfirmation(core.DataTransferStatusAccepted), nil
|
||||
}
|
||||
|
||||
// OnReset handles the CS message
|
||||
func (s *Core) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return core.NewResetConfirmation(core.ResetStatusAccepted), nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/util"
|
||||
sc "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging"
|
||||
)
|
||||
|
||||
type SmartCharging struct {
|
||||
log *util.Logger
|
||||
}
|
||||
|
||||
func NewSmartCharging(log *util.Logger) *SmartCharging {
|
||||
return &SmartCharging{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// OnSetChargingProfile handles the CS message
|
||||
func (s *SmartCharging) OnSetChargingProfile(request *sc.SetChargingProfileRequest) (confirmation *sc.SetChargingProfileConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return sc.NewSetChargingProfileConfirmation(sc.ChargingProfileStatusRejected), nil
|
||||
}
|
||||
|
||||
// OnClearChargingProfile handles the CS message
|
||||
func (s *SmartCharging) OnClearChargingProfile(request *sc.ClearChargingProfileRequest) (confirmation *sc.ClearChargingProfileConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return sc.NewClearChargingProfileConfirmation(sc.ClearChargingProfileStatusUnknown), nil
|
||||
}
|
||||
|
||||
// OnGetCompositeSchedule handles the CS message
|
||||
func (s *SmartCharging) OnGetCompositeSchedule(request *sc.GetCompositeScheduleRequest) (confirmation *sc.GetCompositeScheduleConfirmation, err error) {
|
||||
s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
|
||||
return sc.NewGetCompositeScheduleConfirmation(sc.GetCompositeScheduleStatusRejected), nil
|
||||
}
|
||||
113
i18n/de.toml
113
i18n/de.toml
|
|
@ -26,13 +26,36 @@ never = "nur mit genug PV-Überschuss"
|
|||
titleAdd = "Batterie Zähler hinzufügen"
|
||||
titleEdit = "Batterie Zähler bearbeiten"
|
||||
|
||||
[config.circuits]
|
||||
description = "Stellt sicher, dass die Summe aller Ladepunkte, die an einen Stromkreis angeschlossen sind, die konfigurierten Leistungs- und Stromgrenzen nicht überschreitet. Stromkreise können verschachtelt werden, um eine Hierarchie aufzubauen."
|
||||
title = "Lastmanagement"
|
||||
|
||||
[config.control]
|
||||
description = "Normalerweise sind die Standardwerte in Ordnung. Ändere nur etwas, wenn du weißt, was du tust."
|
||||
descriptionInterval = "Regelkreis Aktualisierungszyklus in Sekunden. Definiert, wie oft evcc Messdaten liest, die Ladeleistung anpasst und die Benutzeroberfläche aktualisiert. Kurze Intervalle (< 30s) können zu Schwingungen und unerwünschtem Verhalten führen."
|
||||
descriptionMaxGridSupply = "Nur relevant für Hybrid-Wechselrichter, die nicht in der Lage sind, die volle DC-Erzeugungsleistung über AC an das Haus auszugeben. Dieses Szenario kann zu unerwünschtem Netzbezug führen, da evcc davon ausgeht, dass die volle DC-Leistung verfügbar ist. Verwende einen Wert von mindestens 50 W, um dies zu verhindern."
|
||||
descriptionResidualPower = "Verschiebt den Betriebspunkt des Regelkreises. Wenn du eine Hausbatterie hast, wird empfohlen, einen Wert von 100 W einzustellen. Auf diese Weise erhält die Batterie eine leichte Priorität gegenüber dem Netzbezug."
|
||||
labelInterval = "Aktualisierungsintervall"
|
||||
labelMaxGridSupply = "Max. Netzbezug während des Ladens"
|
||||
labelResidualPower = "Residualleistung"
|
||||
title = "Regelverhalten"
|
||||
|
||||
[config.deviceValue]
|
||||
capacity = "Kapazität"
|
||||
chargeStatus = "Status"
|
||||
chargeStatusA = "nicht verbunden"
|
||||
chargeStatusB = "verbunden"
|
||||
chargeStatusC = "lädt"
|
||||
chargeStatusE = "keine Leistung"
|
||||
chargeStatusF = "Fehler"
|
||||
chargedEnergy = "Geladen"
|
||||
co2 = "Netz-CO₂"
|
||||
currency = "Währung"
|
||||
current = "Strom"
|
||||
enabled = "Aktiviert"
|
||||
energy = "Energie"
|
||||
feedinPrice = "Einspeisevergütung"
|
||||
gridPrice = "Netzpreis"
|
||||
odometer = "Kilometerstand"
|
||||
phaseCurrents = "Strom L1..L3"
|
||||
phasePowers = "Leistung L1..L3"
|
||||
|
|
@ -41,6 +64,11 @@ power = "Leistung"
|
|||
range = "Reichweite"
|
||||
soc = "SoC"
|
||||
socLimit = "SoC Begrenzung"
|
||||
temp = "Temperatur"
|
||||
|
||||
[config.eebus]
|
||||
description = "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten."
|
||||
title = "EEBus"
|
||||
|
||||
[config.form]
|
||||
example = "Beispiel"
|
||||
|
|
@ -48,22 +76,54 @@ optional = "optional"
|
|||
|
||||
[config.general]
|
||||
cancel = "Abbrechen"
|
||||
docsLink = "Siehe Dokumentation."
|
||||
experimental = "Experimentell"
|
||||
off = "aus"
|
||||
on = "an"
|
||||
password = "Passwort"
|
||||
remove = "Entfernen"
|
||||
save = "Speichern"
|
||||
telemetry = "Telemetrie"
|
||||
title = "Titel"
|
||||
|
||||
[config.grid]
|
||||
titleAdd = "Stromzähler (Netz) hinzufügen"
|
||||
titleEdit = "Stromzähler (Netz) bearbeiten"
|
||||
title = "Netzzähler"
|
||||
titleAdd = "Netzzähler hinzufügen"
|
||||
titleEdit = "Netzzähler bearbeiten"
|
||||
|
||||
[config.hems]
|
||||
description = "Connect evcc to another home energy management system."
|
||||
title = "HEMS"
|
||||
|
||||
[config.influx]
|
||||
description = "Schreibt Ladedaten und andere Metriken in InfluxDB. Verwende Grafana oder andere Tools, um die Daten zu visualisieren."
|
||||
descriptionToken = "Siehe die InfluxDB-Dokumentation, um zu erfahren, wie du einen erstellst. https://docs.influxdata.com/influxdb/v2/admin/"
|
||||
labelBucket = "Bucket"
|
||||
labelDatabase = "Datenbank"
|
||||
labelOrg = "Organisation"
|
||||
labelPassword = "Passwort"
|
||||
labelToken = "API-Token"
|
||||
labelUrl = "URL"
|
||||
labelUser = "Benutzer"
|
||||
title = "InfluxDB"
|
||||
v1Support = "Unterstützung für InfluxDB 1.x?"
|
||||
v2Support = "Zurück zu InfluxDB 2.x"
|
||||
|
||||
[config.main]
|
||||
addLoadpoint = "Ladepunkt hinzufügen"
|
||||
addPvBattery = "PV oder Speicher hinzufügen"
|
||||
addVehicle = "Fahrzeug hinzufügen"
|
||||
configured = "konfiguriert"
|
||||
edit = "bearbeiten"
|
||||
title = "Konfiguration"
|
||||
unconfigured = "nicht konfiguriert"
|
||||
vehicles = "Meine Fahrzeuge"
|
||||
yaml = "Konfiguration in evcc.yaml gefunden. Nicht per UI editierbar"
|
||||
|
||||
[config.messaging]
|
||||
description = "Erhalte Benachrichtigungen über Ladevorgänge und andere Ereignisse."
|
||||
title = "Benachrichtigungen"
|
||||
|
||||
[config.meter]
|
||||
cancel = "Abbrechen"
|
||||
delete = "Löschen"
|
||||
|
|
@ -72,6 +132,35 @@ template = "Hersteller"
|
|||
titleChoice = "Was möchtest du hinzufügen?"
|
||||
validateSave = "Überprüfen & speichern"
|
||||
|
||||
[config.modbusproxy]
|
||||
description = "Erlaubt mehreren Clients den Zugriff auf ein einzelnes Modbus-Gerät."
|
||||
title = "Modbus-Proxy"
|
||||
|
||||
[config.mqtt]
|
||||
authentication = "Authentifizierung"
|
||||
brokerLabel = "Server"
|
||||
description = "Verbinde evcc mit einem MQTT-Broker, um Daten mit anderen Systemen in deinem Netzwerk auszutauschen."
|
||||
descriptionClientId = "Autor der Nachrichten. Wenn leer, wird `evcc-[rand]` verwendet."
|
||||
descriptionTopic = "Leer lassen, um das Publizieren zu deaktivieren."
|
||||
labelBroker = "Broker"
|
||||
labelCheckInsecure = "Erlaube unsichere Verbindungen"
|
||||
labelClientId = "Client ID"
|
||||
labelInsecure = "Zertifikatüberprüfung"
|
||||
labelPassword = "Passwort"
|
||||
labelTopic = "Topic"
|
||||
labelUser = "Benutzer"
|
||||
publishing = "Veröffentlichen"
|
||||
title = "MQTT"
|
||||
|
||||
[config.network]
|
||||
descriptionHost = "Verwende den .local-Suffix, um mDNS zu aktivieren. Wird zur Erkennung der mobilen App und einiger OCPP-Wallboxen benötigt."
|
||||
descriptionPort = "Port für die Web-Oberfläche und API. Du musst deine Browser-URL aktualisieren, wenn du dies änderst."
|
||||
descriptionSchema = "Beeinflusst nur die URL-Generierung. Die Auswahl von HTTPS aktiviert keine Verschlüsselung."
|
||||
labelHost = "Hostname"
|
||||
labelPort = "Port"
|
||||
labelSchema = "Schema"
|
||||
title = "Netzwerk"
|
||||
|
||||
[config.options]
|
||||
|
||||
[config.options.endianness]
|
||||
|
|
@ -88,7 +177,21 @@ titleEdit = "Zähler (PV) bearbeiten"
|
|||
|
||||
[config.section]
|
||||
general = "Allgemein"
|
||||
grid = "Netzanschluss"
|
||||
integrations = "Integrationen"
|
||||
loadpoints = "Ladepunkte"
|
||||
meter = "PV & Batterie"
|
||||
system = "System"
|
||||
vehicles = "Fahrzeuge"
|
||||
|
||||
[config.sponsor]
|
||||
addToken = "Sponsortoken eingeben"
|
||||
changeToken = "Sponsortoken ändern"
|
||||
description = "Das Sponsoring-Modell hilft uns, das Projekt zu pflegen und nachhaltig neue und spannende Funktionen zu entwickeln. Als Sponsor erhältst du Zugriff auf alle Wallbox-Implementierungen."
|
||||
descriptionToken = "Du erhältst das Token von {url}. Wir bieten auch ein Testtoken für Tests an."
|
||||
error = "Das Sponsortoken ist ungültig."
|
||||
labelToken = "Sponsortoken"
|
||||
title = "Sponsorship"
|
||||
|
||||
[config.system]
|
||||
logs = "Logs"
|
||||
|
|
@ -98,6 +201,10 @@ restartRequiredMessage = "Konfiguration geändert."
|
|||
restartingDescription = "Bitte warten …"
|
||||
restartingMessage = "evcc wird neu gestartet."
|
||||
|
||||
[config.tariffs]
|
||||
description = "Hinterlege deine Stromtarife, um die Kosten zu berechnen."
|
||||
title = "Tarife"
|
||||
|
||||
[config.title]
|
||||
description = "Wird in der Hauptansicht und im Browser-Tab angezeigt."
|
||||
label = "Titel"
|
||||
|
|
@ -515,7 +622,7 @@ auto = "Automatisch"
|
|||
label = "Sprache"
|
||||
|
||||
[settings.sponsorToken]
|
||||
expires = "Dein Sponsortoken läuft {inXDays} ab. {getNewToken} und aktualisiere deine Konfigurationsdatei."
|
||||
expires = "Dein Sponsortoken läuft {inXDays} ab. {getNewToken} und aktualisiere es hier."
|
||||
getNew = "Hol dir ein Neues"
|
||||
hint = "Hinweis: Wir werden das zukünftig automatisieren."
|
||||
|
||||
|
|
|
|||
120
i18n/en.toml
120
i18n/en.toml
|
|
@ -26,14 +26,42 @@ never = "only with enough surplus"
|
|||
titleAdd = "Add Battery Meter"
|
||||
titleEdit = "Edit Battery Meter"
|
||||
|
||||
[config.circuits]
|
||||
description = "Ensures, that the sum of all loadpoints connected to a circuit does not exceed the configured power and current limits. Circuits can be nested to build a hierarchy."
|
||||
title = "Load Management"
|
||||
|
||||
[config.control]
|
||||
description = "Usually the default values are fine. Only change them if you know what you are doing."
|
||||
descriptionInterval = "Control loop update cycle in seconds. Defines how often evcc reads meter data, adjusts charging power and updates the UI. Short intervals (< 30s) can cause oszillations and unwanted behavior."
|
||||
descriptionMaxGridSupply = "Only relevant for hybrid inverters that are not able to output the full DC generation power to the home via AC. This szenario can lead to unwanted grid use, since evcc assumes the full DC power is available. Use a value of at least 50 W to prevent this."
|
||||
descriptionResidualPower = "Shifts the operation point of the control loop. If you have a home battery it's recommended to set a value of 100 W. This way the battery will get slight priority over grid use."
|
||||
labelInterval = "Update interval"
|
||||
labelMaxGridSupply = "Max. grid supply while charging"
|
||||
labelResidualPower = "Residual power"
|
||||
title = "Control behavior"
|
||||
|
||||
[config.deviceValue]
|
||||
broker = "Broker"
|
||||
bucket = "Bucket"
|
||||
capacity = "Capacity"
|
||||
chargeStatus = "Status"
|
||||
chargeStatusA = "not connected"
|
||||
chargeStatusB = "connected"
|
||||
chargeStatusC = "charging"
|
||||
chargeStatusE = "no power"
|
||||
chargeStatusF = "error"
|
||||
chargedEnergy = "Charged"
|
||||
co2 = "Grid CO₂"
|
||||
configured = "Configured"
|
||||
currency = "Currency"
|
||||
current = "Current"
|
||||
enabled = "Enabled"
|
||||
energy = "Energy"
|
||||
feedinPrice = "Feed-in price"
|
||||
gridPrice = "Grid price"
|
||||
no = "no"
|
||||
odometer = "Odometer"
|
||||
org = "Organization"
|
||||
phaseCurrents = "Current L1..L3"
|
||||
phasePowers = "Power L1..L3"
|
||||
phaseVoltages = "Voltage L1..L3"
|
||||
|
|
@ -41,6 +69,14 @@ power = "Power"
|
|||
range = "Range"
|
||||
soc = "SoC"
|
||||
socLimit = "Limit"
|
||||
temp = "Temperature"
|
||||
topic = "Topic"
|
||||
url = "URL"
|
||||
yes = "yes"
|
||||
|
||||
[config.eebus]
|
||||
description = "Configuration that enables evcc to communicate with other EEBus devices."
|
||||
title = "EEBus"
|
||||
|
||||
[config.form]
|
||||
example = "Example"
|
||||
|
|
@ -48,22 +84,54 @@ optional = "optional"
|
|||
|
||||
[config.general]
|
||||
cancel = "Cancel"
|
||||
docsLink = "See documentation."
|
||||
experimental = "Experimental"
|
||||
off = "off"
|
||||
on = "on"
|
||||
password = "Password"
|
||||
remove = "Remove"
|
||||
save = "Save"
|
||||
telemetry = "Telemetry"
|
||||
title = "Title"
|
||||
|
||||
[config.grid]
|
||||
title = "Grid meter"
|
||||
titleAdd = "Add Grid Meter"
|
||||
titleEdit = "Edit Grid Meter"
|
||||
|
||||
[config.hems]
|
||||
description = "Connect evcc to another home energy management system."
|
||||
title = "HEMS"
|
||||
|
||||
[config.influx]
|
||||
description = "Writes charging data and other metrics to InfluxDB. Use Grafana or other tools to visualize the data."
|
||||
descriptionToken = "Check the InfluxDB documentation to learn how to create one. https://docs.influxdata.com/influxdb/v2/admin/"
|
||||
labelBucket = "Bucket"
|
||||
labelDatabase = "Database"
|
||||
labelOrg = "Organization"
|
||||
labelPassword = "Password"
|
||||
labelToken = "API Token"
|
||||
labelUrl = "URL"
|
||||
labelUser = "Username"
|
||||
title = "InfluxDB"
|
||||
v1Support = "Need support for InfluxDB 1.x?"
|
||||
v2Support = "Back to InfluxDB 2.x"
|
||||
|
||||
[config.main]
|
||||
addLoadpoint = "Add charge point"
|
||||
addPvBattery = "Add solar or battery"
|
||||
addVehicle = "Add vehicle"
|
||||
configured = "configured"
|
||||
edit = "edit"
|
||||
title = "Configuration"
|
||||
unconfigured = "not configured"
|
||||
vehicles = "My Vehicles"
|
||||
yaml = "Configured in evcc.yaml. Not editable in the UI."
|
||||
|
||||
[config.messaging]
|
||||
description = "Recieve messages about your charging sessions."
|
||||
title = "Notifications"
|
||||
|
||||
[config.meter]
|
||||
cancel = "Cancel"
|
||||
delete = "Delete"
|
||||
|
|
@ -72,6 +140,34 @@ template = "Manufacturer"
|
|||
titleChoice = "What Do You Want To Add?"
|
||||
validateSave = "Validate & save"
|
||||
|
||||
[config.modbusproxy]
|
||||
description = "Allow multiple clients to access a single Modbus device."
|
||||
title = "Modbus Proxy"
|
||||
|
||||
[config.mqtt]
|
||||
authentication = "Authentication"
|
||||
description = "Connect to an MQTT broker to exchange data with other systems on your network."
|
||||
descriptionClientId = "Author of the messages. If empty `evcc-[rand]` is used."
|
||||
descriptionTopic = "Leave empty to disable publishing."
|
||||
labelBroker = "Broker"
|
||||
labelCheckInsecure = "Allow self-signed certificates"
|
||||
labelClientId = "Client ID"
|
||||
labelInsecure = "Certificate validation"
|
||||
labelPassword = "Password"
|
||||
labelTopic = "Topic"
|
||||
labelUser = "Username"
|
||||
publishing = "Publishing"
|
||||
title = "MQTT"
|
||||
|
||||
[config.network]
|
||||
descriptionHost = "Use .local suffix to enable mDNS. Relevant for discovery of the mobile app and some OCPP chargers."
|
||||
descriptionPort = "Port for the web interface and API. You'll need to update your browser URL if you change this."
|
||||
descriptionSchema = "Only affects how URLs are generated. Selecting HTTPS will not enable encryption."
|
||||
labelHost = "Hostname"
|
||||
labelPort = "Port"
|
||||
labelSchema = "Schema"
|
||||
title = "Network"
|
||||
|
||||
[config.options]
|
||||
|
||||
[config.options.endianness]
|
||||
|
|
@ -88,7 +184,21 @@ titleEdit = "Edit Solar Meter"
|
|||
|
||||
[config.section]
|
||||
general = "General"
|
||||
grid = "Grid"
|
||||
integrations = "Integrations"
|
||||
loadpoints = "Charging points"
|
||||
meter = "Solar & Battery"
|
||||
system = "System"
|
||||
vehicles = "Vehicles"
|
||||
|
||||
[config.sponsor]
|
||||
addToken = "Enter sponsor token"
|
||||
changeToken = "Change sponsor token"
|
||||
description = "The sponsoring model helps us to maintain the project and sustainably build new and exciting features. As a sponsor you get access to all charger implementations."
|
||||
descriptionToken = "You get the token from {url}. We also offer a trial token for testing."
|
||||
error = "The sponsor token is not valid."
|
||||
labelToken = "Sponsor token"
|
||||
title = "Sponsorship"
|
||||
|
||||
[config.system]
|
||||
logs = "Logs"
|
||||
|
|
@ -98,6 +208,10 @@ restartRequiredMessage = "Configuration changed."
|
|||
restartingDescription = "Please wait…"
|
||||
restartingMessage = "Restarting evcc."
|
||||
|
||||
[config.tariffs]
|
||||
description = "Define your energy tariffs to calculate the costs of your charging sessions."
|
||||
title = "Tariffs"
|
||||
|
||||
[config.title]
|
||||
description = "Displayed on main screen and browser tab."
|
||||
label = "Title"
|
||||
|
|
@ -428,7 +542,9 @@ modalTitle = "Notifications"
|
|||
|
||||
[offline]
|
||||
message = "Not connected to a server."
|
||||
reload = "Reload?"
|
||||
restart = "Restart"
|
||||
restartNeeded = "Required to apply changes."
|
||||
restarting = "Server will be back in a moment."
|
||||
|
||||
[passwordModal]
|
||||
description = "Set a password to protect the configuration settings. Using the main screen is still possible without login."
|
||||
|
|
@ -514,7 +630,7 @@ auto = "Automatic"
|
|||
label = "Language"
|
||||
|
||||
[settings.sponsorToken]
|
||||
expires = "You sponsor token expires {inXDays}. {getNewToken} and update your configuration file."
|
||||
expires = "You sponsor token expires {inXDays}. {getNewToken} and update it here."
|
||||
getNew = "Grab a fresh one"
|
||||
hint = "Note: We will automate this in the future."
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue