HEMS: published detailed config including max power (#25869)

This commit is contained in:
andig 2025-12-11 09:48:39 +01:00 • committed by GitHub
parent f365e2099a
commit c25bf05086
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 364 additions and 177 deletions

View file

@ -105,7 +105,8 @@ export default {
},
sponsorStatus() {
const sponsor = store.state?.sponsor || {};
const { name, expiresSoon } = sponsor;
const name = sponsor.status.name;
const expiresSoon = sponsor.status.expiresSoon;
let textClass = "";
let badgeClass = "";
let title = name;

View file

@ -110,13 +110,13 @@ export default {
return store?.state?.sponsor;
},
token() {
return this.sponsor?.token;
return this.sponsor?.status.token;
},
fromYaml() {
return this.sponsor?.fromYaml;
},
name() {
return this.sponsor?.name || "";
return this.sponsor?.status.name || "";
},
showTokenForm() {
return this.editMode || !this.token;

View file

@ -6,7 +6,7 @@
data-testid="telemetry-modal"
>
<p>{{ $t("config.telemetry.description") }}</p>
<TelemetrySettings :sponsorActive="sponsor && !!sponsor.name" :telemetry="telemetry" />
<TelemetrySettings :sponsorActive="sponsorActive" :telemetry="telemetry" />
</GenericModal>
</template>
@ -23,5 +23,10 @@ export default defineComponent({
sponsor: { type: Object as PropType<Sponsor> },
telemetry: { type: Boolean },
},
computed: {
sponsorActive(): boolean {
return !!this.sponsor?.status.name;
},
},
});
</script>

View file

@ -191,7 +191,7 @@
<div v-else class="my-4">
<LiveCommunity />
<TelemetrySettings
:sponsorActive="!!sponsor?.name"
:sponsorActive="sponsorActive"
:telemetry="telemetry"
/>
</div>
@ -241,6 +241,9 @@ export default defineComponent({
};
},
computed: {
sponsorActive(): boolean {
return !!this.sponsor?.status.name;
},
tariffLink() {
return `${docsPrefix()}/docs/reference/configuration/tariffs`;
},

View file

@ -46,23 +46,25 @@ NoSponsor.args = {};
export const Trial = Template.bind({});
Trial.args = {
name: "trial",
status: { name: "trial" },
};
export const IndividualSponsor = Template.bind({});
IndividualSponsor.args = {
name: "naltatis",
status: { name: "naltatis" },
};
export const VictronDevice = Template.bind({});
VictronDevice.args = {
name: "victron",
status: { name: "victron" },
};
// Add an extra story showing the expiring state
export const ExpiringSponsor = Template.bind({});
ExpiringSponsor.args = {
name: "naltatis",
expiresSoon: true,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
status: {
name: "naltatis",
expiresSoon: true,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
},
};

View file

@ -67,7 +67,8 @@ import "@h2d2/shopicons/es/regular/heart";
import "@h2d2/shopicons/es/regular/stars";
import "@h2d2/shopicons/es/regular/clock";
import { docsPrefix } from "@/i18n";
import { defineComponent } from "vue";
import { defineComponent, type PropType } from "vue";
import type { SponsorStatus } from "@/types/evcc";
export const TRIAL = "trial";
export const VICTRON_DEVICE = "victron";
@ -75,13 +76,13 @@ export const VICTRON_DEVICE = "victron";
export default defineComponent({
name: "Sponsor",
props: {
name: String,
expiresAt: String,
expiresSoon: Boolean,
status: Object as PropType<SponsorStatus>,
fromYaml: Boolean,
token: String,
},
computed: {
name() {
return this.status?.name;
},
isTrial() {
return this.name === TRIAL;
},

View file

@ -26,8 +26,10 @@ const Template: StoryFn<typeof SponsorTokenExpires> = (args) => ({
// Create stories for each variant
export const SomeDay = Template.bind({});
SomeDay.args = {
expiresSoon: true,
expiresAt: new Date(Date.now() + 22 * 20 * 60 * 1000).toISOString(),
status: {
expiresSoon: true,
expiresAt: new Date(Date.now() + 22 * 20 * 60 * 1000).toISOString(),
},
};
export const Empty = Template.bind({});

View file

@ -1,15 +1,28 @@
<template>
<div v-if="expiresSoon" class="alert alert-warning my-4" role="alert">
<i18n-t tag="div" keypath="settings.sponsorToken.expires" scope="global">
<template #inXDays>
{{ inXDays }}
</template>
<template #getNewToken>
<a href="https://sponsor.evcc.io" target="_blank" class="text-danger">
{{ $t("settings.sponsorToken.getNew") }}
</a>
</template>
</i18n-t>
<div>
<i18n-t tag="span" keypath="settings.sponsorToken.expires" scope="global">
<template #inXDays>
{{ inXDays }}
</template>
</i18n-t>
{{ " " }}
<i18n-t
tag="span"
:keypath="
fromYaml
? 'settings.sponsorToken.expiresUpdateYaml'
: 'settings.sponsorToken.expiresUpdateUi'
"
scope="global"
>
<template #getNewToken>
<a href="https://sponsor.evcc.io" target="_blank" class="text-danger">
{{ $t("settings.sponsorToken.getNewToken") }}
</a>
</template>
</i18n-t>
</div>
<em v-if="!isTrial" class="d-block mt-2">
{{ $t("settings.sponsorToken.hint") }}
@ -20,18 +33,26 @@
<script lang="ts">
import formatter from "@/mixins/formatter";
import { TRIAL } from "./Sponsor.vue";
import { defineComponent } from "vue";
import { defineComponent, type PropType } from "vue";
import type { SponsorStatus } from "@/types/evcc";
export default defineComponent({
name: "SponsorTokenExpires",
mixins: [formatter],
props: {
expiresSoon: Boolean,
expiresAt: String,
name: String,
status: Object as PropType<SponsorStatus>,
fromYaml: Boolean,
},
computed: {
expiresSoon() {
return this.status?.expiresSoon;
},
expiresAt() {
return this.status?.expiresAt;
},
name() {
return this.status?.name;
},
inXDays() {
return this.expiresAt
? this.fmtTimeAgo(new Date(this.expiresAt).getTime() - new Date().getTime())

View file

@ -55,9 +55,11 @@ PendingOAuthStatus.args = {
export const TokenExpires = Template.bind({});
TokenExpires.args = {
sponsor: {
name: "Sponsor",
expiresAt: new Date().toISOString(),
expiresSoon: true,
status: {
name: "Sponsor",
expiresAt: new Date().toISOString(),
expiresSoon: true,
},
fromYaml: false,
},
};

View file

@ -175,9 +175,11 @@ export default defineComponent({
authorizationRequired() {
return this.providers.length > 0;
},
sponsorExpires(): boolean {
return !!this.sponsor?.status.expiresSoon;
},
showConfigBadge() {
const userConfigError = isUserConfigError(this.fatal);
return this.sponsor.expiresSoon || userConfigError;
return this.sponsorExpires || isUserConfigError(this.fatal);
},
showRootBadge() {
return this.authorizationRequired || this.showConfigBadge;

View file

@ -34,7 +34,17 @@ export interface InfluxConfig {
}
export interface HemsConfig {
type: any;
type: string;
}
export interface HemsStatus {
maxPower: number;
}
export interface Hems {
status?: HemsStatus;
config: HemsConfig;
fromYaml: boolean;
}
export interface ShmConfig {
@ -69,7 +79,7 @@ export interface State {
tariffSolar?: number;
mqtt?: MqttConfig;
influx?: InfluxConfig;
hems?: HemsConfig;
hems?: Hems;
shm?: ShmConfig;
sponsor?: Sponsor;
eebus?: any;
@ -345,11 +355,15 @@ export type SessionInfoKey =
| "price"
| "co2";
export interface Sponsor {
name: string;
expiresAt: string;
expiresSoon: boolean;
export interface SponsorStatus {
name?: string;
expiresAt?: string;
expiresSoon?: boolean;
token?: string;
}
export interface Sponsor {
status: SponsorStatus;
fromYaml: boolean;
}

View file

@ -288,7 +288,7 @@
</DeviceCard>
<DeviceCard
:title="$t('config.hems.title')"
editable
:editable="!hems?.fromYaml"
:error="hasClassError('hems')"
data-testid="hems"
@edit="openModal('hemsModal')"
@ -636,8 +636,11 @@ export default defineComponent({
shmTags() {
return { configured: { value: true } };
},
hems() {
return store.state?.hems;
},
hemsTags() {
const { type } = store.state?.hems || {};
const type = this.hems?.config?.type;
if (!type) {
return { configured: { value: false } };
}
@ -656,13 +659,12 @@ export default defineComponent({
return result;
},
isSponsor() {
const { name } = store.state?.sponsor || {};
return !!name;
},
sponsor() {
return store.state?.sponsor;
},
isSponsor(): boolean {
return !!this.sponsor?.status.name;
},
telemetry() {
// @ts-expect-error: telemetry property exists but not in TypeScript definitions
return store.state?.telemetry === true;

View file

@ -12,8 +12,10 @@ import (
"syscall"
"time"
"github.com/evcc-io/evcc/api/globalconfig"
"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/core/keys"
hemsapi "github.com/evcc-io/evcc/hems/hems"
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server"
"github.com/evcc-io/evcc/server/db"
@ -280,8 +282,12 @@ func runRoot(cmd *cobra.Command, args []string) {
}
// start HEMS server
var hems hemsapi.API
if err == nil {
err = wrapErrorWithClass(ClassHEMS, configureHEMS(&conf.HEMS, site))
hems, err = configureHEMS(&conf.HEMS, site)
if err != nil {
err = wrapErrorWithClass(ClassHEMS, err)
}
}
// setup MCP
@ -303,7 +309,6 @@ func runRoot(cmd *cobra.Command, args []string) {
// publish initial settings
valueChan <- util.Param{Key: keys.EEBus, Val: conf.EEBus.Configured()}
valueChan <- util.Param{Key: keys.Hems, Val: conf.HEMS}
valueChan <- util.Param{Key: keys.Shm, Val: conf.SHM}
valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
@ -311,7 +316,23 @@ func runRoot(cmd *cobra.Command, args []string) {
valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy}
valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
valueChan <- util.Param{Key: keys.Sponsor, Val: sponsor.Status()}
valueChan <- util.Param{Key: keys.Sponsor, Val: struct {
Status sponsor.Status `json:"status"`
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.GetStatus(),
FromYaml: fromYaml.sponsor,
}}
valueChan <- util.Param{Key: keys.Hems, Val: struct {
Config globalconfig.Hems `json:"config"`
Status *hemsapi.Status `json:"status,omitempty"`
FromYaml bool `json:"fromYaml"`
}{
Config: conf.HEMS,
Status: hemsapi.GetStatus(hems),
FromYaml: fromYaml.hems,
}}
// publish system infos
valueChan <- util.Param{Key: keys.Version, Val: util.FormattedVersion()}
@ -347,7 +368,9 @@ func runRoot(cmd *cobra.Command, args []string) {
valueChan <- util.Param{Key: keys.DemoMode, Val: true}
}
httpd.RegisterSystemHandler(site, valueChan, cache, authObject, func() {
httpd.RegisterSystemHandler(site, func(k string, v any) {
valueChan <- util.Param{Key: k, Val: v}
}, cache, authObject, func() {
log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.")
err = errors.New("restart required") // https://gokrazy.org/development/process-interface/
once.Do(func() { close(stopC) }) // signal loop to end

View file

@ -26,6 +26,7 @@ import (
"github.com/evcc-io/evcc/core/session"
coresettings "github.com/evcc-io/evcc/core/settings"
"github.com/evcc-io/evcc/hems"
hemsapi "github.com/evcc-io/evcc/hems/hems"
"github.com/evcc-io/evcc/hems/shm"
"github.com/evcc-io/evcc/meter"
"github.com/evcc-io/evcc/plugin/golang"
@ -79,6 +80,11 @@ var conf = globalconfig.All{
},
}
var fromYaml struct {
sponsor bool
hems bool
}
var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
func nameValid(name string) error {
@ -492,11 +498,10 @@ func configureSponsorship(token string) (err error) {
if token, err = settings.String(keys.SponsorToken); err != nil {
return err
}
sponsor.SetFromYaml(false) // from database
} else if token != "" {
fromYaml.sponsor = true
}
// TODO migrate settings
return sponsor.ConfigureSponsorship(token)
}
@ -711,32 +716,36 @@ func configureGo(conf []globalconfig.Go) error {
}
// setup HEMS
func configureHEMS(conf *globalconfig.Hems, site *core.Site) error {
// migrate settings
if settings.Exists(keys.Hems) {
*conf = globalconfig.Hems{}
if err := settings.Yaml(keys.Hems, new(map[string]any), &conf); err != nil {
return err
func configureHEMS(conf *globalconfig.Hems, site *core.Site) (hemsapi.API, error) {
// use yaml if configured
if conf.Type == "" {
if settings.Exists(keys.Hems) {
*conf = globalconfig.Hems{}
if err := settings.Yaml(keys.Hems, new(map[string]any), &conf); err != nil {
return nil, err
}
}
} else {
fromYaml.hems = true
}
if conf.Type == "" {
return nil
return nil, nil
}
props, err := customDevice(conf.Other)
if err != nil {
return fmt.Errorf("cannot decode custom hems '%s': %w", conf.Type, err)
return nil, fmt.Errorf("cannot decode custom hems '%s': %w", conf.Type, err)
}
hems, err := hems.NewFromConfig(context.TODO(), conf.Type, props, site)
if err != nil {
return fmt.Errorf("failed configuring hems: %w", err)
return nil, fmt.Errorf("failed configuring hems: %w", err)
}
go hems.Run()
return nil
return hems, nil
}
// networkSettings reads/migrates network settings

View file

@ -73,7 +73,7 @@ var _ site.API = (*Site)(nil)
// Site is the main configuration container. A site can host multiple loadpoints.
type Site struct {
uiChan chan<- util.Param // client push messages
valueChan chan<- util.Param // client push messages
lpUpdateChan chan *Loadpoint
*Health
@ -485,11 +485,16 @@ func (site *Site) DumpConfig() {
// publish sends values to UI and databases
func (site *Site) publish(key string, val any) {
// test helper
if site.uiChan == nil {
if site.valueChan == nil {
return
}
site.uiChan <- util.Param{Key: key, Val: val}
site.valueChan <- util.Param{Key: key, Val: val}
}
// publish sends values to UI and databases
func (site *Site) Publish(key string, val any) {
site.publish(key, val)
}
func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) []measurement {
@ -1043,7 +1048,7 @@ func (site *Site) prepare() {
}
// Prepare attaches communication channels to site and loadpoints
func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
func (site *Site) Prepare(valueChan chan<- util.Param, pushChan chan<- push.Event) {
// https://github.com/evcc-io/evcc/issues/11191 prevent deadlock
// https://github.com/evcc-io/evcc/pull/11675 maintain message order
@ -1051,12 +1056,12 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
ch := chanx.NewUnboundedChan[util.Param](context.Background(), 2)
// use ch.In for writing
site.uiChan = ch.In
site.valueChan = ch.In
// use ch.Out for reading
go func() {
for p := range ch.Out {
uiChan <- p
valueChan <- p
}
}()
@ -1074,7 +1079,7 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
select {
case param := <-lpUIChan:
param.Loadpoint = &id
site.uiChan <- param
site.valueChan <- param
case ev := <-lpPushChan:
ev.Loadpoint = &id
pushChan <- ev

View file

@ -5,8 +5,15 @@ import (
"github.com/evcc-io/evcc/core/loadpoint"
)
// publisher gives access to the site's publish function
type Publisher interface {
Publish(key string, val any)
}
// API is the external site API
type API interface {
Publisher
Healthy() bool
Loadpoints() []loadpoint.API
Vehicles() Vehicles

View file

@ -7,16 +7,12 @@ import (
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/hems/eebus"
"github.com/evcc-io/evcc/hems/hems"
"github.com/evcc-io/evcc/hems/relay"
)
// HEMS describes the HEMS system interface
type HEMS interface {
Run()
}
// NewFromConfig creates new HEMS from config
func NewFromConfig(ctx context.Context, typ string, other map[string]any, site site.API) (HEMS, error) {
func NewFromConfig(ctx context.Context, typ string, other map[string]any, site site.API) (hems.API, error) {
switch strings.ToLower(typ) {
case "sma", "shm", "semp":
return nil, errors.New("breaking change: Sunny Home Manager integration is always on. See https://github.com/evcc-io/evcc/releases and https://docs.evcc.io/en/docs/integrations/sma-sunny-home-manager")

View file

@ -170,6 +170,10 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b
return c, nil
}
func (c *EEBus) ConsumptionLimit() float64 {
return c.consumptionLimit.Value
}
func (c *EEBus) Run() {
for range time.Tick(c.interval) {
if err := c.run(); err != nil {

21
hems/hems/api.go Normal file
View file

@ -0,0 +1,21 @@
package hems
// API describes the HEMS system interface
type API interface {
Run()
ConsumptionLimit() float64
}
type Status struct {
ConsumptionLimit float64 `json:"consumptionLimit"`
// FeedinLimit float64 `json:"feedinLimit,omitempty"`
}
func GetStatus(api API) *Status {
if api == nil {
return nil
}
return &Status{
ConsumptionLimit: api.ConsumptionLimit(),
}
}

View file

@ -85,6 +85,10 @@ func NewRelay(root api.Circuit, limit func() (bool, error), passthrough func(boo
return c, nil
}
func (c *Relay) ConsumptionLimit() float64 {
return c.maxPower
}
func (c *Relay) Run() {
for range time.Tick(c.interval) {
if err := c.run(); err != nil {

View file

@ -965,8 +965,10 @@
"label": "Език"
},
"sponsorToken": {
"expires": "Вашият спонсорски токен изтича след {inXDays}. {getNewToken} и го актуализирайте тук.",
"getNew": "Вземете нов",
"expires": "Вашият спонсорски токен изтича след {inXDays}.",
"expiresUpdateUi": "{getNewToken} и го актуализирайте тук.",
"expiresUpdateYaml": "{getNewToken} и го актуализирайте във вашия evcc.yaml.",
"getNewToken": "Вземете нов",
"hint": "Забележка: В бъдеще ще автоматизираме това."
},
"telemetry": {

View file

@ -279,8 +279,10 @@
"label": "Idioma"
},
"sponsorToken": {
"expires": "El teu token de patrocinador expira en {inXDays}. {getNewToken} i actualitza el teu fitxer de configuració.",
"getNew": "Agafa'n un de nou",
"expires": "El teu token de patrocinador expira en {inXDays}.",
"expiresUpdateUi": "{getNewToken} i actualitza el teu fitxer de configuració.",
"expiresUpdateYaml": "{getNewToken} i actualitza'l al teu evcc.yaml.",
"getNewToken": "Agafa'n un de nou",
"hint": "Nota: automatitzarém això en el futur."
},
"telemetry": {

View file

@ -1158,8 +1158,10 @@
"show": "Zobrazit {title}"
},
"sponsorToken": {
"expires": "Váš sponzorský token vyprší za {inXDays}. {getNewToken} a aktualizujte ho zde.",
"getNew": "Vezmi čerstvý",
"expires": "Váš sponzorský token vyprší za {inXDays}.",
"expiresUpdateUi": "{getNewToken} a aktualizujte ho zde.",
"expiresUpdateYaml": "{getNewToken} a aktualizujte ho ve vašem evcc.yaml.",
"getNewToken": "Vezmi čerstvý",
"hint": "Poznámka: Toto v budoucnu zautomatizujeme."
},
"telemetry": {

View file

@ -1163,8 +1163,10 @@
"show": "Vis {title}"
},
"sponsorToken": {
"expires": "Din sponsortoken udløber {inXDays}. {getNewToken} og opdater det her.",
"getNew": "Hent en ny",
"expires": "Din sponsortoken udløber {inXDays}.",
"expiresUpdateUi": "{getNewToken} og opdater det her.",
"expiresUpdateYaml": "{getNewToken} og opdater det i din evcc.yaml.",
"getNewToken": "Hent en ny",
"hint": "Note: Vi automatiserer dette i fremtiden."
},
"telemetry": {

View file

@ -1172,8 +1172,10 @@
"show": "{title} anzeigen"
},
"sponsorToken": {
"expires": "Dein Sponsortoken läuft {inXDays} ab. {getNewToken} und aktualisiere es hier.",
"getNew": "Hol dir ein Neues",
"expires": "Dein Sponsortoken läuft {inXDays} ab.",
"expiresUpdateUi": "{getNewToken} und aktualisiere es hier.",
"expiresUpdateYaml": "{getNewToken} und aktualisiere es in deiner evcc.yaml.",
"getNewToken": "Hol dir ein Neues",
"hint": "Hinweis: Wir werden das zukünftig automatisieren."
},
"telemetry": {

View file

@ -1142,8 +1142,10 @@
"show": "Εμφάνιση {title}"
},
"sponsorToken": {
"expires": "Το sponsor token λήγει {inXDays}. {getNewToken} και κάντε εδώ μία ενημέρωση.",
"getNew": "Πάρτε ένα φρέσκο",
"expires": "Το sponsor token λήγει {inXDays}.",
"expiresUpdateUi": "{getNewToken} και κάντε εδώ μία ενημέρωση.",
"expiresUpdateYaml": "{getNewToken} και ενημερώστε το στο evcc.yaml σας.",
"getNewToken": "Πάρτε ένα φρέσκο",
"hint": "Σημείωση: Θα το αυτοματοποιήσουμε στο μέλλον."
},
"telemetry": {

View file

@ -1172,8 +1172,10 @@
"show": "Show {title}"
},
"sponsorToken": {
"expires": "You sponsor token expires {inXDays}. {getNewToken} and update it here.",
"getNew": "Grab a fresh one",
"expires": "You sponsor token expires {inXDays}.",
"expiresUpdateUi": "{getNewToken} and update it here.",
"expiresUpdateYaml": "{getNewToken} and update it in your evcc.yaml.",
"getNewToken": "Grab a fresh one",
"hint": "Note: We will automate this in the future."
},
"telemetry": {

View file

@ -728,8 +728,10 @@
"label": "Idioma"
},
"sponsorToken": {
"expires": "Tu token de patrocinador expira en {inXDays}. {getNewToken} y puedes renovarlo aquí.",
"getNew": "Coge uno nuevo",
"expires": "Tu token de patrocinador expira en {inXDays}.",
"expiresUpdateUi": "{getNewToken} y puedes renovarlo aquí.",
"expiresUpdateYaml": "{getNewToken} y actualízalo en tu evcc.yaml.",
"getNewToken": "Coge uno nuevo",
"hint": "Nota: automatizaremos esto en el futuro."
},
"telemetry": {

View file

@ -1162,8 +1162,10 @@
"show": "Näytä {title}"
},
"sponsorToken": {
"expires": "Sponsorointitunnuksesi vanhenee {inXDays}. {getNewToken} ja päivitä se tänne.",
"getNew": "Ota uusi",
"expires": "Sponsorointitunnuksesi vanhenee {inXDays}.",
"expiresUpdateUi": "{getNewToken} ja päivitä se tänne.",
"expiresUpdateYaml": "{getNewToken} ja päivitä se evcc.yaml-tiedostossa.",
"getNewToken": "Ota uusi",
"hint": "Huomaa: automatisoimme tämän tulevaisuudessa."
},
"telemetry": {

View file

@ -1162,8 +1162,10 @@
"show": "Afficher {title}"
},
"sponsorToken": {
"expires": "Votre jeton de sponsor expire dans {inXDays}. {getNewToken} et mettez-le à jour ici.",
"getNew": "Obtenez-en un nouveau",
"expires": "Votre jeton de sponsor expire dans {inXDays}.",
"expiresUpdateUi": "{getNewToken} et mettez-le à jour ici.",
"expiresUpdateYaml": "{getNewToken} et mettez-le à jour dans votre evcc.yaml.",
"getNewToken": "Obtenez-en un nouveau",
"hint": "Remarque : nous allons automatiser cela à l'avenir."
},
"telemetry": {

View file

@ -1157,8 +1157,10 @@
"show": "Prikaži {title}"
},
"sponsorToken": {
"expires": "Vaš sponzorski token isteče {inXDays}. {getNewToken} i obnovi ga ovdje.",
"getNew": "Preuzmi novi",
"expires": "Vaš sponzorski token isteče {inXDays}.",
"expiresUpdateUi": "{getNewToken} i obnovi ga ovdje.",
"expiresUpdateYaml": "{getNewToken} i obnovi ga u svojoj evcc.yaml.",
"getNewToken": "Preuzmi novi",
"hint": "Napomena: To ćemo automatizirati u budućnosti."
},
"telemetry": {

View file

@ -1129,8 +1129,10 @@
"show": "Mutasd a {title}-t"
},
"sponsorToken": {
"expires": "A szponzor token-ed le fog járni {inXDays}. {getNewToken} és frissítsd itt.",
"getNew": "Kérj egy újat",
"expires": "A szponzor token-ed le fog járni {inXDays}.",
"expiresUpdateUi": "{getNewToken} és frissítsd itt.",
"expiresUpdateYaml": "{getNewToken} és frissítsd az evcc.yaml fájlodban.",
"getNewToken": "Kérj egy újat",
"hint": "Megjegyzés: Ezt a jövőben automatizálni fogjuk."
},
"telemetry": {

View file

@ -927,8 +927,10 @@
"label": "Lingua"
},
"sponsorToken": {
"expires": "Il tuo token sponsor scadrà in {inXDays}. {getNewToken} e inseriscilo qua.",
"getNew": "Prendine uno nuovo",
"expires": "Il tuo token sponsor scadrà in {inXDays}.",
"expiresUpdateUi": "{getNewToken} e inseriscilo qua.",
"expiresUpdateYaml": "{getNewToken} e inseriscilo nel tuo evcc.yaml.",
"getNewToken": "Prendine uno nuovo",
"hint": "Nota: questa funzione verrà automatizzata."
},
"telemetry": {

View file

@ -1161,8 +1161,10 @@
"show": "{title} uweisen"
},
"sponsorToken": {
"expires": "Däi Sponsor Token leeft aus an {inXDays}. {getNewToken} an update et hei.",
"getNew": "Huel dir een neit",
"expires": "Däi Sponsor Token leeft aus an {inXDays}.",
"expiresUpdateUi": "{getNewToken} an update et hei.",
"expiresUpdateYaml": "{getNewToken} an update et an dengem evcc.yaml.",
"getNewToken": "Huel dir een neit",
"hint": "N.b.: Mir wäerten dëst an Zukunft automatiséieren."
},
"telemetry": {

View file

@ -1163,8 +1163,10 @@
"show": "Rodyti {title}"
},
"sponsorToken": {
"expires": "Jūsų rėmėjo žetonas baigsis {inXDays}. {getNewToken} ir išsaugokite jį čia.",
"getNew": "Gaukite naują",
"expires": "Jūsų rėmėjo žetonas baigsis {inXDays}.",
"expiresUpdateUi": "{getNewToken} ir išsaugokite jį čia.",
"expiresUpdateYaml": "{getNewToken} ir išsaugokite jį savo evcc.yaml faile.",
"getNewToken": "Gaukite naują",
"hint": "Pastaba: ateityje šis procesas bus automatizuotas."
},
"telemetry": {

View file

@ -1161,8 +1161,10 @@
"show": "{title} tonen"
},
"sponsorToken": {
"expires": "Je sponsortoken vervalt {inXDays}. {getNewToken} en update het hier.",
"getNew": "Vraag een nieuwe aan",
"expires": "Je sponsortoken vervalt {inXDays}.",
"expiresUpdateUi": "{getNewToken} en update het hier.",
"expiresUpdateYaml": "{getNewToken} en update het in je evcc.yaml.",
"getNewToken": "Vraag een nieuwe aan",
"hint": "Notabene: We zullen dit in de toekomst automatiseren."
},
"telemetry": {

View file

@ -394,8 +394,10 @@
"label": "Språk"
},
"sponsorToken": {
"expires": "Sponsorsymbolet ditt utløper om {inXDays}. {getNewToken} og oppdater oppsettsfilen din.",
"getNew": "Få en ny token",
"expires": "Sponsorsymbolet ditt utløper om {inXDays}.",
"expiresUpdateUi": "{getNewToken} og oppdater oppsettsfilen din.",
"expiresUpdateYaml": "{getNewToken} og oppdater i evcc.yaml.",
"getNewToken": "Få en ny token",
"hint": "Merk: Dette vil bli automatisert i fremtiden."
},
"telemetry": {

View file

@ -847,8 +847,10 @@
"show": "Pokaż {title}"
},
"sponsorToken": {
"expires": "Twój token sponsora wygasa {inXDays}. {getNewToken} i zaktualizuj tutaj.",
"getNew": "Weź świeżego",
"expires": "Twój token sponsora wygasa {inXDays}.",
"expiresUpdateUi": "{getNewToken} i zaktualizuj tutaj.",
"expiresUpdateYaml": "{getNewToken} i zaktualizuj w swoim evcc.yaml.",
"getNewToken": "Weź świeżego",
"hint": "Uwaga: zautomatyzujemy to w przyszłości."
},
"telemetry": {

View file

@ -1161,8 +1161,10 @@
"show": "Mostrar {title}"
},
"sponsorToken": {
"expires": "O seu token de Apoiante (Sponsor) expira em {inXDays}. {getNewToken} e atualize-o aqui.",
"getNew": "Adquira um novo",
"expires": "O seu token de Apoiante (Sponsor) expira em {inXDays}.",
"expiresUpdateUi": "{getNewToken} e atualize-o aqui.",
"expiresUpdateYaml": "{getNewToken} e atualize-o no seu evcc.yaml.",
"getNewToken": "Adquira um novo",
"hint": "Nota: Vamos tornar isso automático no futuro."
},
"telemetry": {

View file

@ -408,8 +408,10 @@
"label": "Limba"
},
"sponsorToken": {
"expires": "Tokenul de sponsorizare expira {inXDays}. {getNewToken} si adauga-l in fisierul de configurare.",
"getNew": "Descarcati unul nou",
"expires": "Tokenul de sponsorizare expira {inXDays}.",
"expiresUpdateUi": "{getNewToken} si adauga-l in fisierul de configurare.",
"expiresUpdateYaml": "{getNewToken} si adauga-l în fișierul evcc.yaml.",
"getNewToken": "Descarcati unul nou",
"hint": "Nota: Vom automatiza acest proces in viitor."
},
"telemetry": {

View file

@ -1129,8 +1129,10 @@
"show": "Prikaži {title}"
},
"sponsorToken": {
"expires": "Sponzorskemu žetonu poteče veljavnost čez {inXDays}. {getNewToken} in ga posodobite tukaj.",
"getNew": "Zgrabi svežega",
"expires": "Sponzorskemu žetonu poteče veljavnost čez {inXDays}.",
"expiresUpdateUi": "{getNewToken} in ga posodobite tukaj.",
"expiresUpdateYaml": "{getNewToken} in ga posodobite v svoji evcc.yaml.",
"getNewToken": "Zgrabi svežega",
"hint": "Opomba: To bomo v prihodnosti avtomatizirali."
},
"telemetry": {

View file

@ -1163,8 +1163,10 @@
"show": "Visa {title}"
},
"sponsorToken": {
"expires": "Din sponsortoken löper ut om {inXDays}. {getNewToken} och uppdatera här.",
"getNew": "Hämta en ny",
"expires": "Din sponsortoken löper ut om {inXDays}.",
"expiresUpdateUi": "{getNewToken} och uppdatera här.",
"expiresUpdateYaml": "{getNewToken} och uppdatera i din evcc.yaml.",
"getNewToken": "Hämta en ny",
"hint": "Observera: Vi kommer att automatisera detta i framtiden."
},
"telemetry": {

View file

@ -1014,8 +1014,10 @@
"label": "மொழி"
},
"sponsorToken": {
"expires": "நீங்கள் ஒப்புரவாளர் கிள்ளாக்கு காலாவதியாகிறது {inXDays}. {getNewToken} மற்றும் அதை இங்கே புதுப்பிக்கவும்.",
"getNew": "ஒரு புதிய ஒன்றைப் பிடிக்கவும்",
"expires": "நீங்கள் ஒப்புரவாளர் கிள்ளாக்கு காலாவதியாகிறது {inXDays}.",
"expiresUpdateUi": "{getNewToken} மற்றும் அதை இங்கே புதுப்பிக்கவும்.",
"expiresUpdateYaml": "{getNewToken} மற்றும் உங்கள் evcc.yaml இல் புதுப்பிக்கவும்.",
"getNewToken": "ஒரு புதிய ஒன்றைப் பிடிக்கவும்",
"hint": "குறிப்பு: எதிர்காலத்தில் இதை தானியக்கமாக்குவோம்."
},
"telemetry": {

View file

@ -1161,8 +1161,10 @@
"show": "{title} göster"
},
"sponsorToken": {
"expires": "Sponsor jetonun {inXDays} sonra sona erecek. {getNewToken} ve burada güncelle.",
"getNew": "Yeni bir tane al",
"expires": "Sponsor jetonun {inXDays} sonra sona erecek.",
"expiresUpdateUi": "{getNewToken} ve burada güncelle.",
"expiresUpdateYaml": "{getNewToken} ve evcc.yaml dosyanızda güncelle.",
"getNewToken": "Yeni bir tane al",
"hint": "Not: İleride bunu otomatik hale getireceğiz."
},
"telemetry": {

View file

@ -1163,8 +1163,10 @@
"show": "Показати {title}"
},
"sponsorToken": {
"expires": "Термін дії вашого спонсорського токена закінчується {inXDays}. {getNewToken} і оновіть його тут.",
"getNew": "Візьми свіжий",
"expires": "Термін дії вашого спонсорського токена закінчується {inXDays}.",
"expiresUpdateUi": "{getNewToken} і оновіть його тут.",
"expiresUpdateYaml": "{getNewToken} і оновіть його у вашому evcc.yaml.",
"getNewToken": "Візьми свіжий",
"hint": "Примітка: Ми автоматизуємо це в майбутньому."
},
"telemetry": {

View file

@ -939,8 +939,10 @@
"label": "语言"
},
"sponsorToken": {
"expires": "您的赞助令牌将在 {inXDays} 后过期。{getNewToken} 并在此处更新。",
"getNew": "获取一个新的",
"expires": "您的赞助令牌将在 {inXDays} 后过期。",
"expiresUpdateUi": "{getNewToken} 并在此处更新。",
"expiresUpdateYaml": "{getNewToken} 并在你的 evcc.yaml 中更新。",
"getNewToken": "获取一个新的",
"hint": "注意:我们将来会自动化此过程。"
},
"telemetry": {

View file

@ -25,6 +25,8 @@ import (
"github.com/gorilla/mux"
)
type publisher func(string, any)
type route struct {
Method string
Pattern string
@ -221,7 +223,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
}
// RegisterSystemHandler provides system level handlers
func (s *HTTPd) RegisterSystemHandler(site *core.Site, valueChan chan<- util.Param, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string) {
func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string) {
router := s.Server.Handler.(*mux.Router)
// api
@ -288,8 +290,8 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, valueChan chan<- util.Par
"testconfig": {"POST", "/test/{class:[a-z]+}", testConfigHandler},
"testmerged": {"POST", "/test/{class:[a-z]+}/merge/{id:[0-9.]+}", testConfigHandler},
"interval": {"POST", "/interval/{value:[0-9.]+}", settingsSetDurationHandler(keys.Interval)},
"updatesponsortoken": {"POST", "/sponsortoken", updateSponsortokenHandler},
"deletesponsortoken": {"DELETE", "/sponsortoken", deleteSponsorTokenHandler},
"updatesponsortoken": {"POST", "/sponsortoken", updateSponsortokenHandler(pub)},
"deletesponsortoken": {"DELETE", "/sponsortoken", deleteSponsorTokenHandler(pub)},
}
// yaml handlers
@ -314,8 +316,8 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, valueChan chan<- util.Par
keys.Shm: func() any { return new(shm.Config) },
keys.Influx: func() any { return new(globalconfig.Influx) },
} {
routes["update"+key] = route{Method: "POST", Pattern: "/" + key, HandlerFunc: settingsSetJsonHandler(key, valueChan, fun)}
routes["delete"+key] = route{Method: "DELETE", Pattern: "/" + key, HandlerFunc: settingsDeleteJsonHandler(key, valueChan, fun())}
routes["update"+key] = route{Method: "POST", Pattern: "/" + key, HandlerFunc: settingsSetJsonHandler(key, pub, fun)}
routes["delete"+key] = route{Method: "DELETE", Pattern: "/" + key, HandlerFunc: settingsDeleteJsonHandler(key, pub, fun())}
}
for _, r := range routes {

View file

@ -9,33 +9,53 @@ import (
"github.com/evcc-io/evcc/util/sponsor"
)
func updateSponsortokenHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Token string `json:"token"`
}
func updateSponsortokenHandler(pub publisher) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if req.Token != "" {
if err := sponsor.ConfigureSponsorship(req.Token); err != nil {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
sponsor.SetFromYaml(false)
if req.Token != "" {
if err := sponsor.ConfigureSponsorship(req.Token); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
pub(keys.Sponsor, struct {
Status sponsor.Status `json:"status"`
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.GetStatus(),
FromYaml: false,
})
}
// TODO find better place
settings.SetString(keys.SponsorToken, req.Token)
setConfigDirty()
jsonWrite(w, sponsor.GetStatus())
}
// TODO find better place
settings.SetString(keys.SponsorToken, req.Token)
setConfigDirty()
jsonWrite(w, sponsor.Status())
}
func deleteSponsorTokenHandler(w http.ResponseWriter, r *http.Request) {
settings.SetString(keys.SponsorToken, "")
setConfigDirty()
jsonWrite(w, true)
func deleteSponsorTokenHandler(pub publisher) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
settings.SetString(keys.SponsorToken, "")
pub(keys.Sponsor, struct {
Status sponsor.Status `json:"status"`
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.Status{},
FromYaml: false,
})
setConfigDirty()
jsonWrite(w, true)
}
}

View file

@ -10,7 +10,6 @@ import (
"time"
"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/redact"
"github.com/gorilla/mux"
"go.yaml.in/yaml/v4"
@ -79,7 +78,7 @@ func settingsSetYamlHandler(key string, other, struc any) http.HandlerFunc {
}
}
func settingsSetJsonHandler(key string, valueChan chan<- util.Param, newStruc func() any) http.HandlerFunc {
func settingsSetJsonHandler(key string, pub publisher, newStruc func() any) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
struc := newStruc()
dec := json.NewDecoder(r.Body)
@ -103,18 +102,18 @@ func settingsSetJsonHandler(key string, valueChan chan<- util.Param, newStruc fu
settings.SetJson(key, struc)
setConfigDirty()
valueChan <- util.Param{Key: key, Val: struc}
pub(key, struc)
jsonWrite(w, true)
}
}
func settingsDeleteJsonHandler(key string, valueChan chan<- util.Param, struc any) http.HandlerFunc {
func settingsDeleteJsonHandler(key string, pub publisher, struc any) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
settings.SetString(key, "")
setConfigDirty()
valueChan <- util.Param{Key: key, Val: struc}
pub(key, struc)
jsonWrite(w, true)
}

View file

@ -15,7 +15,6 @@ import (
var (
mu sync.RWMutex
Subject, Token string
fromYaml bool = true
ExpiresAt time.Time
)
@ -36,13 +35,6 @@ func IsAuthorizedForApi() bool {
return IsAuthorized() && Subject != unavailable && Token != ""
}
// SetFromYaml sets whether the token comes from YAML config or database
func SetFromYaml(val bool) {
mu.Lock()
defer mu.Unlock()
fromYaml = val
}
// check and set sponsorship token
func ConfigureSponsorship(token string) error {
mu.Lock()
@ -98,16 +90,15 @@ func redactToken(token string) string {
return token[:6] + "......." + token[len(token)-6:]
}
type sponsorStatus struct {
type Status struct {
Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
ExpiresSoon bool `json:"expiresSoon,omitempty"`
Token string `json:"token,omitempty"`
FromYaml bool `json:"fromYaml"`
}
// Status returns the sponsorship status
func Status() sponsorStatus {
// GetStatus returns the sponsorship status
func GetStatus() Status {
mu.RLock()
defer mu.RUnlock()
@ -116,11 +107,10 @@ func Status() sponsorStatus {
expiresSoon = true
}
return sponsorStatus{
return Status{
Name: Subject,
ExpiresAt: ExpiresAt,
ExpiresSoon: expiresSoon,
Token: redactToken(Token),
FromYaml: fromYaml,
}
}